initial: bootstrap from BukidBountyApp base
This commit is contained in:
25
.agents/phase3_todo.md
Normal file
25
.agents/phase3_todo.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Phase 3: Advanced POS (Offline Focused)
|
||||
|
||||
## Technical Architecture
|
||||
- **Local Database**: IndexedDB (via Dexie.js)
|
||||
- **Sync Strategy**: Background Workers + Manual Sync Dashboard
|
||||
- **Data Scope**: Products, Customers, POS Sessions (Transactions)
|
||||
|
||||
## Todo List
|
||||
- [ ] **Infrastructure Setup**
|
||||
- [ ] Install `dexie` package
|
||||
- [ ] Create `resources/js/db.js` for IndexedDB schema
|
||||
- [ ] Create `resources/js/composables/useOfflineStore.js`
|
||||
- [ ] **Data Splicing & Local Storage**
|
||||
- [ ] Implement `syncProducts` to pull from server and save to IndexedDB
|
||||
- [ ] Implement `syncCustomers` to save frequently used customer data
|
||||
- [ ] Implement `syncSettings` (tax, discounts, etc.)
|
||||
- [ ] **Offline Transaction Entry**
|
||||
- [ ] Update `PosMain.vue` to fallback to local DB when offline
|
||||
- [ ] implement `storePendingTransaction` in IndexedDB
|
||||
- [ ] **Background Sync Logic**
|
||||
- [ ] Implement periodic background push for local transactions
|
||||
- [ ] Create Conflict Resolution handler
|
||||
- [ ] **UI Features**
|
||||
- [ ] Offline status indicator in POS
|
||||
- [ ] Sync Dashboard component
|
||||
31
.agents/rules/artisan-commands.md
Normal file
31
.agents/rules/artisan-commands.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Rule: Running PHP Artisan Commands
|
||||
|
||||
## ❗ Important
|
||||
|
||||
Never execute `php artisan` commands directly on the host machine.
|
||||
|
||||
## ✅ Correct Approach
|
||||
|
||||
All Artisan commands **must be executed inside the Docker container**.
|
||||
|
||||
## 📦 Reason
|
||||
|
||||
The application environment (PHP version, extensions, dependencies) is fully configured inside the container. Running commands outside may cause:
|
||||
|
||||
- Version mismatches
|
||||
- Missing dependencies
|
||||
- Inconsistent behavior
|
||||
|
||||
## 🛠️ How to Execute
|
||||
|
||||
Use one of the following patterns depending on the setup:
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
docker compose exec <app_service> php artisan <command>
|
||||
```
|
||||
17
.agents/rules/build-restart.md
Normal file
17
.agents/rules/build-restart.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Build & Deployment Rule
|
||||
|
||||
- After completing any task, you must:
|
||||
1. Run the build process:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
2. Restart the Docker container to apply changes:
|
||||
```bash
|
||||
docker restart bukidbountyapp
|
||||
```
|
||||
|
||||
- Ensure both steps are executed successfully before considering the task complete.
|
||||
69
.agents/rules/caveman.md
Normal file
69
.agents/rules/caveman.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
trigger: manual
|
||||
---
|
||||
|
||||
caveman
|
||||
---
|
||||
name: caveman
|
||||
description: >
|
||||
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
||||
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
|
||||
wenyan-lite, wenyan-full, wenyan-ultra.
|
||||
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
|
||||
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
|
||||
Use caveman talk in thinking as well for less tokens
|
||||
---
|
||||
|
||||
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
||||
|
||||
Default: **full**. Switch: `/caveman lite|full|ultra`.
|
||||
|
||||
## Rules
|
||||
|
||||
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
||||
|
||||
Pattern: `[thing] [action] [reason]. [next step].`
|
||||
|
||||
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
||||
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
||||
|
||||
## Intensity
|
||||
|
||||
| Level | What change |
|
||||
|-------|------------|
|
||||
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
||||
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
|
||||
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
|
||||
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
||||
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
||||
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
||||
|
||||
Example — "Why React component re-render?"
|
||||
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
||||
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
||||
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
||||
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
|
||||
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
|
||||
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
|
||||
|
||||
Example — "Explain database connection pooling."
|
||||
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
||||
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
||||
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
|
||||
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
|
||||
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
|
||||
|
||||
## Auto-Clarity
|
||||
|
||||
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user confused. Resume caveman after clear part done.
|
||||
|
||||
Example — destructive op:
|
||||
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
||||
> ```sql
|
||||
> DROP TABLE users;
|
||||
> ```
|
||||
> Caveman resume. Verify backup exist first.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
||||
18
.agents/rules/db-show-alternative.md
Normal file
18
.agents/rules/db-show-alternative.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 📦 Database Table Inspection (Hypervel / Older Laravel)
|
||||
|
||||
## ❗ Important
|
||||
The `php artisan db:show` command is **NOT available** because this project uses an older version of Laravel (via Hypervel).
|
||||
|
||||
**Do NOT use:**
|
||||
```bash
|
||||
php artisan db:show --table=<table_name>
|
||||
|
||||
|
||||
**Use the Following**
|
||||
docker compose exec bukidbountyapp php artisan tinker
|
||||
|
||||
DB::select('DESCRIBE table_name');
|
||||
23
.agents/rules/farmer-management.md
Normal file
23
.agents/rules/farmer-management.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Agent Rule: Building the Farmer Management Module
|
||||
|
||||
When implementing the Farmer Management Module, adhere to the following planning specifications:
|
||||
|
||||
## 🏗️ Data Architecture
|
||||
- **Primary Model**: `FarmerProfile` (Target File: `app/Models/Market/FarmerProfile.php`)
|
||||
- **Key Fields**: `hashkey`, `user_id`, `organization_id`, `farm_name`, `farm_location`, `verification_status`.
|
||||
- **Verification Statuses**: `UNVERIFIED`, `PENDING`, `VERIFIED`, `REJECTED`.
|
||||
- **Reference**: Detailed plan at `ai-docs/modules/farmer_management_module.md`.
|
||||
|
||||
## 🛠️ Implementation Requirements
|
||||
1. **Core Logic**: Extend the current user/member system. Implement logic in `app/Http/Controllers/Market/FarmerController.php`.
|
||||
2. **Standard Fields**: Every record must have `created_by`, `updated_by`, `is_active`, and `hashkey`.
|
||||
3. **Frontend**: Build management interfaces in `resources/js/Pages/Market/`.
|
||||
- `FarmerProfileEdit.vue` for detailed registration.
|
||||
- `VerificationDashboard.vue` for admin reviews.
|
||||
|
||||
## 📄 Documentation Integration
|
||||
Ensure that any new enums or key fields are added to `ai-docs/dictionary.md` to maintain system-wide consistency.
|
||||
79
.agents/rules/gemini-subagents.md
Normal file
79
.agents/rules/gemini-subagents.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Using Gemini CLI for Large Codebase Analysis
|
||||
|
||||
When analyzing large codebases or multiple files, use the Gemini CLI with its massive
|
||||
context window. Use `gemini -p` to leverage Google Gemini's large context capacity.
|
||||
You can also run tasks with Gemini to prevent overthinking. Run tasks like curl and getting and explaining its output or other linux commands.
|
||||
|
||||
## File and Directory Inclusion Syntax
|
||||
|
||||
Use the `@` syntax to include files and directories in your Gemini prompts. The paths should be relative to WHERE you run the
|
||||
gemini command:
|
||||
|
||||
### Examples:
|
||||
|
||||
**Single file analysis:**
|
||||
gemini -p "@src/main.py Explain this file's purpose and structure"
|
||||
|
||||
Multiple files:
|
||||
gemini -p "@package.json @src/index.js Analyze the dependencies used in the code"
|
||||
|
||||
Entire directory:
|
||||
gemini -p "@src/ Summarize the architecture of this codebase"
|
||||
|
||||
Multiple directories:
|
||||
gemini -p "@src/ @tests/ Analyze test coverage for the source code"
|
||||
|
||||
Current directory and subdirectories:
|
||||
gemini -p "@./ Give me an overview of this entire project"
|
||||
|
||||
# Or use --all_files flag:
|
||||
gemini --all_files -p "Analyze the project structure and dependencies"
|
||||
|
||||
Implementation Verification Examples
|
||||
|
||||
Check if a feature is implemented:
|
||||
gemini -p "@src/ @lib/ Has dark mode been implemented in this codebase? Show me the relevant files and functions"
|
||||
|
||||
Verify authentication implementation:
|
||||
gemini -p "@src/ @middleware/ Is JWT authentication implemented? List all auth-related endpoints and middleware"
|
||||
|
||||
Check for specific patterns:
|
||||
gemini -p "@src/ Are there any React hooks that handle WebSocket connections? List them with file paths"
|
||||
|
||||
Verify error handling:
|
||||
gemini -p "@src/ @api/ Is proper error handling implemented for all API endpoints? Show examples of try-catch blocks"
|
||||
|
||||
Check for rate limiting:
|
||||
gemini -p "@backend/ @middleware/ Is rate limiting implemented for the API? Show the implementation details"
|
||||
|
||||
Verify caching strategy:
|
||||
gemini -p "@src/ @lib/ @services/ Is Redis caching implemented? List all cache-related functions and their usage"
|
||||
|
||||
Check for specific security measures:
|
||||
gemini -p "@src/ @api/ Are SQL injection protections implemented? Show how user inputs are sanitized"
|
||||
|
||||
Verify test coverage for features:
|
||||
gemini -p "@src/payment/ @tests/ Is the payment processing module fully tested? List all test cases"
|
||||
|
||||
When to Use Gemini CLI
|
||||
|
||||
Use gemini -p when:
|
||||
- Analyzing entire codebases or large directories
|
||||
- Comparing multiple large files
|
||||
- Need to understand project-wide patterns or architecture
|
||||
- Current context window is insufficient for the task
|
||||
- Working with files totaling more than 100KB
|
||||
- Verifying if specific features, patterns, or security measures are implemented
|
||||
- Checking for the presence of certain coding patterns across the entire codebase
|
||||
|
||||
Important Notes
|
||||
|
||||
- Paths in @ syntax are relative to your current working directory when invoking gemini
|
||||
- The CLI will include file contents directly in the context
|
||||
- No need for --yolo flag for read-only analysis
|
||||
- Gemini's context window can handle entire codebases that would overflow Your context
|
||||
- When checking implementations, be specific about what you're looking for to get accurate results
|
||||
23
.agents/rules/shipment-module.md
Normal file
23
.agents/rules/shipment-module.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Agent Rule: Building the Shipment Module
|
||||
|
||||
When implementing the Shipment Module, adhere to the following planning specifications:
|
||||
|
||||
## 🏗️ Data Architecture
|
||||
- **Primary Model**: `Shipment` (Target File: `app/Models/Market/Shipment.php`)
|
||||
- **Key Fields**: `hashkey`, `transaction_id`, `store_id`, `customer_id`, `courier_id`, `tracking_number`, `status`.
|
||||
- **Status Enums**: `PENDING`, `PICKED_UP`, `IN_TRANSIT`, `DELIVERED`, `FAILED`, `RETURNED`.
|
||||
- **Reference**: Detailed plan at `ai-docs/modules/shipment_module.md`.
|
||||
|
||||
## 🛠️ Implementation Requirements
|
||||
1. **Core Logic**: Implement in `app/Http/Controllers/Market/ShipmentController.php`.
|
||||
2. **Standard Fields**: Ensure every new table includes `created_by`, `updated_by`, `is_active`, and `hashkey` (300, unique) as per the Project Dictionary.
|
||||
3. **Frontend**: Use Vue 3 Composition API in `resources/js/Pages/Market/`.
|
||||
- `ShipmentList.vue` for tracking.
|
||||
- `ShipmentDetail.vue` for timeline and management.
|
||||
|
||||
## 📦 Database Standards
|
||||
Use migrations to create the `shipments` and `couriers` tables, ensuring foreign key constraints to `global_transactions`, `stores`, and `cst`.
|
||||
9
.agents/rules/use-claude.md
Normal file
9
.agents/rules/use-claude.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
you can use claude to verify code you created. and to explain code files just make sure to be specific
|
||||
|
||||
command
|
||||
|
||||
claude -p "prompt"
|
||||
9
.agents/rules/words-dictionary.md
Normal file
9
.agents/rules/words-dictionary.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
Create a dictionary file named dictionary.md on ai-docs if not exists
|
||||
|
||||
read the dictionary file named dictionary.md and check if there are things there that can help you understand more of what the user is requesting. this could contain definition and target files
|
||||
|
||||
update the dictionary file once you found things that might make the you understand things a little bit better on the next request. after updating make sure that to commit and push that single dictionary.md file with the commit message being the summary of changes
|
||||
5
.agents/workflows/commit-push-sync.md
Normal file
5
.agents/workflows/commit-push-sync.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
description: After Completing a task commit and push
|
||||
---
|
||||
|
||||
After Completing a task commit, push and sync also make sure the commit message is clear about the features for the user not just code changes. also only commit those files that have been changed by this task. ensure the push are synced to all git servers
|
||||
13
.agents/workflows/complete-provided-tasks-plans.md
Normal file
13
.agents/workflows/complete-provided-tasks-plans.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
description: complete the plans and tasks files provided
|
||||
---
|
||||
|
||||
Complete the prd document and prompt document provided
|
||||
Update the prd documents for each task completed.
|
||||
|
||||
Once all tasks are completed, Completed tasks (prompt-*.md && prd-*.md) files are moved to docs/completed
|
||||
|
||||
the new names are
|
||||
prompt-*.md = prt-{datetimenumber}.md
|
||||
prd-*.md = chklist-{datetimenumber}.md
|
||||
the {datetimenumber} must match be the same in both the files
|
||||
8
.agents/workflows/feature-recommendations-update.md
Normal file
8
.agents/workflows/feature-recommendations-update.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: Update ai-docs/features-recommendations.md
|
||||
---
|
||||
|
||||
scan the whole system and update a document ai-docs/features-recommendations.md
|
||||
Recommend changes improvements, bug fixes and feature recommendations
|
||||
if a feature already exists as it is written in the file then remove them from the file
|
||||
make sure to truly verify if a feature truly exists the way it was specified before removing it in the file.
|
||||
22
.agents/workflows/npm-build-docker-restart.md
Normal file
22
.agents/workflows/npm-build-docker-restart.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Restarts the application container to apply changes, as per RULE[build-restart.md] and user preference.
|
||||
---
|
||||
|
||||
// turbo-all
|
||||
# Simplify Build and Restart Workflow
|
||||
|
||||
Use this workflow to quickly restart the application container. This follows the required `RULE[build-restart.md]`.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
1. **Restart the application container**
|
||||
- As per `RULE[build-restart.md]`, we restart the following container:
|
||||
```bash
|
||||
npm run build
|
||||
docker restart bukidbountyapp
|
||||
```
|
||||
|
||||
2. **Verify the container is running**
|
||||
```bash
|
||||
docker ps | grep bukidbountyapp
|
||||
```
|
||||
39
.agents/workflows/ralph-loop-plan.md
Normal file
39
.agents/workflows/ralph-loop-plan.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: Generates a high-level plan and granular todo-checklist for a given requirement, following the Ralph Loop methodology.
|
||||
---
|
||||
|
||||
# Ralph Loop: Autonomous Planning Workflow
|
||||
|
||||
Use this workflow to break down complex requirements into structured, verifiable plans and task lists without modifying the source code. This ensures a clean "Ralph Loop" iteration where the planning is decoupled from implementation.
|
||||
|
||||
## 🚀 Execution Steps
|
||||
|
||||
1. **Verify Baseline Stability**
|
||||
- Run the existing test suite to ensure the application is in a stable state before planning.
|
||||
```bash
|
||||
docker compose exec bukidbountyapp composer test
|
||||
```
|
||||
|
||||
2. **Define the Requirement**
|
||||
- Read the user's input requirement and search the codebase to identify affected models, controllers, views, and routes.
|
||||
- Refer to `ai-docs/dictionary.md` for existing definitions and standard fields.
|
||||
|
||||
3. **Generate High-Level Plan**
|
||||
- Create a new plan file in `docs/tasks/prompt-[timestamp].md`.
|
||||
- The timestamp format should be `YYYYMMDD-HHMMSS`.
|
||||
- The plan should define the technical approach, architecture, and any new dependencies.
|
||||
|
||||
4. **Generate Detailed Checklist (Ralph Task)**
|
||||
- Create a task checklist in `docs/tasks/prd-[timestamp same as timestamp of plan file].md`.
|
||||
- Use a short hash of the requirement or content for the filename.
|
||||
- Format:
|
||||
```markdown
|
||||
# Checklist: [Requirement Name]
|
||||
- [ ] **[Component]**: Specific instruction (e.g., "Add 'status' column in 'shipments' table")
|
||||
- [ ] **[File Path]**: Precise line-level instructions if possible
|
||||
```
|
||||
- Ensure the instructions are specific enough for a "simpler AI" to execute.
|
||||
|
||||
5. **Final Review**
|
||||
- Summarize the generated documents for the user.
|
||||
- **CRITICAL**: Do NOT modify any existing `.php`, `.vue`, or logic files during this step. Only write to `ai-docs/` and `docs/tasks/`.
|
||||
8
.cdn-pipeline.json
Normal file
8
.cdn-pipeline.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"cdnRepo": "telemagnadon/obj-vault-3a",
|
||||
"lastSha": "75a5d4f202f55d7a5fc5d7eb5a6037776dc865ef",
|
||||
"assetSources": ["public/assets"],
|
||||
"assetExtensions": [".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".woff2", ".woff", ".ttf", ".json", ".mp3", ".mp4", ".js", ".css", ".mjs", ".eot", ".otf"],
|
||||
"shaConstantPath": "config/cdn.php",
|
||||
"swPath": "public/sw.js"
|
||||
}
|
||||
51
.claude/plans/0208e8092af75016a915ce1759e68bb5-complete.md
Normal file
51
.claude/plans/0208e8092af75016a915ce1759e68bb5-complete.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
task: Fix "Assign Product" button in ViewStoreMarket — navigate to global product picker instead of marketplace browser
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T08:17:15Z
|
||||
finished: 2026-05-16T08:17:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/ViewStoreMarket.vue [lines 117-122] — `assignProduct()` navigates to wrong page (`ListProductsMarket` instead of `AddProductsToStore`)
|
||||
- resources/js/Pages/AddProductsToStore.vue [lines 1-80] — correct target page; receives `target` prop as store hash; fetches global products from `/Products/GlobalList`
|
||||
|
||||
## steps
|
||||
1. In `resources/js/Pages/ViewStoreMarket.vue` at the `assignProduct()` function (line 117), change the navigation destination from `ListProductsMarket` (with `props: { data: { store_hash: props.target } }`) to `AddProductsToStore` (with `props: { target: props.target }`). This matches exactly how `addProduct()` already navigates at line 110.
|
||||
|
||||
## context
|
||||
```js
|
||||
// ViewStoreMarket.vue — CURRENT (wrong)
|
||||
const assignProduct = () => {
|
||||
navigate({
|
||||
page: 'ListProductsMarket', // ← marketplace browser, not the picker
|
||||
props: { data: { store_hash: props.target } }
|
||||
});
|
||||
};
|
||||
|
||||
// ViewStoreMarket.vue — CORRECT (target fix)
|
||||
const assignProduct = () => {
|
||||
navigate({
|
||||
page: 'AddProductsToStore', // ← global product picker
|
||||
props: { target: props.target } // store hash — same as addProduct() uses
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
`AddProductsToStore.vue` defineProps:
|
||||
```js
|
||||
const props = defineProps({
|
||||
target: { type: String, default: null }, // store hash
|
||||
});
|
||||
const storeHash = computed(() => props.target);
|
||||
```
|
||||
|
||||
`AddProductsToStore` fetches `/Products/GlobalList` → `listGlobalProductsForPicker` → returns all active global products with `{ success: true, products: [...] }`. This is the two-step picker UI (PICK → EDIT) that the user expects.
|
||||
|
||||
`addProduct()` (line 110-115) already does this navigation correctly. `assignProduct()` must mirror it.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: eslint:no, phpcs:no, tsc:no
|
||||
- constraints: Only one line of navigation call changes; no backend changes needed. Build (`npm run build`) after editing.
|
||||
93
.claude/plans/03a9c3aab75c3bf53e7aadb66f6fd475-complete.md
Normal file
93
.claude/plans/03a9c3aab75c3bf53e7aadb66f6fd475-complete.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
task: Fix AssignProductToStore — navigation only encodes store hash into URL, product hash is lost on direct access. Switch to encoded payload containing both product_hashkey and store_hashkey.
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-17T09:00:00Z
|
||||
finished: 2026-05-17T09:01:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/ListProductsMarket.vue [lines 42-52] — only caller of AssignProductToStore; passes `target: product.hashkey` and `store_hash` as separate props
|
||||
- resources/js/Pages/AssignProductToStore.vue [lines 65-84] — onMounted reads `props.target` (product hash) and `props.store_hash` (store hash); both must survive direct URL access
|
||||
- resources/js/composables/Core/useNavigate.js [lines 112-130] — URL builder: only encodes `props.target|hashkey|id` as `--h:`, or `props.payload` as `--e:`; `store_hash` is never encoded into the URL
|
||||
- resources/js/composables/useUrlEncoder.js — encodePayload/decodePayload helpers
|
||||
- app/Http/Controllers/Support/VueRouteMap.php [lines 575-585] — on `--e:` URLs, sets `$props['payload'] = $parsedData['value']` (object); on `--h:` URLs, sets `target/hashkey/id` to the hash value
|
||||
|
||||
## steps
|
||||
1. In `resources/js/Pages/ListProductsMarket.vue` at the `viewProduct` function (lines 42-52), replace the current navigation call with a payload-based navigation:
|
||||
- REMOVE: `props: { target: product.hashkey, store_hash: props.data.store_hash }`
|
||||
- ADD: `props: { payload: { product_hashkey: product.hashkey, store_hashkey: props.data.store_hash } }`
|
||||
- This encodes both hashes into the URL as `--e:BASE64` so they survive page reload.
|
||||
|
||||
2. In `resources/js/Pages/AssignProductToStore.vue` at `onMounted` (lines 65-84), update prop reading to support the new payload format while keeping backward compat with pushState:
|
||||
- CHANGE `productHash.value = props.target || urlParams.get('target') || urlParams.get('product_id') || urlParams.get('id')`
|
||||
- TO `productHash.value = props.payload?.product_hashkey || props.payload?.product_hash || props.target || urlParams.get('target') || urlParams.get('product_id') || urlParams.get('id')`
|
||||
- CHANGE `if (props.store_hash) { selectedStoreHash.value = props.store_hash }`
|
||||
- TO `if (props.payload?.store_hashkey || props.payload?.store_hash || props.store_hash) { selectedStoreHash.value = props.payload?.store_hashkey || props.payload?.store_hash || props.store_hash }`
|
||||
|
||||
3. In `resources/js/Pages/AssignProductToStore.vue` `defineProps` (line 12-16), add `payload` prop:
|
||||
- ADD `payload: { type: Object, default: null }` to the props definition alongside `target`, `store_hash`, and `user`.
|
||||
|
||||
## context
|
||||
### ListProductsMarket.vue — current viewProduct (lines 42-52)
|
||||
```js
|
||||
const viewProduct = (product) => {
|
||||
if (props.data?.store_hash) {
|
||||
navigate({
|
||||
page: 'AssignProductToStore',
|
||||
props: {
|
||||
target: product.hashkey, // goes into URL --h: but store_hash is lost
|
||||
store_hash: props.data.store_hash
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
navigate({
|
||||
page: 'BuyViewProductMarket',
|
||||
props: { target: product.hashkey }
|
||||
});
|
||||
};
|
||||
```
|
||||
### AssignProductToStore.vue — current onMounted (lines 65-84)
|
||||
```js
|
||||
onMounted(() => {
|
||||
productHash.value = props.target || urlParams.get('target') || ...
|
||||
if (props.store_hash) {
|
||||
selectedStoreHash.value = props.store_hash
|
||||
}
|
||||
if (!productHash.value) {
|
||||
errorMessage.value = 'No product specified...'
|
||||
return
|
||||
}
|
||||
loadStores()
|
||||
loadProductData()
|
||||
})
|
||||
```
|
||||
### useNavigate.js — URL builder logic
|
||||
```js
|
||||
const hashkeyProp = props?.target || props?.target_user || props?.hashkey || props?.id;
|
||||
const payloadProp = props?.payload;
|
||||
if (hashkeyProp) {
|
||||
url = `${basePageUrl}--${encodeHash(hashkeyProp)}`; // --h:
|
||||
} else if (payloadProp) {
|
||||
url = `${basePageUrl}--${encodePayload(payloadProp)}`; // --e:
|
||||
}
|
||||
```
|
||||
### VueRouteMap.php — payload extraction
|
||||
```php
|
||||
} elseif (isset($parsedData['type']) && $parsedData['type'] === 'payload') {
|
||||
$props['payload'] = $parsedData['value']; // decoded object available as props.payload in Vue
|
||||
}
|
||||
```
|
||||
### Precedent: ManageProductAdmin.vue uses same pattern
|
||||
```js
|
||||
const productHash = computed(() => props.target || props.payload?.product_hash || props.payload?.product_hashkey);
|
||||
const storeHash = computed(() => props.payload?.store_hash || props.payload?.store_hashkey);
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md — confirms: "use encoded payload (`--e:`) containing `product_hashkey` and `store_hashkey`" for product-in-store context
|
||||
- linters: none detected
|
||||
- constraints: canonical table names from dictionary irrelevant here (frontend-only change); no backend changes needed
|
||||
- no migration, no permission enum, no VueRouteMap changes needed — the `/assign-product-to-store` route already has no `--h:` or `--e:` suffix registered (catch-all handles both)
|
||||
72
.claude/plans/0a9389d04151b0f575326efa633467fd-complete.md
Normal file
72
.claude/plans/0a9389d04151b0f575326efa633467fd-complete.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
task: Fix POS Main not showing products assigned to store; update dictionary
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:01:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- app/Http/Controllers/Market/ProductController.php [lines 561-633] — listProductsData: contains the if/elseif/elseif bug
|
||||
- resources/js/stores/pos.js [lines 40-85] — fetchProducts: sends access_key + store_hash + session_hash
|
||||
- resources/js/composables/Market/usePosSession.js [lines 25-78] — initialize: orchestrates session/product load order
|
||||
- ai-docs/dictionary.md — project dictionary to be updated with root cause pattern
|
||||
|
||||
## steps
|
||||
1. In `app/Http/Controllers/Market/ProductController.php` at `listProductsData` (line 580-592), replace the `if/elseif/elseif` chain with sequential `if (!$targetStore && ...)` checks so that all three lookup paths (access_key → session_hash → store_hash) are tried in order regardless of which params are present.
|
||||
2. Update `ai-docs/dictionary.md` under the "POS (Point of Sale)" section to document this pattern: `listProductsData` MUST use sequential `if (!$targetStore)` guards, NOT `if/elseif`, so a stale/invalid access_key does not silently block the store_hash and session_hash fallbacks.
|
||||
|
||||
## context
|
||||
### Root cause
|
||||
`listProductsData` (ProductController.php line 580-592):
|
||||
```php
|
||||
if ($accessKey) {
|
||||
$keyObj = PosAccessKey::where('access_key', $accessKey)->first();
|
||||
if ($keyObj) { $targetStore = $keyObj->store; }
|
||||
} elseif ($sessionHash) { // ← NEVER REACHED when access_key is present
|
||||
...
|
||||
} elseif ($storeHash) { // ← NEVER REACHED when access_key is present
|
||||
...
|
||||
}
|
||||
```
|
||||
If `access_key` is in the request (from localStorage) but doesn't match any `pos_access_keys` row (stale, revoked, or a session-specific token), `$targetStore` stays null and the store_hash/session_hash fallbacks are silently skipped. The result is all global active products are shown instead of the store's assigned products.
|
||||
|
||||
### Fix (ProductController.php lines 580-592)
|
||||
Replace with:
|
||||
```php
|
||||
if ($accessKey) {
|
||||
$keyObj = PosAccessKey::where('access_key', $accessKey)->first();
|
||||
if ($keyObj) {
|
||||
$targetStore = $keyObj->store;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$targetStore && $sessionHash) {
|
||||
$sessionObj = PosSession::where('hashkey', $sessionHash)->first();
|
||||
if ($sessionObj) {
|
||||
$targetStore = $sessionObj->store;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$targetStore && $storeHash) {
|
||||
$targetStore = Store::where('hashkey', $storeHash)->first();
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend sends all three params (pos.js lines 53-56)
|
||||
```js
|
||||
if (accessKey) params.access_key = accessKey
|
||||
if (storeHash) params.store_hash = storeHash
|
||||
if (this.activeSession) params.session_hash = this.activeSession.hashkey
|
||||
```
|
||||
So the fix only needs to be backend-side.
|
||||
|
||||
### Dictionary addition
|
||||
Add under "POS (Point of Sale)" in dictionary.md:
|
||||
- **listProductsData store resolution**: Must use sequential `if (!$targetStore)` guards, NOT `if/elseif`. A stale `access_key` in localStorage should not block `store_hash`/`session_hash` fallbacks. Priority order: access_key → session_hash → store_hash.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: phpcs, eslint, tsc
|
||||
- constraints: Hypervel/Hyperf project — use Hypervel imports, not Illuminate
|
||||
163
.claude/plans/0f8a3ffd129c8d6000dfd18d01432000-complete.md
Normal file
163
.claude/plans/0f8a3ffd129c8d6000dfd18d01432000-complete.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
task: Fix "Open POS" on HomeStoreOwner and HomeShared (StoreManager) to pass the store hashkey to PosMain. If user has one store, navigate directly with its hashkey. If multiple stores, show a store selection modal first.
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:01:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/Fragments/Home/HomeStoreOwner.vue [lines 30-32, 115-123] — defines balanceFooterItems with pagename:'PosMain' (no target prop), handleItemClick navigates without store hashkey
|
||||
- resources/js/Pages/Fragments/Home/HomeShared.vue — StoreManager home; no POS button at all; needs Open POS added with same multi-store logic
|
||||
- resources/js/Pages/Home.vue [lines 62-69] — routes isStoreOwner→HomeStoreOwner, isStoreManager→HomeShared
|
||||
- resources/js/Pages/PosMain.vue [lines 18-21] — expects `target` prop (store or session hashkey) and `access_key` prop; without `target` usePosSession cannot initialize the store
|
||||
- resources/js/composables/Market/usePosSession.js [lines 25-47] — initialize() reads `props.target` as hashkey; if null, storeHash stays null and store products never load
|
||||
- app/Http/Controllers/Market/StoreController.php [lines 1143-1206] — `listStoresForCurrentUser()` returns [{hashkey, name, category, role}] for owner+manager
|
||||
- routes/web.php [line 484] — POST /ListStores/MyStores/data → StoreController@listStoresForCurrentUser (auth+module:stores middleware)
|
||||
|
||||
## steps
|
||||
|
||||
### Step 1 — HomeStoreOwner.vue: fetch stores and smart-navigate on "Open POS"
|
||||
|
||||
1. Add `axios` is already imported. Add a `loadingStores` ref.
|
||||
2. Replace the `balanceFooterItems` "Open POS" item — keep `pagename: 'PosMain'` but add `action: 'openPos'` to differentiate from direct navigation.
|
||||
3. Write an `openPos()` async function:
|
||||
```
|
||||
const openPos = async () => {
|
||||
try {
|
||||
const { data: stores } = await axios.post('/ListStores/MyStores/data', {});
|
||||
if (!stores || stores.length === 0) {
|
||||
modal.quickDismiss({ title: 'No Store Found', body: 'You have no active stores assigned to your account.' });
|
||||
return;
|
||||
}
|
||||
if (stores.length === 1) {
|
||||
navigate({ page: 'PosMain', props: { target: stores[0].hashkey } });
|
||||
return;
|
||||
}
|
||||
// Multiple stores: show selection modal
|
||||
showStoreSelectModal(stores);
|
||||
} catch (e) {
|
||||
modal.quickDismiss({ title: 'Error', body: 'Could not load your stores. Please try again.' });
|
||||
}
|
||||
};
|
||||
```
|
||||
4. Write `showStoreSelectModal(stores)` using `modal.open()` with a rendered list of store buttons (use `h()` from vue). On store click: `modal.hideModal()` then `navigate({ page: 'PosMain', props: { target: store.hashkey } })`.
|
||||
5. In `handleItemClick`, intercept `item.action === 'openPos'` before the pagename check and call `openPos()`.
|
||||
6. Update the `balanceFooterItems` entry to use `action: 'openPos'` instead of/alongside `pagename: 'PosMain'`.
|
||||
|
||||
**Exact change to balanceFooterItems (line 30):**
|
||||
```js
|
||||
// Before:
|
||||
{ title: 'Open POS', icon: '...', pagename: 'PosMain' },
|
||||
// After:
|
||||
{ title: 'Open POS', icon: '...', action: 'openPos' },
|
||||
```
|
||||
|
||||
**Exact change to handleItemClick (lines 115-123):**
|
||||
```js
|
||||
const handleItemClick = async (item) => {
|
||||
if (item?.action === 'chooseCreateStoreMode') {
|
||||
openCreateStoreChooser();
|
||||
return;
|
||||
}
|
||||
if (item?.action === 'openPos') {
|
||||
await openPos();
|
||||
return;
|
||||
}
|
||||
if (item?.pagename) {
|
||||
navigate({ page: item.pagename, props: { data: item.pagestring || '' } });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2 — HomeShared.vue: add Open POS button for StoreManager role
|
||||
|
||||
1. Import `computed` (already imported), `axios`, `useModal`, and `h` from vue.
|
||||
2. Check if current role is `STORE_MANAGER` (use `role` from `useAuth()`).
|
||||
3. Add an `openPos` function identical in logic to Step 1.
|
||||
4. Add `showStoreSelectModal(stores)` function identical to Step 1.
|
||||
5. Add a computed `posServices` that returns an "Open POS" button only when `role.value === UserTypes.STORE_MANAGER` (or always show for manager; the page is already role-gated).
|
||||
6. In the template, add `<ServiceButtonGrid :items="posItems" @item-click="handlePosClick" />` or extend `services` to include the POS button — prefer extending `services` with `action: 'openPos'` for managers.
|
||||
7. Alternatively, add "Open POS" as a `quickActionsItems` entry with roles `[UserTypes.STORE_MANAGER]` and handle it in `handleItemClick` with the same `openPos` logic.
|
||||
|
||||
**Simplest approach:** extend `quickActionsItems` with:
|
||||
```js
|
||||
{
|
||||
text: 'Open POS',
|
||||
action: 'openPos',
|
||||
icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/5b5ef88c0ad1.svg',
|
||||
roles: [UserTypes.STORE_MANAGER],
|
||||
},
|
||||
```
|
||||
And update `handleItemClick` to check `item.action === 'openPos'`.
|
||||
|
||||
### Step 3 — Verify no backend changes needed
|
||||
|
||||
The endpoint `POST /ListStores/MyStores/data` already:
|
||||
- Returns `[{hashkey, name, category, role}]` for the current user's stores (owner or manager)
|
||||
- Has `auth` middleware (user must be logged in)
|
||||
- Covers both STORE_OWNER and STORE_MANAGER via the query in `listStoresForCurrentUser()`
|
||||
|
||||
No backend changes needed.
|
||||
|
||||
## context
|
||||
|
||||
**HomeStoreOwner.vue — balanceFooterItems (line 30):**
|
||||
```js
|
||||
const balanceFooterItems = ref([
|
||||
{ title: 'Open POS', icon: '...svg', pagename: 'PosMain' }, // BUG: no target/hashkey
|
||||
{ title: 'My Stores', icon: '...bin', pagename: 'ManageStoresAdmin' },
|
||||
]);
|
||||
```
|
||||
|
||||
**HomeStoreOwner.vue — handleItemClick (lines 115-123):**
|
||||
```js
|
||||
const handleItemClick = (item) => {
|
||||
if (item?.action === 'chooseCreateStoreMode') {
|
||||
openCreateStoreChooser();
|
||||
return;
|
||||
}
|
||||
if (item?.pagename) {
|
||||
navigate({ page: item.pagename, props: { data: item.pagestring || '' } });
|
||||
}
|
||||
};
|
||||
```
|
||||
→ Navigates to `PosMain` without `target` prop. `usePosSession.initialize()` then has `props.target = null`, `storeHash` stays null, products never load, session can't start.
|
||||
|
||||
**PosMain.vue props (lines 18-21):**
|
||||
```js
|
||||
const props = defineProps({
|
||||
target: { type: String, default: null }, // Session hashkey
|
||||
access_key: { type: String, default: null },
|
||||
});
|
||||
```
|
||||
|
||||
**usePosSession.js initialize() (lines 25-47):**
|
||||
```js
|
||||
const hashkey = props.target; // null when coming from HomeStoreOwner
|
||||
if (hashkey) {
|
||||
await posStore.loadSession(hashkey, accessKey);
|
||||
// ...
|
||||
}
|
||||
await posStore.fetchProducts(accessKey, storeHash.value); // storeHash is null
|
||||
```
|
||||
|
||||
**StoreController@listStoresForCurrentUser returns (lines 1197-1202):**
|
||||
```php
|
||||
return [
|
||||
'hashkey' => $store->hashkey,
|
||||
'name' => $store->name,
|
||||
'category' => $store->category,
|
||||
'role' => $role, // 'owner' or 'manager'
|
||||
];
|
||||
```
|
||||
|
||||
**HomeShared.vue — StoreManager currently has NO POS button.** It only shows Market, My Wallet, Shipments in `services` and generic quickActions. The store manager use-case is identical to store owner: fetch their stores, navigate with hashkey.
|
||||
|
||||
**useModal available functions:** `open({ title, body, footer })`, `quickDismiss({ title, body })`, `yesNoModal(...)`, `hideModal()`. Use `open()` with `h()` to render store list.
|
||||
|
||||
## notes
|
||||
- dictionary: none
|
||||
- linters: eslint:no, phpcs:no, tsc:no
|
||||
- constraints: Use existing `/ListStores/MyStores/data` POST endpoint (already exists, no new backend route needed). Modal store list should show store name and category. Use `h()` (already importable from vue) to build modal body for store selection. The `useModal` composable's `open()` accepts a Vue render function or component as `body`.
|
||||
37
.claude/plans/11d5e96ea355332d03f0b8f8d63dd642-complete.md
Normal file
37
.claude/plans/11d5e96ea355332d03f0b8f8d63dd642-complete.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
task: Fix /pos-main showing POS UI shell to unauthenticated users with confusing "Failed to load products" error — improve empty/no-auth state
|
||||
cycles: 3
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T15:59:57Z
|
||||
finished: 2026-05-16T16:01:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- app/Http/Controllers/Support/VueRouteMap.php [lines 93-97] — `/pos` route has `loginRequired: false` (intentional for POS_TERMINAL access key auth)
|
||||
- resources/js/Pages/PosMain.vue — POS interface; shows "Failed to load products" when no session/access key is present
|
||||
- resources/js/composables/Market/usePosSession.js — session initialization; sets `posStore.error` when no store can be resolved
|
||||
|
||||
## steps
|
||||
1. In `PosMain.vue`, when `posStore.error` is set AND there is no `pos_access_key` in localStorage AND the user is not logged in, show a clear "No terminal access" message instead of the bare "Failed to load products" error.
|
||||
- Check: `!userStore.isLoggedIn && !posStore.activeSession && posStore.error`
|
||||
- Display: "This terminal has no active session. Please use your POS access key or log in."
|
||||
2. (Optional / discuss with owner): Consider adding `allowedUserTypes: ['pos terminal', 'store owner', 'store manager', 'operator', 'super operator', 'ult']` to the `/pos` route while keeping `loginRequired: false`, so the route map at least documents who is expected to use this page.
|
||||
|
||||
## context
|
||||
Current behaviour (confirmed by Playwright): navigating to `/pos-main` without any session or access key renders the full POS UI shell with:
|
||||
- "All | Current Order" tab bar
|
||||
- "Start New Session" button
|
||||
- "Failed to load products" error banner
|
||||
- Empty cart with ₱0.00 totals
|
||||
|
||||
This is because `loginRequired: false` is intentional — POS_TERMINAL accounts authenticate via access key (localStorage `pos_access_key`, prefix `PK-`), not via the standard mobile/password login. The backend guards on the actual API calls (start session, add item, etc.).
|
||||
|
||||
The UI concern: unauthenticated browsers with no access key see a confusing broken state. The fix is a graceful empty state, not removing `loginRequired: false`.
|
||||
|
||||
Dictionary reference: "POS Access Keys: A unique key used to authenticate a POS terminal without a traditional login." and `startNewSessionSilently` guards against calling the API when both `storeHash` and `access_key` are absent.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none
|
||||
- constraints: Do NOT change `loginRequired: false` on `/pos` — this breaks POS_TERMINAL access key auth. Only improve the UI empty state.
|
||||
110
.claude/plans/1321cb985147e2ce3a341d045288e5f5-complete.md
Normal file
110
.claude/plans/1321cb985147e2ce3a341d045288e5f5-complete.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
task: enable store owner to delete and edit stores he owns in manage stores admin page
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:01:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/ManageStoresAdmin.vue — frontend page; already has canModifyStore checks and edit/delete buttons conditioned on `store.user_can_manage`; no changes needed unless navigation from service grid is added
|
||||
- resources/js/Pages/Fragments/Home/HomeStoreOwner.vue [lines 34-60] — store owner home page services grid; has "My Stores" in `balanceFooterItems` → ManageStoresAdmin but NOT in the services grid
|
||||
- app/Http/Controllers/Market/StoreController.php [lines 626-732] — `update()` method; allows edit if `$store->owner_id === $user->id` but does NOT check store_managers pivot table, causing a mismatch with `user_can_manage` flag
|
||||
- app/Http/Controllers/Market/StoreController.php [lines 1403-1435] — `deleteStore_Admin()` method; allows owner to delete but uses `catch (Exception $e)` without backslash (namespace bug) and does NOT check store_managers pivot
|
||||
- app/Http/Controllers/Market/StoreController.php [lines 1437-1475] — `toggleStoreStatus_Admin()` method; same `catch (Exception $e)` bug
|
||||
- app/Http/Controllers/Market/StoreController.php [lines 1297-1353] — `listStores_Admin()`: sets `user_can_manage = true` when user is in `allowedUserIds` (includes owner, manager_id, and managers pivot) — but backend edit/delete endpoints don't fully mirror this logic
|
||||
|
||||
## steps
|
||||
1. In `StoreController.php` `deleteStore_Admin()` (line ~1425): fix `catch (Exception $e)` → `catch (\Exception $e)` (namespace issue; unqualified `Exception` in a namespace resolves to `App\Http\Controllers\Market\Exception` which doesn't exist)
|
||||
|
||||
2. In `StoreController.php` `toggleStoreStatus_Admin()` (line ~1468): same fix — `catch (Exception $e)` → `catch (\Exception $e)`
|
||||
|
||||
3. In `StoreController.php` `deleteStore_Admin()` (line ~1423): after `$allowedIds` is defined, add a check for managers pivot so any user listed in `store_managers` table can also delete (currently only checks `owner_id` and `manager_id` columns). Add:
|
||||
```php
|
||||
$isManagerViaPivot = $store->managers()->whereIn('user_id', $allowedIds)->exists();
|
||||
if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds) && !$isManagerViaPivot) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
```
|
||||
|
||||
4. In `StoreController.php` `update()` (line ~648): the permission check only allows: isBig3, isParentOfOwner, or `$store->owner_id === $user->id`. Add store managers pivot check so managers assigned via pivot can also edit:
|
||||
```php
|
||||
$isStoreOwner = $acctType === UserTypes::STORE_OWNER;
|
||||
$isManagerViaPivot = $store->managers()->where('user_id', $user->id)->exists();
|
||||
$isDirectManager = $store->manager_id === $user->id;
|
||||
if (!$isBig3 && !$isParentOfOwner && $store->owner_id !== $user->id && !$isDirectManager && !$isManagerViaPivot) {
|
||||
return response()->json(['error' => 'Unauthorized to modify this store'], 403);
|
||||
}
|
||||
```
|
||||
Also guard `owner_id`/`manager_id` changes for STORE_OWNER in update — they should not be able to reassign ownership away from themselves (mirror the restriction in `store()` create method):
|
||||
```php
|
||||
if ($isStoreOwner) {
|
||||
$ownerId = $store->owner_id; // lock owner to self
|
||||
}
|
||||
```
|
||||
|
||||
5. In `HomeStoreOwner.vue` `services` computed (line ~34): add a "Manage Stores" service button so store owners can navigate directly from their service grid (currently only accessible via balance box footer):
|
||||
```js
|
||||
{
|
||||
icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/85605eacd4c8.bin',
|
||||
title: 'Manage Stores',
|
||||
pagename: 'ManageStoresAdmin',
|
||||
},
|
||||
```
|
||||
|
||||
## context
|
||||
### StoreController.php — update() permission block (lines 644-651)
|
||||
```php
|
||||
$acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type);
|
||||
$isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]);
|
||||
|
||||
// Permission check: Big 3 or Parent of Owner/Manager
|
||||
$isParentOfOwner = UserPermissions::isDescendantOfCurrentUser($store->owner_id);
|
||||
if (!$isBig3 && !$isParentOfOwner && $store->owner_id !== $user->id) {
|
||||
return response()->json(['error' => 'Unauthorized to modify this store'], 403);
|
||||
}
|
||||
```
|
||||
|
||||
### StoreController.php — deleteStore_Admin() (lines 1403-1435)
|
||||
```php
|
||||
public function deleteStore_Admin(Request $request)
|
||||
{
|
||||
...
|
||||
$isUltimate = $user->acct_type === UserTypes::ULTIMATE;
|
||||
if (!$isUltimate) {
|
||||
$descendants = $user->getAllDescendants();
|
||||
$descendantIds = $descendants->pluck('id')->toArray();
|
||||
$allowedIds = array_merge([$user->id], $descendantIds);
|
||||
if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
}
|
||||
$store->delete();
|
||||
...
|
||||
} catch (Exception $e) { // BUG: missing backslash
|
||||
```
|
||||
|
||||
### StoreController.php — listStores_Admin() user_can_manage logic (lines 1330-1334)
|
||||
```php
|
||||
->each(function ($s) use ($allowedUserIds) {
|
||||
$s->user_can_manage = in_array($s->owner_id, $allowedUserIds)
|
||||
|| in_array($s->manager_id, $allowedUserIds)
|
||||
|| $s->managers->pluck('user_id')->intersect($allowedUserIds)->isNotEmpty();
|
||||
unset($s->managers);
|
||||
});
|
||||
```
|
||||
The `user_can_manage` flag includes managers-pivot check, but `update()` and `deleteStore_Admin()` do not — this is the root mismatch that causes store managers to see buttons but get 403.
|
||||
|
||||
### HomeStoreOwner.vue — services array (lines 34-60)
|
||||
Currently has: Create Store, Import Products, New Product, My Products, POS Keys.
|
||||
"My Stores" exists only in `balanceFooterItems`. Adding it to `services` makes it more discoverable.
|
||||
|
||||
### Store model relations
|
||||
`$store->managers()` returns `StoreManager` records (pivot model at `app/Models/Market/StoreManager.php`).
|
||||
`$store->managerUsers()` returns `User` models via the pivot.
|
||||
|
||||
## notes
|
||||
- dictionary: /home/josh/development/personal/BukidBountyApp/ai-docs/dictionary.md
|
||||
- linters: none detected
|
||||
- constraints: Hypervel (not Laravel) — use `Hypervel\Support\Facades\*` not `Illuminate\*`; table name for stores is `str` (abbreviated); `declare(strict_types=1)` is in effect
|
||||
66
.claude/plans/22aa928cc5485695daf9052448433271-complete.md
Normal file
66
.claude/plans/22aa928cc5485695daf9052448433271-complete.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
task: Fix GET /api/public/cooperative/:hash returning 500 instead of 404 for invalid hash
|
||||
cycles: 3
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T16:00:46Z
|
||||
finished: 2026-05-16T16:01:15Z
|
||||
---
|
||||
|
||||
## files
|
||||
- app/Http/Controllers/Market/CooperativeController.php [lines 390-403] — `publicGetCooperative` method; uses `response()->json()` instead of `Response::json()`
|
||||
- routes/web.php [line 673] — `Route::get('/api/public/cooperative/{hkey}', ...)` — no auth middleware, public
|
||||
|
||||
## steps
|
||||
1. In `CooperativeController::publicGetCooperative()` replace `response()->json()` calls with `Response::json()` using the Hypervel facade. Add `use Hypervel\Support\Facades\Response;` import if not present.
|
||||
2. Wrap the `Organization::where(...)` query in a try/catch to return a clean 500 JSON error (not bare Server Error page) if the DB call itself fails.
|
||||
3. Also audit `publicRegisterMember()` (lines 405-450) for the same `response()->json()` pattern and fix there too.
|
||||
|
||||
## context
|
||||
```php
|
||||
// BROKEN (lines 390-403):
|
||||
public function publicGetCooperative(Request $request, string $hkey)
|
||||
{
|
||||
$cooperative = Organization::where('hashkey', $hkey)
|
||||
->where('type', 'COOPERATIVE')
|
||||
->where('is_active', true)
|
||||
->select(['id', 'hashkey', 'name', 'type', ...])
|
||||
->first();
|
||||
|
||||
if (!$cooperative) {
|
||||
return response()->json(['success' => false, 'message' => 'Cooperative not found'], 404);
|
||||
// ^ Should be Response::json() — response() helper may not exist in Hypervel
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $cooperative]);
|
||||
}
|
||||
|
||||
// FIX:
|
||||
use Hypervel\Support\Facades\Response;
|
||||
|
||||
public function publicGetCooperative(Request $request, string $hkey)
|
||||
{
|
||||
try {
|
||||
$cooperative = Organization::where('hashkey', $hkey)
|
||||
->where('type', 'COOPERATIVE')
|
||||
->where('is_active', true)
|
||||
->select(['id', 'hashkey', 'name', 'type', 'cooperative_type', 'cooperative_category', 'contact_person', 'contact_number', 'address'])
|
||||
->first();
|
||||
} catch (\Throwable $e) {
|
||||
return Response::json(['success' => false, 'message' => 'Service temporarily unavailable'], 500);
|
||||
}
|
||||
|
||||
if (!$cooperative) {
|
||||
return Response::json(['success' => false, 'message' => 'Cooperative not found'], 404);
|
||||
}
|
||||
|
||||
return Response::json(['success' => true, 'data' => $cooperative]);
|
||||
}
|
||||
```
|
||||
|
||||
Impact: `RegisterCoop.vue` fetches `GET /api/public/cooperative/{hkey}` to render the public self-registration form. An invalid hash in the URL crashes the server instead of showing "Cooperative not found." This also affects the public cooperative registration QR code links shared by cooperatives — any typo or expired link causes a confusing server error.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md (organizations table = `organizations`, cooperative members = `cooperative_members`)
|
||||
- linters: none
|
||||
- constraints: Use `Response::json()` facade (Hypervel), not `response()->json()` (Laravel global helper).
|
||||
137
.claude/plans/2e79878fa79727eedfab4ed9ab823fff-complete.md
Normal file
137
.claude/plans/2e79878fa79727eedfab4ed9ab823fff-complete.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
task: Enable accounting and sales reports access for STORE_OWNER and STORE_MANAGER — add permissions, open routes, and add Reports/Accounting shortcuts to HomeStoreOwner dashboard
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:05:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `app/Http/Controllers/Helpers/Permissions/UserPermissions.php` [lines 838-851] — STORE_OWNER block; missing `ViewAccountingReports` and `ViewGlobalReports`
|
||||
- `app/Http/Controllers/Support/VueRouteMap.php` [lines 249-254, 333-338] — `/list-reports` and `/accounting-dashboard` both exclude `store owner` and `store manager`
|
||||
- `app/Http/Controllers/Accounting/AccountingController.php` — gated by `ViewAccountingReports`; data is global, no store scope needed for demo
|
||||
- `resources/js/Pages/Fragments/Home/HomeStoreOwner.vue` — needs Reports and Accounting shortcut buttons added
|
||||
- `resources/js/Pages/AccountingDashboard.vue` — check if it has any UI that breaks for non-Big3 users (e.g. "Manage Accounts" button that should be hidden)
|
||||
- `resources/js/Pages/ListReports.vue` — check if it has any Big3-only controls that need conditional hiding
|
||||
|
||||
## steps
|
||||
1. **`app/Http/Controllers/Helpers/Permissions/UserPermissions.php`** — Add to `UserTypes::STORE_OWNER->value` permissions array (after `JoinCooperative`):
|
||||
```php
|
||||
UserActions::ViewAccountingReports,
|
||||
UserActions::ViewGlobalReports,
|
||||
UserActions::ViewGlobalTransactions,
|
||||
```
|
||||
Add to `UserTypes::STORE_MANAGER->value` permissions array (after `JoinCooperative`):
|
||||
```php
|
||||
UserActions::ViewAccountingReports,
|
||||
UserActions::ViewGlobalReports,
|
||||
UserActions::ViewGlobalTransactions,
|
||||
```
|
||||
|
||||
2. **`app/Http/Controllers/Support/VueRouteMap.php`** — Update `allowedUserTypes` for:
|
||||
- `/list-reports` (line ~251): change from `['ult', 'super operator', 'operator']` to `['ult', 'super operator', 'operator', 'store owner', 'store manager']`
|
||||
- `/accounting-dashboard` (line ~336): change from `['ult', 'super operator', 'operator']` to `['ult', 'super operator', 'operator', 'store owner', 'store manager']`
|
||||
|
||||
3. **`resources/js/Pages/AccountingDashboard.vue`** — Audit for Big3-only controls:
|
||||
- Find any "Manage Accounts", "Create Account", "Delete Account" buttons
|
||||
- Wrap them in `v-if="isUltimate || isSuperOperator || isOperator"` using `useAuth()` composable
|
||||
- Store owners should see the read-only Tree/Leaf views and reports but not be able to create/delete accounting nodes
|
||||
- If the component already uses permission-based hiding, verify it works for `STORE_OWNER`
|
||||
|
||||
4. **`resources/js/Pages/ListReports.vue`** — Audit for Big3-only controls:
|
||||
- Find any "Export All", "Delete Transaction", or administrative bulk-action buttons
|
||||
- Wrap in `v-if="isUltimate || isSuperOperator || isOperator"`
|
||||
- Confirm the report data loads correctly (POST `/admin/accounting/reports` — AccountingController checks `ViewAccountingReports` permission which STORE_OWNER will now have)
|
||||
|
||||
5. **`resources/js/Pages/Fragments/Home/HomeStoreOwner.vue`** — Add Reports and Accounting shortcut buttons to the `services` computed array:
|
||||
Add after the existing `POS Keys` entry:
|
||||
```js
|
||||
{
|
||||
icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f87407046b18.bin',
|
||||
title: 'Reports',
|
||||
pagename: 'ListReports',
|
||||
},
|
||||
{
|
||||
icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/fa711c34b4ef.svg',
|
||||
title: 'Accounting',
|
||||
pagename: 'AccountingDashboard',
|
||||
},
|
||||
```
|
||||
The `services` array currently has 6 tiles; this brings it to 8, which is the standard 2×4 grid layout.
|
||||
|
||||
6. **`resources/js/Pages/Fragments/Home/HomeStoreOwner.vue`** — Add `balanceFooterItems` shortcut for Reports:
|
||||
Current footer has `Open POS` and `My Stores`. Add:
|
||||
```js
|
||||
{ title: 'Reports', icon: 'https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f87407046b18.bin', pagename: 'ListReports' }
|
||||
```
|
||||
(BalanceBox footer typically shows 2-3 items; verify `WalletFooter` renders a third item correctly — check `BalanceBox.vue` / `WalletFooter.vue` props for max items)
|
||||
|
||||
7. **Verify `AddTransaction` route** — Confirm `store owner` is in `allowedUserTypes` for `/add-transaction` in VueRouteMap. If not, add it (store owners need to be able to record manual transactions for their stores).
|
||||
|
||||
8. **Manual integration test checklist** (run after server is up):
|
||||
- Login as store owner (`099` / `polomiko32!`)
|
||||
- Navigate to `/list-reports` — should load without 403
|
||||
- Navigate to `/accounting-dashboard` — should load Tree/Leaf view
|
||||
- Confirm no "Manage Accounts" or destructive buttons appear for the store owner
|
||||
- Confirm `Reports` and `Accounting` tiles appear on the home dashboard
|
||||
- Navigate to Home — verify the 8-tile services grid renders correctly
|
||||
|
||||
## context
|
||||
```
|
||||
// Current STORE_OWNER permissions block (app/Http/Controllers/Helpers/Permissions/UserPermissions.php lines 838-851):
|
||||
UserTypes::STORE_OWNER->value => [
|
||||
UserActions::CreateUserStoreManager,
|
||||
UserActions::CreateUserRider,
|
||||
UserActions::CreateUserPOSTerminal,
|
||||
UserActions::ViewUserInfo,
|
||||
UserActions::ManageUserInfo,
|
||||
UserActions::ViewShipments,
|
||||
UserActions::ViewPosReports,
|
||||
UserActions::ViewPosAccessKeys,
|
||||
UserActions::CreatePosAccessKey,
|
||||
UserActions::DeletePosAccessKey,
|
||||
UserActions::TogglePosAccessKey,
|
||||
UserActions::JoinCooperative,
|
||||
// ADD: ViewAccountingReports, ViewGlobalReports, ViewGlobalTransactions
|
||||
],
|
||||
|
||||
// VueRouteMap /list-reports (line ~249-254):
|
||||
'/list-reports' => [
|
||||
'component' => 'ListReports',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes' => ['ult', 'super operator', 'operator'], // ADD: 'store owner', 'store manager'
|
||||
'module' => 'accounting',
|
||||
],
|
||||
|
||||
// VueRouteMap /accounting-dashboard (line ~333-338):
|
||||
'/accounting-dashboard' => [
|
||||
'component' => 'AccountingDashboard',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes' => ['ult', 'super operator', 'operator'], // ADD: 'store owner', 'store manager'
|
||||
'module' => 'accounting',
|
||||
],
|
||||
|
||||
// AccountingController permission gates:
|
||||
// ViewAccountingReports → listTransactions(), getTree(), getLeaf(), reports()
|
||||
// ManageAccounting → createAccount(), updateAccount(), deleteAccount(), createTransaction()
|
||||
// STORE_OWNER should get ViewAccountingReports only (read-only view)
|
||||
|
||||
// HomeStoreOwner.vue services currently (6 items):
|
||||
// Create Store, Import Products, New Product, My Products, POS Keys, Manage Stores
|
||||
// After task: 8 items (+ Reports, Accounting)
|
||||
|
||||
// CDN icon URLs in use:
|
||||
// Reports: .../a/f87407046b18.bin
|
||||
// Accounting: .../a/fa711c34b4ef.svg (used in HomeOperator.vue)
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: `ai-docs/dictionary.md`
|
||||
- linters: none detected
|
||||
- constraints:
|
||||
- STORE_OWNER gets **read-only** accounting access (`ViewAccountingReports`) — do NOT add `ManageAccounting`
|
||||
- The accounting data shown to store owners will be global (all accounts/transactions) since the accounting module is not yet scoped per-store. This is acceptable for a demo. A follow-up task could scope by owned stores.
|
||||
- `ViewGlobalReports` and `ViewGlobalTransactions` are needed because `ListReports` backend checks these in some endpoints — add them to avoid unexpected 403s when navigating report sub-pages
|
||||
- If `WalletFooter` has a hard-coded max of 2 items, skip adding the third footer item and only add the tile grid shortcut
|
||||
- Dark mode compliance: no bg-white, no text-dark in any added code
|
||||
63
.claude/plans/3b3af2f37a16a11851d950fa33df090e-complete.md
Normal file
63
.claude/plans/3b3af2f37a16a11851d950fa33df090e-complete.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
task: Add "Open POS" button in PosAccessKeys.vue that opens the POS main page in a new tab
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T08:38:57Z
|
||||
finished: 2026-05-16T08:39:10Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/PosAccessKeys.vue [lines 275-289] — contains the action button group (Copy Key, Copy URL, QR Code, Share) per access key row
|
||||
|
||||
## steps
|
||||
1. In `PosAccessKeys.vue` at the button group (line ~283, after the QR code button and before the Share button), add a new `<button>` that calls `openPosInNewTab(key.access_key)`.
|
||||
2. Add the `openPosInNewTab(accessKey)` method to the `<script setup>` block (after `shareKey`, around line 161). It should call `window.open(getPosUrl(accessKey), '_blank')`.
|
||||
|
||||
## context
|
||||
|
||||
Existing button group in template (lines 275–289):
|
||||
```html
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<code class="bg-light px-2 py-1 rounded small">{{ key.access_key }}</code>
|
||||
<button @click="copyToClipboard(key.access_key)" class="btn btn-sm btn-link p-0 text-muted" title="Copy Key">
|
||||
<i class="far fa-copy"></i>
|
||||
</button>
|
||||
<button @click="copyToClipboard(getPosUrl(key.access_key))" class="btn btn-sm btn-link p-0 text-muted" title="Copy POS URL">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
<button @click="showQrCode(key.access_key)" class="btn btn-sm btn-link p-0 text-muted" title="View QR Code">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</button>
|
||||
<button v-if="canShare" @click="shareKey(key.access_key, key.name)" class="btn btn-sm btn-link p-0 text-muted" title="Share via Protocol">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
`getPosUrl` (line 138–141):
|
||||
```js
|
||||
const getPosUrl = (accessKey) => {
|
||||
const baseUrl = window.location.origin;
|
||||
return `${baseUrl}/pos?key=${accessKey}`;
|
||||
};
|
||||
```
|
||||
|
||||
New method to add after `shareKey` (line ~161):
|
||||
```js
|
||||
const openPosInNewTab = (accessKey) => {
|
||||
window.open(getPosUrl(accessKey), '_blank');
|
||||
};
|
||||
```
|
||||
|
||||
New button to insert after the QR code button, before the Share button:
|
||||
```html
|
||||
<button @click="openPosInNewTab(key.access_key)" class="btn btn-sm btn-link p-0 text-muted" title="Open POS Terminal">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</button>
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none detected
|
||||
- constraints: No backend changes needed — URL is already constructed by existing `getPosUrl` helper. Button must match the existing style (btn-sm btn-link p-0 text-muted). No new permissions required — this is a client-side navigation action.
|
||||
139
.claude/plans/3c56b393bd94fe01eed118b965a18df0-complete.md
Normal file
139
.claude/plans/3c56b393bd94fe01eed118b965a18df0-complete.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
task: Fix accounting theme switch+apply — switching theme should auto-seed accounts, apply should not silently fail
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-29T00:00:00Z
|
||||
finished: 2026-05-29T00:00:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `resources/js/Pages/ManageAccounts.vue` [lines 457-491] — switchTheme() and reapplyTheme() JS functions
|
||||
- `app/Http/Controllers/Accounting/AccountingController.php` [lines 677-715] — setTheme() and applyTheme() controllers; also getAccountsTree() lines 78-109 has a scoping bug
|
||||
- `app/Support/AccountingTheme.php` [lines 201-276] — static apply() / applyNode() logic
|
||||
|
||||
## steps
|
||||
|
||||
### Bug 1 — "Switch" does not seed accounts (primary UX failure)
|
||||
1. In `ManageAccounts.vue` `switchTheme()` (line ~457), after `POST /admin/accounting/theme/set` succeeds, immediately call `POST /admin/accounting/theme/apply` (reuse `reapplyTheme()` logic inline or call it).
|
||||
Concrete change: after `await loadThemes(); await fetchAll();` inside the `if (res.data?.success)` block, also call `await reapplyTheme()` — OR refactor: make `switchTheme()` call a combined backend endpoint.
|
||||
|
||||
Simplest safe fix: after confirming `setTheme` success, call `reapplyTheme()` directly:
|
||||
```js
|
||||
if (res.data?.success) {
|
||||
await setThemeKey(selectThemeKey.value); // already done
|
||||
await reapplyTheme(); // NEW: seed accounts for the newly-active theme
|
||||
}
|
||||
```
|
||||
Since `reapplyTheme()` reads the current theme from the backend (not a parameter), and `setTheme` already saved it, the order is correct.
|
||||
|
||||
### Bug 2 — `getAccountsTree()` discards scope on grandchildren (silent data loss)
|
||||
2. In `AccountingController.php` `getAccountsTree()` (lines 100-102), the innermost `with()` closure does:
|
||||
```php
|
||||
$this->scopeAccounts($q2, $storeIds, $isBig3); // return value DISCARDED
|
||||
```
|
||||
Fix: capture the return value:
|
||||
```php
|
||||
$q2 = $this->scopeAccounts($q2, $storeIds, $isBig3);
|
||||
```
|
||||
Line 102 — change `$this->scopeAccounts($q2, $storeIds, $isBig3);` → `$q2 = $this->scopeAccounts($q2, $storeIds, $isBig3);`
|
||||
|
||||
### Bug 3 — `applyTheme()` has no error handling; DB failures are silent 500s
|
||||
3. In `AccountingController.php` `applyTheme()` (line ~696), wrap the `AccountingTheme::apply()` call in try/catch:
|
||||
```php
|
||||
try {
|
||||
$stats = \App\Support\AccountingTheme::apply();
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to apply theme: ' . $e->getMessage(),
|
||||
], 422);
|
||||
}
|
||||
```
|
||||
|
||||
### Bug 4 — switchTheme UX: don't allow re-clicking Switch on already-active theme after apply
|
||||
4. After `reapplyTheme()` completes inside `switchTheme()`, call `loadThemes()` once more so `themeInfo.current` is refreshed and the Switch button re-disables correctly. (If `reapplyTheme()` already calls `loadThemes()`, this is automatic — just verify no double-call confusion.)
|
||||
|
||||
## context
|
||||
|
||||
### ManageAccounts.vue switchTheme() (lines 457-473):
|
||||
```js
|
||||
async function switchTheme() {
|
||||
if (!selectThemeKey.value || selectThemeKey.value === themeInfo.value?.current) return;
|
||||
switchingTheme.value = true;
|
||||
try {
|
||||
const res = await axios.post('/admin/accounting/theme/set', { key: selectThemeKey.value });
|
||||
if (res.data?.success) {
|
||||
await loadThemes();
|
||||
await fetchAll();
|
||||
// BUG: no apply call here — accounts never seeded
|
||||
} else {
|
||||
showNotice(res.data?.message || 'Could not switch theme.', { variant: 'danger' });
|
||||
}
|
||||
} catch (e) {
|
||||
showNotice(e.response?.data?.message || 'Could not switch theme.', { variant: 'danger' });
|
||||
} finally {
|
||||
switchingTheme.value = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ManageAccounts.vue reapplyTheme() (lines 475-491):
|
||||
```js
|
||||
async function reapplyTheme() {
|
||||
applyingTheme.value = true;
|
||||
try {
|
||||
const res = await axios.post('/admin/accounting/theme/apply', {});
|
||||
if (res.data?.success) {
|
||||
showNotice(res.data.message || 'Theme applied.', { variant: 'success', title: 'Theme Applied' });
|
||||
await loadThemes();
|
||||
await fetchAll();
|
||||
} else {
|
||||
showNotice(res.data?.message || 'Apply failed.', { variant: 'danger' });
|
||||
}
|
||||
} catch (e) {
|
||||
showNotice(e.response?.data?.message || 'Apply failed.', { variant: 'danger' });
|
||||
} finally {
|
||||
applyingTheme.value = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AccountingController.php getAccountsTree() children scope (lines 96-105):
|
||||
```php
|
||||
->with(['children' => function ($q) use ($storeIds, $isBig3) {
|
||||
$q = $q->where('is_active', true);
|
||||
$q = $this->scopeAccounts($q, $storeIds, $isBig3); // ✓ captured
|
||||
$q->with(['children' => function ($q2) use ($storeIds, $isBig3) {
|
||||
$q2 = $q2->where('is_active', true);
|
||||
$this->scopeAccounts($q2, $storeIds, $isBig3); // ✗ BUG: return discarded
|
||||
}]);
|
||||
}])
|
||||
```
|
||||
|
||||
### AccountingController.php applyTheme() (lines 696-715):
|
||||
```php
|
||||
public function applyTheme(Request $request)
|
||||
{
|
||||
if (!UserPermissions::isActionPermitted(...)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
$stats = \App\Support\AccountingTheme::apply(); // no try-catch
|
||||
return response()->json(['success' => true, 'message' => ..., 'data' => $stats]);
|
||||
}
|
||||
```
|
||||
|
||||
### AccountingTheme::apply() tree source:
|
||||
- Reads `Config::get('accounting.themes', [])` from `config/accounting/themes.php`
|
||||
- `banana_trading` key has `tree` with 4 root nodes (Sales, Supplier Purchases, Delivery & Logistics, Operating Expenses) + leaf children
|
||||
- `blank` key has `'tree' => []` — if `current()` falls back to `blank`, apply creates 0 accounts
|
||||
|
||||
### Scope for Big3 vs store-level:
|
||||
- `scopeAccounts($query, $storeIds, $isBig3)`: Big3 → `whereNull('store_id')`, store-level → `whereIn('store_id', $storeIds)`
|
||||
- `applyNode()` creates accounts with `store_id` absent (NULL) → correct for Big3 global chart
|
||||
- Root query: `whereNull('parent_id') AND is_active = 1 AND store_id IS NULL` → should match theme-applied accounts
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none detected
|
||||
- constraints: Do NOT delete existing accounts; apply is idempotent/additive. Do NOT change the `setTheme` backend — it is correct as-is (just saves key). The frontend fix in `switchTheme()` is the primary fix. Wrap `reapplyTheme()` call inside `switchTheme()` with `switchingTheme.value = true` still active so the button stays disabled during the full switch+apply operation.
|
||||
208
.claude/plans/4165d258481eb317bef1ddcfd08f2295-complete.md
Normal file
208
.claude/plans/4165d258481eb317bef1ddcfd08f2295-complete.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Plan: Batch Add Products — Photo Upload + Category Dropdown Fix
|
||||
|
||||
## Goal
|
||||
Two fixes for `resources/js/Pages/BatchAddProducts.vue`:
|
||||
1. **Optional photo upload per product leaf** (new products only) — tap an area to pick a photo, upload immediately via `useFileUpload`, and include the returned hashkey in the submit payload.
|
||||
2. **Category field converts to a `<select>` dropdown** — currently uses `<input type="text" list="leaf-categories">` which behaves as plain text on mobile. Replace with a `<select>` populated from the `categories` ref (same data already loaded from `/Products/New/Category/Datalist`). Keep "None / other" as the first empty option so the field stays optional.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Key files
|
||||
- **Frontend**: `resources/js/Pages/BatchAddProducts.vue` — all changes happen here
|
||||
- **Composable**: `resources/js/composables/useFileUpload.js` — `uploadFile(file)` → POST `/File/Upload/ProductMarket` → returns `{ hashkey }`. Already used in `CreateProductStoreOwner.vue`.
|
||||
- **Backend**: `app/Http/Controllers/Market/BatchController.php` → `batchCreateProducts()` — needs to accept `photourl` and pass it to `Product::create()`.
|
||||
- **Product model**: `app/Models/Market/Product.php` — `photourl` is already a cast array field (line 27/59).
|
||||
|
||||
### Category data
|
||||
`fetchCategories()` already calls `POST /Products/New/Category/Datalist` → `{ success: true, categories: ['Vegetables', 'Fruits', ...] }`. The raw `categories` ref holds a string array. The `<datalist>` approach already uses it; replacing with `<select>` just maps the same array to `<option>` elements.
|
||||
|
||||
### Photo upload flow (existing pattern from CreateProductStoreOwner.vue)
|
||||
```js
|
||||
import { useFileUpload } from '../composables/useFileUpload.js'
|
||||
const { uploadFile, uploadError } = useFileUpload({ category: 'ProductMarket' })
|
||||
|
||||
// on file input change:
|
||||
const result = await uploadFile(file)
|
||||
if (result?.hashkey) leaf.photoHash = result.hashkey
|
||||
```
|
||||
|
||||
### Leaf state shape (current makeLeaf)
|
||||
```js
|
||||
const makeLeaf = () => ({
|
||||
source: 'new',
|
||||
product_hash: '',
|
||||
linked: null,
|
||||
name: '',
|
||||
price: 0,
|
||||
available: 0,
|
||||
unitname: 'pcs',
|
||||
description: '',
|
||||
category: '',
|
||||
subcategory: '',
|
||||
barcode: '',
|
||||
})
|
||||
```
|
||||
Add `photoHash: ''` and `photoUploading: false` to this shape.
|
||||
|
||||
### Backend payload change
|
||||
In `saveProducts()`, for `source === 'new'` leaves add:
|
||||
```js
|
||||
photourl: p.photoHash ? [p.photoHash] : [],
|
||||
```
|
||||
In `BatchController::batchCreateProducts()`, add to the validator for `source === 'new'`:
|
||||
```php
|
||||
'photourl' => 'nullable|array',
|
||||
'photourl.*' => 'nullable|string',
|
||||
```
|
||||
And in `Product::create([...])` add:
|
||||
```php
|
||||
'photourl' => $productData['photourl'] ?? [],
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step implementation
|
||||
|
||||
### Step 1 — Add `photoHash` + `photoUploading` to leaf state
|
||||
In `makeLeaf()`:
|
||||
```js
|
||||
const makeLeaf = () => ({
|
||||
// ...existing fields...
|
||||
photoHash: '',
|
||||
photoUploading: false,
|
||||
})
|
||||
```
|
||||
|
||||
### Step 2 — Import `useFileUpload` and wire a single uploader instance
|
||||
Because each leaf uploads independently, instantiate `useFileUpload` once and use a helper:
|
||||
```js
|
||||
import { useFileUpload } from '../composables/useFileUpload.js'
|
||||
const { uploadFile } = useFileUpload({ category: 'ProductMarket' })
|
||||
|
||||
const handleLeafPhoto = async (index, event) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
products.value[index].photoUploading = true
|
||||
const result = await uploadFile(file)
|
||||
products.value[index].photoUploading = false
|
||||
if (result?.hashkey) products.value[index].photoHash = result.hashkey
|
||||
}
|
||||
|
||||
const removeLeafPhoto = (index) => {
|
||||
products.value[index].photoHash = ''
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3 — Add photo upload UI inside the `source === 'new'` template block
|
||||
Place above the description field, after the Barcode row:
|
||||
|
||||
```html
|
||||
<!-- Photo (optional) -->
|
||||
<label class="form-label small fw-bold text-muted mb-1 mt-1">Photo</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label
|
||||
v-if="!product.photoHash"
|
||||
class="btn btn-outline-secondary btn-sm rounded-pill flex-grow-1"
|
||||
:class="{ disabled: product.photoUploading }"
|
||||
:for="`photo-input-${index}`"
|
||||
style="cursor:pointer;"
|
||||
>
|
||||
<span v-if="product.photoUploading">
|
||||
<LoadingSpinner size="small" class="me-1" /> Uploading…
|
||||
</span>
|
||||
<span v-else>
|
||||
<i class="fas fa-camera me-1"></i> Add Photo
|
||||
</span>
|
||||
</label>
|
||||
<div v-else class="d-flex align-items-center gap-2 flex-grow-1">
|
||||
<img
|
||||
:src="`/RequestData/File/${product.photoHash}`"
|
||||
class="rounded-2 border"
|
||||
style="width:48px;height:48px;object-fit:cover;"
|
||||
alt="Product photo"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-link btn-sm text-danger p-0"
|
||||
@click="removeLeafPhoto(index)"
|
||||
title="Remove photo"
|
||||
>
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
:id="`photo-input-${index}`"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="d-none"
|
||||
@change="(e) => handleLeafPhoto(index, e)"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
Use `/RequestData/File/${hashkey}` for the preview (same path used by `FileList::resolvedUrl()` fallback).
|
||||
|
||||
### Step 4 — Replace category `<input list>` with `<select>`
|
||||
Replace:
|
||||
```html
|
||||
<input
|
||||
v-model="product.category"
|
||||
type="text"
|
||||
list="leaf-categories"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="e.g. Vegetables"
|
||||
>
|
||||
```
|
||||
With:
|
||||
```html
|
||||
<select v-model="product.category" class="form-select form-select-sm">
|
||||
<option value="">— Category —</option>
|
||||
<option v-for="cat in categories" :key="cat" :value="cat">{{ cat }}</option>
|
||||
</select>
|
||||
```
|
||||
Also remove the `<datalist id="leaf-categories">` element at the bottom of the template since it is no longer needed.
|
||||
|
||||
### Step 5 — Include `photourl` in the save payload
|
||||
In `saveProducts()`, update the `source === 'new'` branch of the payload map:
|
||||
```js
|
||||
{
|
||||
source: 'new',
|
||||
name: p.name,
|
||||
price: p.price,
|
||||
available: p.available,
|
||||
unitname: p.unitname,
|
||||
description: p.description,
|
||||
category: p.category,
|
||||
subcategory: p.subcategory,
|
||||
barcode: p.barcode,
|
||||
photourl: p.photoHash ? [p.photoHash] : [],
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6 — Update `BatchController::batchCreateProducts()` to persist photourl
|
||||
In `app/Http/Controllers/Market/BatchController.php`, in the `source === 'new'` validator rules, add:
|
||||
```php
|
||||
'photourl' => 'nullable|array',
|
||||
'photourl.*' => 'nullable|string',
|
||||
```
|
||||
In `Product::create([...])`, add:
|
||||
```php
|
||||
'photourl' => $productData['photourl'] ?? [],
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done checklist
|
||||
- [ ] New leaf cards show an "Add Photo" button that opens a file picker
|
||||
- [ ] Selecting a photo uploads it and shows a thumbnail preview with a remove ×
|
||||
- [ ] Loading spinner shown during upload; button disabled while uploading
|
||||
- [ ] `photoHash` is included in the save payload as `photourl: [hash]`
|
||||
- [ ] Backend accepts and stores `photourl` on the new product
|
||||
- [ ] Category field is a `<select>` dropdown populated with categories from the API
|
||||
- [ ] Selecting a category from the dropdown sets `product.category` correctly
|
||||
- [ ] `<datalist id="leaf-categories">` removed from template
|
||||
- [ ] Existing fields (subcategory, barcode, description) unchanged
|
||||
- [ ] Dark-mode styles still apply (form-select already covered by existing dark-mode scoped rule)
|
||||
- [ ] `source === 'existing'` leaves are unaffected (no photo upload shown, category not editable)
|
||||
- [ ] Build passes (`npm run build`)
|
||||
264
.claude/plans/4fc3b455fb62b15dfb06790a1f421c96-complete.md
Normal file
264
.claude/plans/4fc3b455fb62b15dfb06790a1f421c96-complete.md
Normal file
@@ -0,0 +1,264 @@
|
||||
---
|
||||
task: Implement COOP_MEMBER and COOP_OFFICER account types with geographic chapter hierarchy
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-28T17:08:44Z
|
||||
finished: 2026-05-28T17:09:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `app/Enums/UserTypes.php` — add COOP_MEMBER and COOP_OFFICER cases
|
||||
- `app/Enums/UserActions.php` — add ViewChapterOrgChart, ManageChapterMembers, ViewScopedMemberReports, AssignChapterOfficer
|
||||
- `app/Http/Controllers/Helpers/Permissions/UserPermissions.php` lines 818–838 — add permission sets for new types; update UserTypeService::getAllowedUserTypes
|
||||
- `app/Models/Chapter.php` — add cooperative_id to fillable; add cooperative() relationship
|
||||
- `app/Models/ChapterMember.php` — add role field to fillable; add isOfficer() helper
|
||||
- `database/migrations/2026_04_19_100001_create_chapters_table.php` — reference only, do NOT modify; write new migration instead
|
||||
- `database/migrations/2026_04_19_100002_create_chapter_members_table.php` — reference only; write new migration for role column
|
||||
- `app/Http/Controllers/Market/CooperativeController.php` — update registerMember to set acct_type=COOP_MEMBER if user is currently USER
|
||||
- `app/Http/Controllers/Support/VueRouteMap.php` lines 217–311 — add new chapter pages; update existing coop pages to allow new types
|
||||
- `resources/js/utils/UserTypes.js` — add COOP_MEMBER and COOP_OFFICER constants
|
||||
- `resources/js/composables/Core/useAuth.js` lines 84–133 — add isCoopMember, isCoopOfficer computed properties
|
||||
- `resources/js/Pages/Home.vue` lines 73–111 — add v-else-if blocks for COOP_OFFICER and COOP_MEMBER before the User fallback
|
||||
- `resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue` — NEW: officer dashboard showing chapter org chart + scoped member stats
|
||||
- `resources/js/Pages/Fragments/Home/HomeCoopMember.vue` — NEW: member dashboard showing membership card, chapter info, wallet, marketplace
|
||||
- `resources/js/Pages/ChapterOrgChart.vue` — NEW: org chart page filtered by officer's chapter level/geographic scope
|
||||
- `resources/js/composables/useChapters.js` — may need fetchOfficerScope() method
|
||||
|
||||
## steps
|
||||
|
||||
### 1. New migration — add cooperative_id to chapters
|
||||
Create `database/migrations/2026_05_28_000001_add_cooperative_id_to_chapters.php`:
|
||||
- `$table->unsignedBigInteger('cooperative_id')->nullable()->after('name');`
|
||||
- `$table->foreign('cooperative_id')->references('id')->on('organizations')->nullOnDelete();`
|
||||
- Add `$table->index('cooperative_id');`
|
||||
- Use Hypervel imports (NOT Illuminate): `use Hyperf\Database\Schema\Blueprint; use Hypervel\Database\Migrations\Migration; use Hypervel\Support\Facades\Schema;`
|
||||
|
||||
### 2. New migration — add role to chapter_members
|
||||
Create `database/migrations/2026_05_28_000002_add_role_to_chapter_members.php`:
|
||||
- `$table->string('role')->nullable()->after('position')->comment('Officer role: PRESIDENT, VICE_PRESIDENT, SECRETARY, TREASURER, AUDITOR, BOARD_MEMBER, MEMBER');`
|
||||
- The existing `position` field remains for free-text title; `role` is the canonical enum-like string
|
||||
- Use Hypervel imports
|
||||
|
||||
### 3. UserTypes PHP enum — app/Enums/UserTypes.php
|
||||
After `case COORDINATOR = 'coordinator';`, add:
|
||||
```php
|
||||
case COOP_OFFICER = 'coop officer';
|
||||
case COOP_MEMBER = 'coop member';
|
||||
```
|
||||
|
||||
### 4. UserActions PHP enum — app/Enums/UserActions.php
|
||||
Add after the last existing case:
|
||||
```php
|
||||
case ViewChapterOrgChart = 'viewchapterorgchart';
|
||||
case ManageChapterMembers = 'managechaptermembers';
|
||||
case ViewScopedMemberReports = 'viewscopedmemberreports';
|
||||
case AssignChapterOfficer = 'assignchapterofficer';
|
||||
```
|
||||
|
||||
### 5. UserPermissions — add new type permission sets
|
||||
In `UserPermissions::roles()` after the `UserTypes::USER->value` block (line 818), add:
|
||||
|
||||
```php
|
||||
UserTypes::COOP_MEMBER->value => [
|
||||
UserActions::JoinCooperative,
|
||||
UserActions::ViewUserInfo,
|
||||
UserActions::ManageUserInfo,
|
||||
UserActions::ViewChapterOrgChart,
|
||||
],
|
||||
|
||||
UserTypes::COOP_OFFICER->value => [
|
||||
UserActions::JoinCooperative,
|
||||
UserActions::ViewUserInfo,
|
||||
UserActions::ManageUserInfo,
|
||||
UserActions::ViewOrganizations,
|
||||
UserActions::ViewChapterOrgChart,
|
||||
UserActions::ManageChapterMembers,
|
||||
UserActions::ViewScopedMemberReports,
|
||||
UserActions::AssignChapterOfficer,
|
||||
UserActions::ViewAccountingReports,
|
||||
UserActions::CheckifMobileNumberExists,
|
||||
UserActions::CheckifUsernameExists,
|
||||
],
|
||||
```
|
||||
|
||||
Also add ViewChapterOrgChart, ManageChapterMembers, ViewScopedMemberReports, AssignChapterOfficer to `$RoleswithNoTargetUser` array.
|
||||
|
||||
### 6. UserPermissions — getAllowedUserTypes
|
||||
In `UserTypeService::getAllowedUserTypes()`, add COOP_OFFICER and COOP_MEMBER:
|
||||
- COORDINATOR can create COOP_OFFICER and COOP_MEMBER
|
||||
- COOP_OFFICER can create COOP_MEMBER
|
||||
- ULTIMATE/SUPER_OPERATOR/OPERATOR can create both (already implied by wildcard but add explicitly)
|
||||
|
||||
### 7. Chapter model — add cooperative_id
|
||||
In `app/Models/Chapter.php`, add `'cooperative_id'` to `$fillable` and add relationship:
|
||||
```php
|
||||
public function cooperative() {
|
||||
return $this->belongsTo(\App\Models\Market\Organization::class, 'cooperative_id');
|
||||
}
|
||||
```
|
||||
|
||||
### 8. ChapterMember model — add role to fillable
|
||||
In `app/Models/ChapterMember.php`, add `'role'` to `$fillable`.
|
||||
Add helper:
|
||||
```php
|
||||
public function isOfficer(): bool {
|
||||
return !empty($this->role) && $this->role !== 'MEMBER';
|
||||
}
|
||||
```
|
||||
|
||||
### 9. CooperativeController — update registerMember to set acct_type
|
||||
In `CooperativeController@registerMember` (and `publicRegisterMember`), after successfully creating/confirming the `cooperative_members` row, check:
|
||||
```php
|
||||
if ($user->acct_type === UserTypes::USER) {
|
||||
$user->acct_type = UserTypes::COOP_MEMBER;
|
||||
$user->save();
|
||||
}
|
||||
```
|
||||
This upgrades plain USER accounts to COOP_MEMBER on cooperative registration. Do not downgrade any existing type that is higher (COORDINATOR, OPERATOR, etc.).
|
||||
|
||||
### 10. Frontend UserTypes.js
|
||||
In `resources/js/utils/UserTypes.js`, add:
|
||||
```js
|
||||
COOP_OFFICER: 'coop officer',
|
||||
COOP_MEMBER: 'coop member',
|
||||
```
|
||||
|
||||
### 11. useAuth.js — add computed helpers
|
||||
In `resources/js/composables/Core/useAuth.js`, after `isUser` (line 113), add:
|
||||
```js
|
||||
const isCoopOfficer = computed(() => role.value === UserTypes.COOP_OFFICER);
|
||||
const isCoopMember = computed(() => role.value === UserTypes.COOP_MEMBER);
|
||||
```
|
||||
Export both from the return object.
|
||||
|
||||
### 12. Home.vue — add dashboard fragments for new types
|
||||
Import two new components at top of `<script setup>`:
|
||||
```js
|
||||
import HomeCoopOfficer from './Fragments/Home/HomeCoopOfficer.vue';
|
||||
import HomeCoopMember from './Fragments/Home/HomeCoopMember.vue';
|
||||
```
|
||||
Add to destructured `useAuth()`: `isCoopOfficer, isCoopMember`.
|
||||
|
||||
In the template, insert BEFORE the `v-else-if="isUser"` block (line 109):
|
||||
```html
|
||||
<!-- Coop Officer -->
|
||||
<template v-else-if="isCoopOfficer">
|
||||
<HomeCoopOfficer />
|
||||
</template>
|
||||
|
||||
<!-- Coop Member -->
|
||||
<template v-else-if="isCoopMember">
|
||||
<HomeCoopMember />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 13. HomeCoopOfficer.vue — NEW officer dashboard
|
||||
`resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue`
|
||||
|
||||
Key elements:
|
||||
- Stats card: member count in officer's chapter scope, sub-chapter count, new members (7d)
|
||||
- Officer's chapter badge showing their level (BARANGAY / MUNICIPAL / PROVINCIAL / etc.) and geographic name
|
||||
- ServiceButtonGrid with: Chapter Org Chart (`ChapterOrgChart`), Members, Reports, My Profile, Cooperative Detail
|
||||
- SideTextButtonList with: Assign Officer, Add Member, Member Ledger
|
||||
- Fetch data from `/home-data` (reuse existing endpoint; add coop_officer branch on backend if stats differ)
|
||||
- The officer's chapter info comes from `user.value?.chapter_membership` (add to home-data response or fetch separately via `/Chapters/MyChapters`)
|
||||
|
||||
### 14. HomeCoopMember.vue — NEW member dashboard
|
||||
`resources/js/Pages/Fragments/Home/HomeCoopMember.vue`
|
||||
|
||||
Key elements:
|
||||
- Membership card: user name, membership type, chapter name (their barangay/municipal chapter), member since
|
||||
- Stats: wallet balance (if applicable), chapter contact officer name
|
||||
- ServiceButtonGrid with: Cooperative Detail, My Profile, Marketplace, My Wallet
|
||||
- SideTextButtonList with: Member Ledger, View Chapter
|
||||
- Fetch stats from `/home-data` (add member branch)
|
||||
|
||||
### 15. ChapterOrgChart.vue — NEW page
|
||||
`resources/js/Pages/ChapterOrgChart.vue`
|
||||
|
||||
- Fetches chapter hierarchy from `/Chapters/OrgChart` (new backend endpoint)
|
||||
- Backend scopes response by caller's `acct_type`:
|
||||
- COOP_OFFICER: return the subtree rooted at their highest chapter
|
||||
- COORDINATOR/OPERATOR/Big3: return full tree or coop-scoped tree
|
||||
- COOP_MEMBER: return their barangay chapter and its officers only (read-only)
|
||||
- Renders a tree: collapsible nodes per level, each showing chapter name, officer names + roles
|
||||
- Route: `/chapter-org-chart` (no hash needed unless scoped to a specific coop)
|
||||
|
||||
### 16. VueRouteMap.php — register new pages
|
||||
Add entries:
|
||||
```php
|
||||
'/chapter-org-chart' => [
|
||||
'component' => 'ChapterOrgChart',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
|
||||
'module' => 'cooperatives',
|
||||
],
|
||||
```
|
||||
Update existing cooperative pages (cooperative-list, cooperative-detail, cooperative-member-register) to also allow `'coop officer'` and `'coop member'` in `allowedUserTypes`.
|
||||
|
||||
### 17. Backend: /Chapters/OrgChart endpoint
|
||||
In `ChapterController.php`, add method `getOrgChart`:
|
||||
- Auth: `auth` middleware
|
||||
- Logic: determine caller's chapter scope (join `chapter_members` → `chapters` for the caller user)
|
||||
- For COOP_OFFICER: find their chapter_members rows, get the highest-level chapter they are in, return full subtree via recursive children() eager load
|
||||
- For COOP_MEMBER: return just their barangay chapter + officers
|
||||
- For Big3/COORDINATOR: accept optional `?cooperative_id=` param, return that coop's full chapter tree
|
||||
- Response: nested JSON `{ id, hashkey, name, level, location_key, children: [...], officers: [{user_id, name, role, position}] }`
|
||||
- Register route in `routes/web.php`: `POST /Chapters/OrgChart` with `auth` middleware
|
||||
|
||||
### 18. Backend: /home-data — add COOP_OFFICER and COOP_MEMBER branches
|
||||
In the controller that handles `GET /home-data`, add:
|
||||
```php
|
||||
if ($user->acct_type === UserTypes::COOP_OFFICER) {
|
||||
// count members in scope, sub-chapters, new members (7d)
|
||||
// attach chapter info (name, level, location_key) from chapter_members join
|
||||
}
|
||||
if ($user->acct_type === UserTypes::COOP_MEMBER) {
|
||||
// attach membership info: chapter name, officer contact, member since
|
||||
}
|
||||
```
|
||||
|
||||
## context
|
||||
|
||||
### Existing chapters migration (canonical)
|
||||
```
|
||||
level enum: ['national', 'region', 'province', 'city', 'barangay']
|
||||
parent_id → chapters.id (nullOnDelete — chapter survives parent deletion)
|
||||
location_key — normalized slug for address matching
|
||||
```
|
||||
NOTE: schema uses 'city' not 'municipal'. In PH context a city/municipality are different — if 'municipal' level is required, add it to the enum in migration step 1 or handle in the plan as a separate sub-step.
|
||||
|
||||
### chapter_members.position
|
||||
Free-text string from `chapter_positions` system setting. The new `role` field (step 2) is the canonical enum: PRESIDENT, VICE_PRESIDENT, SECRETARY, TREASURER, AUDITOR, BOARD_MEMBER, MEMBER.
|
||||
|
||||
### UserPermissions::roles() — existing USER block (line 818)
|
||||
```php
|
||||
UserTypes::USER->value => [
|
||||
UserActions::JoinCooperative,
|
||||
UserActions::ViewUserInfo,
|
||||
UserActions::ManageUserInfo,
|
||||
],
|
||||
```
|
||||
Insert COOP_MEMBER and COOP_OFFICER blocks immediately after this.
|
||||
|
||||
### Home.vue fragment routing pattern
|
||||
- `isCoordinator` → `HomeCooperative` (already exists, used by COORDINATOR)
|
||||
- Insert `isCoopOfficer` → `HomeCoopOfficer` BEFORE `isUser` check
|
||||
- Insert `isCoopMember` → `HomeCoopMember` BEFORE `isUser` check
|
||||
|
||||
### CooperativeController::registerMember acct_type upgrade rule
|
||||
Only upgrade USER → COOP_MEMBER. Never downgrade COORDINATOR, OPERATOR, STORE_OWNER, etc. Check: `$user->acct_type === UserTypes::USER`.
|
||||
|
||||
### Definition of Done checklist reminder
|
||||
- [ ] UserActions::ViewChapterOrgChart etc. added to UserActions.php
|
||||
- [ ] UserPermissions::roles() entries for COOP_MEMBER and COOP_OFFICER
|
||||
- [ ] VueRouteMap allowedUserTypes updated for /chapter-org-chart and existing coop pages
|
||||
- [ ] Direct URL access tested for /chapter-org-chart
|
||||
- [ ] No bg-white/bg-light hardcoded in new Vue fragments
|
||||
- [ ] New raw DB queries (if any) include created_by/updated_by
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none detected
|
||||
- constraints: Use Hypervel imports (NOT Illuminate) in all migrations. chapters.level enum uses 'city' not 'municipal' — discuss with user before changing enum or treat city=municipal for now. The chapter auto-assignment (Chapter::autoAssignUser) already runs on UserInfo save and covers COOP_MEMBER address-based assignment. COOP_OFFICER chapter assignment is manual/administrative only.
|
||||
116
.claude/plans/51269ff2956253b067f7060b41715f84-complete.md
Normal file
116
.claude/plans/51269ff2956253b067f7060b41715f84-complete.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
task: In CreateProductUltimate.vue store-picker modal, allow Big 3 users (ultimate, super operator, operator) to complete product creation without assigning to a store
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-29T04:36:00Z
|
||||
finished: 2026-05-29T04:36:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/CreateProductUltimate.vue [1-323, 570-614] — all logic + store-picker modal template
|
||||
|
||||
## steps
|
||||
1. In `CreateProductUltimate.vue` `<script setup>`, import `useAuth` and compute `isBig3`:
|
||||
```js
|
||||
import { useAuth } from '../composables/Core/useAuth'
|
||||
const { isUltimate, isSuperOperator, isOperator } = useAuth()
|
||||
const isBig3 = computed(() => isUltimate.value || isSuperOperator.value || isOperator.value)
|
||||
```
|
||||
|
||||
2. In `openStorePicker` (line 271-275): for Big 3 users, do NOT pre-select any store so the picker starts with no selection (making it visually optional):
|
||||
```js
|
||||
const openStorePicker = () => {
|
||||
showMatchesModal.value = false
|
||||
pickerStore.value = isBig3.value ? '' : (selectedStore.value || (selectableStores.value[0]?.hashkey ?? ''))
|
||||
showStorePickerModal.value = true
|
||||
}
|
||||
```
|
||||
|
||||
3. In `confirmAndCreate` (line 278): relax the store-required guard so it only applies to non-Big 3:
|
||||
```js
|
||||
const confirmAndCreate = async () => {
|
||||
if (!isBig3.value && selectableStores.value.length > 0 && !pickerStore.value) {
|
||||
error.value = 'Please select a store to assign this product to.'
|
||||
return
|
||||
}
|
||||
selectedStore.value = pickerStore.value
|
||||
showStorePickerModal.value = false
|
||||
await handleSubmit()
|
||||
}
|
||||
```
|
||||
|
||||
4. In the store-picker modal template (line 576-611), make two changes:
|
||||
a. Subtitle: show "optional" note for Big 3 —
|
||||
```html
|
||||
<p class="text-muted small mb-0">
|
||||
Pick the store this product will be listed in.
|
||||
<span v-if="isBig3" class="ms-1 badge bg-info-subtle text-info rounded-pill" style="font-size:0.7em">Optional for your account</span>
|
||||
</p>
|
||||
```
|
||||
b. "Confirm & Create" button `:disabled` — remove store requirement for Big 3:
|
||||
```html
|
||||
:disabled="isLoading || (!isBig3 && selectableStores.length > 0 && !pickerStore)"
|
||||
```
|
||||
c. Add a helper note inside `bb-modal-body` when Big 3 and no store selected:
|
||||
```html
|
||||
<p v-if="isBig3 && !pickerStore" class="text-muted small mt-2 mb-0">
|
||||
<i class="fas fa-info-circle me-1"></i>No store selected — product will be created as a global listing only.
|
||||
</p>
|
||||
```
|
||||
|
||||
## context
|
||||
### useAuth pattern (from MyStores.vue)
|
||||
```js
|
||||
import { useAuth } from '../composables/Core/useAuth'
|
||||
const { user, isLoggedIn, isUltimate, isSuperOperator, isOperator } = useAuth()
|
||||
const isBig3 = computed(() => isUltimate.value || isSuperOperator.value || isOperator.value)
|
||||
```
|
||||
|
||||
### confirmAndCreate current (line 277-285)
|
||||
```js
|
||||
const confirmAndCreate = async () => {
|
||||
if (selectableStores.value.length > 0 && !pickerStore.value) {
|
||||
error.value = 'Please select a store to assign this product to.'
|
||||
return
|
||||
}
|
||||
selectedStore.value = pickerStore.value
|
||||
showStorePickerModal.value = false
|
||||
await handleSubmit()
|
||||
}
|
||||
```
|
||||
|
||||
### handleSubmit sends (line 198-210)
|
||||
```js
|
||||
await axios.post('/Products/Admin/New/', {
|
||||
...
|
||||
TargetStore: selectedStore.value, // backend validates as nullable — OK when empty for Big 3
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
### Backend (ProductController@createNew_Admin line 64)
|
||||
```php
|
||||
'TargetStore' => 'nullable|string',
|
||||
// Big 3 has CreateProductGlobal → no store required, product created globally
|
||||
```
|
||||
|
||||
### Store picker modal footer (line 602-611)
|
||||
```html
|
||||
<div class="bb-modal-footer">
|
||||
<button class="btn btn-link text-muted" @click="showStorePickerModal = false">Cancel</button>
|
||||
<button
|
||||
class="btn btn-primary rounded-pill px-4"
|
||||
:disabled="isLoading || (selectableStores.length > 0 && !pickerStore)"
|
||||
@click="confirmAndCreate"
|
||||
>
|
||||
<span v-if="isLoading"><LoadingSpinner size="small" class="me-2" /> Creating...</span>
|
||||
<span v-else>Confirm & Create</span>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: eslint
|
||||
- constraints: backend already handles empty TargetStore for Big 3 (nullable + CreateProductGlobal permission). No backend changes needed. Frontend-only change.
|
||||
480
.claude/plans/8870909b404440261cd2c2b72ee8e463-complete.md
Normal file
480
.claude/plans/8870909b404440261cd2c2b72ee8e463-complete.md
Normal file
@@ -0,0 +1,480 @@
|
||||
---
|
||||
task: Add DuckDuckGo image search stock photo picker to product creation forms (CreateProductUltimate + CreateProductStoreOwner). Button near dropzone opens modal that auto-searches DDG from product name. Grid uses IntersectionObserver infinite scroll. Selecting a photo downloads the source image + resizes to max 1280x720 server-side (PHP GD), saves via existing FilesMainController pipeline, returns hashkey identical to normal upload. No API key required — DDG is scraped via a 2-step token+JSON approach.
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-28T17:30:13Z
|
||||
finished: 2026-05-28T17:31:50Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `app/Enums/UserActions.php` [last line ~ManageQrphPaymentCode] — add two new actions
|
||||
- `app/Http/Controllers/Helpers/Permissions/UserPermissions.php` — add new actions to role grants
|
||||
- `app/Http/Controllers/Market/ProductPhotoSearchController.php` — NEW controller (search + download)
|
||||
- `app/Http/Controllers/FilesMainController.php` [lines 118-189, 222-228, 328-372] — reuse `uploadFileList()` static; accepts binary string via `isLikelyBinary` branch
|
||||
- `routes/web.php` [line 515 area, near `/File/Upload/{category}`] — add 2 new routes
|
||||
- `resources/js/Components/Core/StockPhotoPicker.vue` — NEW reusable modal component
|
||||
- `resources/js/Pages/CreateProductUltimate.vue` [productName ref line 26] — import picker, add button near dropzone, handle event
|
||||
- `resources/js/Pages/CreateProductStoreOwner.vue` [newProduct.name ref ~line 57+] — import picker, add button near dropzone, handle event
|
||||
|
||||
## steps
|
||||
|
||||
### Backend
|
||||
|
||||
1. **`app/Enums/UserActions.php`** — before the closing `}`, add:
|
||||
```php
|
||||
case SearchStockPhotos = 'searchstockphotos';
|
||||
case DownloadStockPhoto = 'downloadstockphoto';
|
||||
```
|
||||
|
||||
2. **`app/Http/Controllers/Helpers/UserPermissions.php`** — in `roles()`, grant both new actions to `storeowner`, `cooperative`, and `ultimate` roles (same pattern as existing product actions). Do NOT grant to `customer` or guest.
|
||||
|
||||
3. **Create `app/Http/Controllers/Market/ProductPhotoSearchController.php`**:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Http\Controllers\Market;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\FilesMainController;
|
||||
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
|
||||
use App\Enums\UserActions;
|
||||
use Hypervel\Http\Request;
|
||||
|
||||
class ProductPhotoSearchController extends Controller
|
||||
{
|
||||
private const DDG_BROWSER_HEADERS = [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Accept-Language: en-US,en;q=0.9',
|
||||
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Referer: https://duckduckgo.com/',
|
||||
];
|
||||
|
||||
// Step 1: fetch the DDG search page to extract the vqd session token.
|
||||
// DDG requires this token to serve the image JSON endpoint.
|
||||
private static function getDdgVqd(string $query): ?string
|
||||
{
|
||||
$url = 'https://duckduckgo.com/?q=' . urlencode($query) . '&iax=images&ia=images';
|
||||
$ctx = stream_context_create(['http' => [
|
||||
'method' => 'GET',
|
||||
'header' => implode("\r\n", self::DDG_BROWSER_HEADERS),
|
||||
'timeout' => 10,
|
||||
]]);
|
||||
$html = @file_get_contents($url, false, $ctx);
|
||||
if (!$html) return null;
|
||||
// vqd token appears as vqd=4-xxxxxxxxxx or vqd=4-xx-xx in the page JS
|
||||
if (preg_match('/vqd=([0-9a-zA-Z\-]+)/', $html, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET /api/products/photo-search?q=...&page=1
|
||||
public function search(Request $request)
|
||||
{
|
||||
if (!UserPermissions::can(UserActions::SearchStockPhotos)) {
|
||||
return response()->json(['error' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
$query = trim($request->input('q', ''));
|
||||
$page = max(1, (int) $request->input('page', 1));
|
||||
|
||||
if (!$query) {
|
||||
return response()->json(['error' => 'Query required'], 422);
|
||||
}
|
||||
|
||||
$vqd = self::getDdgVqd($query);
|
||||
if (!$vqd) {
|
||||
return response()->json(['error' => 'Could not reach image search service'], 502);
|
||||
}
|
||||
|
||||
// s = offset; DDG returns ~15 results per call; page 1 = s=0, page 2 = s=15, etc.
|
||||
$offset = ($page - 1) * 15;
|
||||
|
||||
$url = 'https://duckduckgo.com/i.js?' . http_build_query([
|
||||
'q' => $query,
|
||||
'vqd' => $vqd,
|
||||
'o' => 'json',
|
||||
'p' => '1',
|
||||
'f' => ',,,',
|
||||
'l' => 'us-en',
|
||||
's' => $offset,
|
||||
]);
|
||||
|
||||
$ctx = stream_context_create(['http' => [
|
||||
'method' => 'GET',
|
||||
'header' => implode("\r\n", self::DDG_BROWSER_HEADERS),
|
||||
'timeout' => 10,
|
||||
]]);
|
||||
|
||||
$raw = @file_get_contents($url, false, $ctx);
|
||||
if ($raw === false) {
|
||||
return response()->json(['error' => 'Failed to fetch image results'], 502);
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
$results = $data['results'] ?? [];
|
||||
|
||||
$photos = array_map(fn($r) => [
|
||||
'id' => md5($r['image']), // stable ID from image URL
|
||||
'thumb' => $r['thumbnail'], // DDG-proxied small thumb (safe to display)
|
||||
'src' => $r['image'], // actual source image URL (used for download)
|
||||
'title' => $r['title'] ?? '',
|
||||
], array_slice($results, 0, 15));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'photos' => $photos,
|
||||
'page' => $page,
|
||||
'has_more' => count($results) >= 15,
|
||||
]);
|
||||
}
|
||||
|
||||
// POST /api/products/photo-download
|
||||
// body: { src: "https://..." } — the actual source image URL from DDG results
|
||||
public function download(Request $request)
|
||||
{
|
||||
if (!UserPermissions::can(UserActions::DownloadStockPhoto)) {
|
||||
return response()->json(['error' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
$src = $request->input('src', '');
|
||||
|
||||
// SSRF guard: must be http/https and must not target private/loopback IPs
|
||||
$parsed = parse_url($src);
|
||||
$scheme = $parsed['scheme'] ?? '';
|
||||
$host = strtolower($parsed['host'] ?? '');
|
||||
|
||||
if (!in_array($scheme, ['http', 'https']) || !$host) {
|
||||
return response()->json(['error' => 'Invalid URL'], 422);
|
||||
}
|
||||
|
||||
// Block private/loopback ranges
|
||||
if (preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0|::1)/i', $host)) {
|
||||
return response()->json(['error' => 'Forbidden URL'], 403);
|
||||
}
|
||||
|
||||
$ctx = stream_context_create(['http' => [
|
||||
'method' => 'GET',
|
||||
'header' => 'User-Agent: Mozilla/5.0' . "\r\n",
|
||||
'timeout' => 15,
|
||||
]]);
|
||||
$raw = @file_get_contents($src, false, $ctx);
|
||||
if ($raw === false || strlen($raw) < 500) {
|
||||
return response()->json(['error' => 'Failed to fetch image'], 502);
|
||||
}
|
||||
|
||||
// Resize to max 1280x720 using PHP GD (bundled — no Intervention Image needed)
|
||||
$srcImg = @imagecreatefromstring($raw);
|
||||
if (!$srcImg) {
|
||||
return response()->json(['error' => 'Invalid image data'], 422);
|
||||
}
|
||||
|
||||
$origW = imagesx($srcImg);
|
||||
$origH = imagesy($srcImg);
|
||||
$maxW = 1280;
|
||||
$maxH = 720;
|
||||
|
||||
$ratio = min($maxW / $origW, $maxH / $origH, 1.0); // never upscale
|
||||
$newW = (int) round($origW * $ratio);
|
||||
$newH = (int) round($origH * $ratio);
|
||||
|
||||
$dstImg = imagescale($srcImg, $newW, $newH, IMG_BILINEAR_FIXED);
|
||||
imagedestroy($srcImg);
|
||||
|
||||
ob_start();
|
||||
imagejpeg($dstImg, null, 85);
|
||||
$binary = ob_get_clean();
|
||||
imagedestroy($dstImg);
|
||||
|
||||
// Save via existing pipeline — binary string branch in uploadFileContent handles this
|
||||
$result = FilesMainController::uploadFileList(
|
||||
$binary,
|
||||
'stock-photo',
|
||||
'stock_photo_' . time() . '.jpg',
|
||||
'',
|
||||
[],
|
||||
'ProductMarket',
|
||||
[],
|
||||
0,
|
||||
'image/jpeg'
|
||||
);
|
||||
|
||||
if (!$result || empty($result->hashkey)) {
|
||||
return response()->json(['error' => 'Save failed'], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'hashkey' => $result->hashkey,
|
||||
'url' => $result->resolvedUrl(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **`routes/web.php`** — near line 515 (existing `/File/Upload/{category}` route), add:
|
||||
```php
|
||||
Route::get('/api/products/photo-search', [\App\Http\Controllers\Market\ProductPhotoSearchController::class, 'search'], ['middleware' => 'auth']);
|
||||
Route::post('/api/products/photo-download', [\App\Http\Controllers\Market\ProductPhotoSearchController::class, 'download'], ['middleware' => 'auth']);
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
5. **Create `resources/js/Components/Core/StockPhotoPicker.vue`**:
|
||||
|
||||
Props: `modelValue: Boolean` (v-model for show/hide), `productName: String`
|
||||
|
||||
Emits: `update:modelValue`, `photo-selected({ hashkey, url })`
|
||||
|
||||
Behavior:
|
||||
- On `modelValue` becoming true: set `query` to `productName`, call `resetAndSearch()`
|
||||
- On `modelValue` becoming false: clear photos, reset page
|
||||
- Search input pre-filled with `productName`, debounce 400ms on manual edits
|
||||
- Photo grid: 3-column responsive grid of cards (thumbnail, title truncated to 1 line)
|
||||
- IntersectionObserver on a sentinel `<div ref="sentinel">` after the last card — when visible and `hasMore && !loadingMore`, call `loadMore()`
|
||||
- Selecting a photo: show spinner overlay on that card, POST to `/api/products/photo-download` with `{ src: photo.src }`, on success emit `photo-selected` and close modal
|
||||
- Error state: dismissible inline alert inside modal body
|
||||
- Initial loading: 9 skeleton cards (3×3) while `loading && photos.length === 0`
|
||||
- Style: CSS variables only — `var(--bg-card)`, `var(--bg-primary)`, `var(--text-primary)`, `var(--accent-color)`; no `bg-white`, `text-dark`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="modelValue" class="modal d-block" tabindex="-1" style="z-index:1055">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content" style="background:var(--bg-card);color:var(--text-primary)">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<div class="d-flex align-items-center gap-2 w-100 me-2">
|
||||
<i class="fas fa-images" style="color:var(--accent-color)"></i>
|
||||
<input v-model="query" type="text" class="form-control rounded-pill form-control-sm"
|
||||
placeholder="Search photos…" style="background:var(--bg-primary);color:var(--text-primary);border-color:rgba(128,128,128,.3)" />
|
||||
</div>
|
||||
<button type="button" class="btn-close" @click="$emit('update:modelValue', false)" />
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<div v-if="error" class="alert alert-danger alert-dismissible py-2 mb-2">
|
||||
{{ error }} <button type="button" class="btn-close" @click="error=null"/>
|
||||
</div>
|
||||
<!-- skeleton -->
|
||||
<div v-if="loading && !photos.length" class="row g-2">
|
||||
<div v-for="n in 9" :key="n" class="col-4">
|
||||
<div class="ratio ratio-4x3 rounded" style="background:rgba(128,128,128,.15);animation:pulse 1.5s infinite" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- grid -->
|
||||
<div v-else class="row g-2">
|
||||
<div v-for="photo in photos" :key="photo.id" class="col-4" style="cursor:pointer" @click="selectPhoto(photo)">
|
||||
<div class="ratio ratio-4x3 position-relative rounded overflow-hidden">
|
||||
<img :src="photo.thumb" class="w-100 h-100 object-fit-cover" :alt="photo.title" loading="lazy" />
|
||||
<div v-if="selecting===photo.id" class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center" style="background:rgba(0,0,0,.45)">
|
||||
<div class="spinner-border text-light spinner-border-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted d-block text-truncate mt-1 px-1" style="font-size:.7rem">{{ photo.title }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<!-- sentinel for infinite scroll -->
|
||||
<div ref="sentinel" class="py-2 text-center">
|
||||
<div v-if="loadingMore" class="spinner-border spinner-border-sm text-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop show" style="z-index:-1" @click="$emit('update:modelValue', false)" />
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const props = defineProps({ modelValue: Boolean, productName: { type: String, default: '' } })
|
||||
const emit = defineEmits(['update:modelValue', 'photo-selected'])
|
||||
|
||||
const photos = ref([])
|
||||
const query = ref('')
|
||||
const page = ref(1)
|
||||
const hasMore = ref(false)
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const selecting = ref(null)
|
||||
const error = ref(null)
|
||||
const sentinel = ref(null)
|
||||
let observer = null
|
||||
let debounceTimer = null
|
||||
|
||||
watch(() => props.modelValue, async (val) => {
|
||||
if (val) { query.value = props.productName; await resetAndSearch() }
|
||||
else { photos.value = []; page.value = 1; observer?.disconnect() }
|
||||
})
|
||||
|
||||
watch(query, () => {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => resetAndSearch(), 400)
|
||||
})
|
||||
|
||||
async function resetAndSearch() {
|
||||
observer?.disconnect()
|
||||
photos.value = []; page.value = 1; loading.value = true; error.value = null
|
||||
await fetchPage(1)
|
||||
loading.value = false
|
||||
await nextTick()
|
||||
setupObserver()
|
||||
}
|
||||
|
||||
async function fetchPage(p) {
|
||||
try {
|
||||
const res = await axios.get('/api/products/photo-search', { params: { q: query.value, page: p } })
|
||||
if (res.data.success) {
|
||||
photos.value.push(...res.data.photos)
|
||||
hasMore.value = res.data.has_more
|
||||
page.value = p
|
||||
}
|
||||
} catch {
|
||||
error.value = 'Could not load photos. Try again.'
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (!hasMore.value || loadingMore.value) return
|
||||
loadingMore.value = true
|
||||
await fetchPage(page.value + 1)
|
||||
loadingMore.value = false
|
||||
await nextTick()
|
||||
setupObserver()
|
||||
}
|
||||
|
||||
function setupObserver() {
|
||||
observer?.disconnect()
|
||||
if (!sentinel.value) return
|
||||
observer = new IntersectionObserver(([e]) => { if (e.isIntersecting) loadMore() }, { threshold: 0.1 })
|
||||
observer.observe(sentinel.value)
|
||||
}
|
||||
|
||||
async function selectPhoto(photo) {
|
||||
if (selecting.value) return
|
||||
selecting.value = photo.id
|
||||
error.value = null
|
||||
try {
|
||||
const res = await axios.post('/api/products/photo-download', { src: photo.src })
|
||||
if (res.data.success) {
|
||||
emit('photo-selected', { hashkey: res.data.hashkey, url: res.data.url })
|
||||
emit('update:modelValue', false)
|
||||
} else {
|
||||
error.value = 'Failed to save photo. Try another.'
|
||||
}
|
||||
} catch {
|
||||
error.value = 'Failed to save photo. Try another.'
|
||||
} finally {
|
||||
selecting.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes pulse { 0%,100%{opacity:.6} 50%{opacity:.3} }
|
||||
</style>
|
||||
```
|
||||
|
||||
6. **`resources/js/Pages/CreateProductUltimate.vue`**:
|
||||
- Import `StockPhotoPicker` at the top with other component imports
|
||||
- Add `const showPhotoPicker = ref(false)` in the script
|
||||
- In the template, just above or below the `<Dropzone>` component, add:
|
||||
```html
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm rounded-pill mb-2"
|
||||
@click="showPhotoPicker = true">
|
||||
<i class="fas fa-images me-1"></i> Search Stock Photos
|
||||
</button>
|
||||
<StockPhotoPicker v-model="showPhotoPicker" :product-name="productName"
|
||||
@photo-selected="onStockPhotoSelected" />
|
||||
```
|
||||
- Add handler in script (after the existing `uploadFile` handlers):
|
||||
```js
|
||||
function onStockPhotoSelected({ hashkey, url }) {
|
||||
// Mirror the shape used in the dropzoneFiles watch (~lines 108-130)
|
||||
// Check the Dropzone component's expected file object shape and match it exactly.
|
||||
// At minimum push to both dropzoneFiles (for UI) and photoHashes (for form submit).
|
||||
dropzoneFiles.value.push({ file: null, hashkey, url, uploading: false, progress: 100, error: null })
|
||||
photoHashes.value.push(hashkey)
|
||||
}
|
||||
```
|
||||
- IMPORTANT: inspect the existing `dropzoneFiles` watch block at ~lines 108-130 to confirm the exact object shape expected. The above is a best-guess — adjust field names if needed.
|
||||
|
||||
7. **`resources/js/Pages/CreateProductStoreOwner.vue`**:
|
||||
- Same as step 6 but:
|
||||
- Product name comes from `newProduct.value.name` — pass as `:product-name="newProduct.name"`
|
||||
- Confirm the name of the dropzone ref and dropzoneFiles ref in this file (likely same as Ultimate but verify)
|
||||
- Same `onStockPhotoSelected` handler
|
||||
|
||||
## context
|
||||
|
||||
```
|
||||
// DuckDuckGo 2-step search flow:
|
||||
// 1. GET https://duckduckgo.com/?q={query}&iax=images&ia=images → extract vqd token from HTML
|
||||
// 2. GET https://duckduckgo.com/i.js?q={query}&vqd={vqd}&o=json&p=1&f=,,,&l=us-en&s={offset}
|
||||
// → JSON: { results: [{ image, thumbnail, title, url }], next: "..." }
|
||||
// thumbnail = DDG-proxied small preview (displayed in grid, safe)
|
||||
// image = actual source URL on original domain (used for download + resize)
|
||||
// offset = (page-1)*15 for pagination; has_more = results.length >= 15
|
||||
|
||||
// SSRF guard in download():
|
||||
// - scheme must be http or https
|
||||
// - host must NOT match: localhost, 127.x, 10.x, 192.168.x, 172.16-31.x, 0.0.0.0, ::1
|
||||
|
||||
// useFileUpload.js:54 — upload endpoint
|
||||
axios.post(`/File/Upload/${category}`, formData) → { success: true, hashkey: "...", url: "..." }
|
||||
|
||||
// FilesMainController.php:135-140 — binary string branch (used by download route)
|
||||
} elseif (self::isLikelyBinary($fileData)) {
|
||||
$fileHash = hash('sha256', $fileData);
|
||||
$fileSize = strlen($fileData);
|
||||
$fileContent = $fileData;
|
||||
$path = Storage::put("files/{$fileHash}", $fileContent);
|
||||
}
|
||||
|
||||
// FilesMainController.php:222-228 — uploadFileList signature
|
||||
public static function uploadFileList(
|
||||
string|UploadedFile $fileData, string $title, string $filename,
|
||||
?string $description = null, ?array $details = [], $categories = null,
|
||||
$tags = [], $hidden = 0, ?string $file_type = null
|
||||
): FileList
|
||||
|
||||
// FilesMainController.php:364 — return shape
|
||||
return response()->json(['success' => true, 'hashkey' => $result->hashkey, 'url' => $file_url, ...])
|
||||
|
||||
// UserActions.php last entries (before closing brace):
|
||||
case ManageQrphPaymentCode = 'manageqrphpaymentcode';
|
||||
}
|
||||
|
||||
// CreateProductUltimate.vue:20-23
|
||||
const { uploadFile, removeHash, photoHashes, isUploading: isFileUploading } = useFileUpload({
|
||||
category: 'ProductMarket', maxSizeMB: 10
|
||||
})
|
||||
// productName ref: line 26 — const productName = ref('')
|
||||
// dropzoneFiles watch: lines ~108-130
|
||||
|
||||
// CreateProductStoreOwner.vue:21-24
|
||||
const { uploadFile, removeHash, photoHashes, isUploading: isFileUploading, uploadError } = useFileUpload({
|
||||
category: 'ProductMarket', maxSizeMB: 10,
|
||||
})
|
||||
// newProduct ref: line ~57+ — newProduct.value.name
|
||||
|
||||
// routes/web.php:515
|
||||
Route::post('/File/Upload/{category}', [FilesMainController::class, 'UploadFilefromRequest'], ['middleware' => 'auth']);
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none detected (eslint:no, phpcs:no, tsc:no)
|
||||
- constraints:
|
||||
- **No API key required** — DuckDuckGo is scraped with browser User-Agent headers; no .env changes needed
|
||||
- Project uses Hypervel (Hyperf + Swoole) — use `Hypervel\*` namespaces, NOT `Illuminate\*`
|
||||
- No Intervention Image installed — use PHP GD (`imagecreatefromstring`, `imagescale`, `imagejpeg`)
|
||||
- No Guzzle — use `file_get_contents` with `stream_context_create`
|
||||
- SSRF guard is on the `download` route (not search) — validate scheme is http/https and host is not a private/loopback address; DO NOT restrict to a specific domain since DDG results link to arbitrary image hosts
|
||||
- No new VueRouteMap entries needed — StockPhotoPicker is a component, not a page
|
||||
- Theme: CSS variables only — no hardcoded `bg-white`, `bg-light`, `text-dark`
|
||||
- The `onStockPhotoSelected` handler must push to BOTH `dropzoneFiles` (UI) AND `photoHashes` (form submit) — verify exact dropzoneFiles entry shape from the Dropzone component before writing
|
||||
- DDG's vqd token format may change; if `getDdgVqd()` returns null, the search returns 502 gracefully — no crash
|
||||
117
.claude/plans/926a10dd4cfc0c544f3bd303986a17a6-complete.md
Normal file
117
.claude/plans/926a10dd4cfc0c544f3bd303986a17a6-complete.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
task: Fix homepage fragment persisting from previous login when switching account types
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-17T16:22:34Z
|
||||
finished: 2026-05-17T16:24:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/stores/user.js [lines 63-95] — fetchCurrentUser() has a guard that skips role re-fetch when acctType is already set; this allows stale sessionStorage role to persist
|
||||
- resources/js/Pages/Home.vue [lines 1-21] — renders role fragments before userStore loading is complete, causing flash of wrong fragment
|
||||
- resources/js/composables/Core/useAuth.js [lines 7-53] — module-level globalRole ref; module-level fetchRole() guard prevents re-fetch when sessionStorage already has a non-public role
|
||||
- resources/js/Pages/Auth/Login.vue [lines 13-38] — sessionStorage.clear() in onMounted runs synchronously, but app.js fetchCurrentUser() is async and can set sessionStorage AFTER the clear
|
||||
|
||||
## steps
|
||||
|
||||
### Step 1 — Fix stale-role guard in `fetchCurrentUser()` (user.js:78-84)
|
||||
The guard `if (!this.acctType || this.acctType === 'public')` prevents re-fetching the real server-side role when `acctType` is already populated from sessionStorage. This is the primary bug: after login, if sessionStorage has the old user's role (e.g. 'ultimate'), `acctType` initialises to 'ultimate' from the store state factory, the guard is false, and the role is never corrected against the server — even though the session now belongs to a completely different user.
|
||||
|
||||
Remove the guard entirely so that `fetchCurrentUser()` **always** re-fetches `acct_type` from `/get/user/acct-type` after successfully loading the user object.
|
||||
|
||||
```js
|
||||
// user.js – fetchCurrentUser(), after setting this.user:
|
||||
// REMOVE the `if (!this.acctType || this.acctType === 'public')` wrapper.
|
||||
// Always fetch and overwrite acctType from the server.
|
||||
const resRole = await axios.get('/get/user/acct-type')
|
||||
if (resRole.data && resRole.data.acct_type) {
|
||||
this.acctType = resRole.data.acct_type
|
||||
sessionStorage.setItem('user_acct_type', this.acctType)
|
||||
}
|
||||
```
|
||||
|
||||
Keep the 401/419 catch path unchanged — it already sets `acctType = 'public'`.
|
||||
|
||||
### Step 2 — Add loading guard to Home.vue before rendering role fragments
|
||||
While `userStore.loading` is true (fetchCurrentUser in flight), `userStore.acctType` may still hold the stale sessionStorage value. Guard all role-specific `<template>` blocks with a top-level loading state so no fragment renders until auth is confirmed.
|
||||
|
||||
In Home.vue add a `userStore` import and wrap the inner content:
|
||||
```js
|
||||
import { useUserStore } from '../stores/user.js';
|
||||
const userStore = useUserStore();
|
||||
```
|
||||
|
||||
In the template, wrap the role switch inside a `v-if="!userStore.loading || userStore.user"` block. While loading and no user yet, show a minimal centered spinner (reuse the existing spinner pattern in the codebase — `<div class="spinner-border text-primary">`). Once `userStore.user` is resolved (non-null), the correct fragment renders reactively.
|
||||
|
||||
### Step 3 — Reset globalRole in useAuth.js when Login.vue mounts
|
||||
Export a `resetRole()` function from `useAuth.js` that sets `globalRole.value = UserTypes.PUBLIC` and removes `sessionStorage.user_acct_type`. Call it from Login.vue's `onMounted` AFTER `sessionStorage.clear()`.
|
||||
|
||||
This ensures the module-level `globalRole` is synchronously reset before any async work sets it again, eliminating any window where the stale in-memory value could be read.
|
||||
|
||||
```js
|
||||
// useAuth.js – add export:
|
||||
export function resetRole() {
|
||||
globalRole.value = UserTypes.PUBLIC;
|
||||
isFetching.value = false;
|
||||
sessionStorage.removeItem('user_acct_type');
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// Login.vue – onMounted, after sessionStorage.clear():
|
||||
import { resetRole } from '../../composables/Core/useAuth.js';
|
||||
import { useUserStore } from '../../stores/user.js';
|
||||
|
||||
onMounted(() => {
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
resetRole();
|
||||
useUserStore().resetCurrentUser(); // clears user + acctType
|
||||
// existing SW unregistration code...
|
||||
});
|
||||
```
|
||||
|
||||
### Step 4 — Extend `resetCurrentUser()` in user.js to also clear acctType
|
||||
Currently `resetCurrentUser()` only sets `this.user = null`. Extend it to also reset `acctType` and remove the sessionStorage key so the role computed falls back to PUBLIC on next render.
|
||||
|
||||
```js
|
||||
resetCurrentUser() {
|
||||
this.user = null;
|
||||
this.acctType = null;
|
||||
sessionStorage.removeItem('user_acct_type');
|
||||
},
|
||||
```
|
||||
|
||||
## context
|
||||
|
||||
### Root cause trace
|
||||
On a fresh page load at `/` after logging in as a different user:
|
||||
|
||||
1. `user.js` Pinia store state factory runs: `acctType = sessionStorage.getItem('user_acct_type') || null`
|
||||
2. If sessionStorage still has the PREVIOUS user's type (e.g. 'ultimate') — possible because `fetchCurrentUser()` on the login page ran AFTER `Login.vue` cleared sessionStorage and re-set it — `acctType = 'ultimate'`
|
||||
3. `app.js` calls `userStore.fetchCurrentUser()` (async)
|
||||
4. Guard at user.js:78 fires: `!this.acctType` = false, `this.acctType !== 'public'` = true → **guard is false → role NOT re-fetched from server**
|
||||
5. `userStore.user` is updated to the NEW user's data but `acctType` stays 'ultimate'
|
||||
6. `role` computed in `useAuth.js` returns `userStore.acctType = 'ultimate'`
|
||||
7. Home.vue renders `<HomeUltimate />` for the new STORE_OWNER user
|
||||
|
||||
### Key reactive chain
|
||||
```
|
||||
role (computed) = userStore.acctType || currentUser.value?.acct_type || globalRole.value
|
||||
↑ populated from sessionStorage on store init → stale
|
||||
```
|
||||
|
||||
### Affected files key refs
|
||||
- `user.js:12` — `acctType: sessionStorage.getItem('user_acct_type') || null`
|
||||
- `user.js:78-84` — the stale guard (primary bug)
|
||||
- `user.js:190` — `resetCurrentUser()` only clears `user`, not `acctType`
|
||||
- `useAuth.js:7` — `globalRole` module-level ref
|
||||
- `useAuth.js:51-53` — module-level fetchRole() call (won't fire if sessionStorage is non-public)
|
||||
- `useAuth.js:76-86` — `role` computed priority: userStore.acctType first
|
||||
- `Home.vue:15-20` — destructures from `useAuth()` with no loading guard
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: eslint, tsc (via vite)
|
||||
- constraints: Hypervel SPA — full page reloads happen after login/logout; sessionStorage persists within a tab across reloads. The guard removal in step 1 adds one extra request per page load but is required for correctness. The loading guard in step 2 may briefly show a spinner before the dashboard; this is acceptable and far better than showing the wrong dashboard.
|
||||
98
.claude/plans/9377517805b9295fb90a761dc4520c03-complete.md
Normal file
98
.claude/plans/9377517805b9295fb90a761dc4520c03-complete.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
task: Fix "Similar products already exist" modal layout — match-row cards unreadable on mobile (flex row squishes info text to single characters, Import button overlaps content)
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-28T16:30:42Z
|
||||
finished: 2026-05-28T16:31:11Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/CreateProductUltimate.vue [lines 534-560] — fuzzy match modal template: `.match-row` items with thumb, info, button
|
||||
- resources/js/Pages/CreateProductUltimate.vue [lines 875-914] — scoped CSS: `.match-row`, `.match-thumb`, `.match-info` and dark mode overrides
|
||||
|
||||
## steps
|
||||
1. In `CreateProductUltimate.vue` template (lines 534-560), wrap the `FileImage` + `.match-info` div in a new `<div class="match-row-top">` inner wrapper, leaving the `<button>` outside it so the layout becomes: outer `.match-row` (column) → `.match-row-top` (thumb + info, row) + button (full-width, below)
|
||||
2. Add `w-100` class to the Import button so it spans the full card width on mobile
|
||||
3. In the scoped CSS, change `.match-row` from `align-items: center` flex-row to a `flex-direction: column; gap: 10px` column layout. Add `.match-row-top { display: flex; align-items: center; gap: 12px; }` rule.
|
||||
4. Remove `align-items: center` from `.match-row` (it now stacks vertically, not horizontally)
|
||||
5. Verify `.match-info { flex: 1; min-width: 0; }` stays on `.match-info` (unchanged — still needed inside `.match-row-top`)
|
||||
6. Build: `npm run build` then `docker restart bukidbountyapp`
|
||||
|
||||
## context
|
||||
|
||||
Current template structure (lines 534-560):
|
||||
```html
|
||||
<div v-for="m in fuzzyMatches" :key="m.hashkey" class="match-row">
|
||||
<FileImage :src="..." class="match-thumb" fallback="..." />
|
||||
<div class="match-info">
|
||||
<div class="fw_6">{{ m.name }}</div>
|
||||
<div class="text-muted small">
|
||||
<span v-if="m.category">{{ m.category }}<span v-if="m.subcategory"> · {{ m.subcategory }}</span> · </span>
|
||||
<span>₱{{ m.price }} / {{ m.unitname }}</span>
|
||||
</div>
|
||||
<div v-if="m.already_in_store" class="text-success smallest mt-1">...</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary rounded-pill" :disabled="m.already_in_store || isImporting" @click="importExistingProduct(m)">
|
||||
<span v-if="isImporting"><LoadingSpinner size="small" /></span>
|
||||
<span v-else-if="m.already_in_store">In Store</span>
|
||||
<span v-else>Import to Store</span>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Target template structure:
|
||||
```html
|
||||
<div v-for="m in fuzzyMatches" :key="m.hashkey" class="match-row">
|
||||
<div class="match-row-top">
|
||||
<FileImage :src="..." class="match-thumb" fallback="..." />
|
||||
<div class="match-info">
|
||||
<div class="fw_6">{{ m.name }}</div>
|
||||
<div class="text-muted small">
|
||||
<span v-if="m.category">{{ m.category }}<span v-if="m.subcategory"> · {{ m.subcategory }}</span> · </span>
|
||||
<span>₱{{ m.price }} / {{ m.unitname }}</span>
|
||||
</div>
|
||||
<div v-if="m.already_in_store" class="text-success smallest mt-1">...</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary rounded-pill w-100" :disabled="m.already_in_store || isImporting" @click="importExistingProduct(m)">
|
||||
...
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Current CSS (lines 875-890):
|
||||
```css
|
||||
.match-row {
|
||||
display: flex;
|
||||
align-items: center; /* ← causes single-column squish when button competes for width */
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.match-thumb { width: 56px; height: 56px; border-radius: 10px; object-fit: cover; flex-shrink: 0; }
|
||||
.match-info { flex: 1; min-width: 0; }
|
||||
```
|
||||
|
||||
Target CSS additions/changes:
|
||||
```css
|
||||
.match-row {
|
||||
display: flex;
|
||||
flex-direction: column; /* vertical stack: top-row then button */
|
||||
gap: 10px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.match-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
/* .match-thumb and .match-info stay unchanged */
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none
|
||||
- constraints: `.bb-modal*` styles are scoped to `CreateProductUltimate.vue` — do NOT touch global CSS. Dark mode override `:global(.dark-mode) .match-row` at line 914 only sets `border-bottom-color` — no changes needed there.
|
||||
- build: `npm run build` then `docker restart bukidbountyapp` (see dictionary Build & Deployment Standards)
|
||||
77
.claude/plans/9407eb3f723064594aef12202c29e320-complete.md
Normal file
77
.claude/plans/9407eb3f723064594aef12202c29e320-complete.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
task: ManageStoresAdmin page — show Delete and Assign Products actions for STORE_OWNER account type on stores they own
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-17T08:00:00Z
|
||||
finished: 2026-05-17T08:01:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/ManageStoresAdmin.vue [lines 1-28, 88-125, 127-148, 242-263] — main page; action buttons gated by `canModifyStore(store)` → `!!store.user_can_manage`; currently only imports `isUltimate, isSuperOperator, isOperator` from useAuth; does not import `isStoreOwner`
|
||||
- app/Http/Controllers/Market/StoreController.php [lines 1299-1350] — `listStores_Admin`: for non-Big3 users sets `$s->user_can_manage` as dynamic attribute via `->each()`; dynamic Eloquent attributes may not serialize reliably in Hypervel
|
||||
- resources/js/composables/Core/useAuth.js [lines 106, 118-141] — exports `isStoreOwner` computed (role === STORE_OWNER); already available but not used in ManageStoresAdmin
|
||||
|
||||
## steps
|
||||
1. In `ManageStoresAdmin.vue` line 16, add `isStoreOwner` to destructured `useAuth()` return
|
||||
2. In `ManageStoresAdmin.vue`, update `canModifyStore` to also return true when `isStoreOwner.value` AND `store.user_can_manage` is truthy — or add a separate `isOwnerOf(store)` helper that checks `isStoreOwner.value && !!store.user_can_manage` as secondary gate on the action buttons
|
||||
3. In `ManageStoresAdmin.vue` action buttons column (lines 247-254), change the `v-if` on Delete and Assign Products buttons to: `v-if="canModifyStore(store) || isStoreOwner"` is NOT correct (too broad); instead ensure `canModifyStore` works by fixing step 4 below
|
||||
4. In `StoreController.php` `listStores_Admin` (around line 1317 and 1333), replace dynamic attribute assignment `$s->user_can_manage = ...` with `$s->setAttribute('user_can_manage', ...)` to guarantee it lands in the model's `$attributes` array and is included in the JSON response
|
||||
5. Verify `listStores_Admin` correctly includes store owner stores: `$allowedUserIds = [$user->id, ...$user->getAllDescendants()]` — `owner_id` of the store must be in this list for store owners who created stores; no change needed if logic is already correct
|
||||
|
||||
## context
|
||||
**ManageStoresAdmin.vue — useAuth import (line 16):**
|
||||
```js
|
||||
const { isUltimate, isSuperOperator, isOperator, user } = useAuth()
|
||||
// missing: isStoreOwner
|
||||
```
|
||||
|
||||
**canModifyStore function (lines 25-27):**
|
||||
```js
|
||||
const canModifyStore = (store) => {
|
||||
return !!store.user_can_manage
|
||||
}
|
||||
```
|
||||
|
||||
**Action buttons (lines 242-254):**
|
||||
```vue
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button @click="viewStore(store)" ...> <!-- no v-if, always shows -->
|
||||
<button v-if="canModifyStore(store)" @click="navigate({page:'AddProductsToStore',...})"> <!-- Assign Products -->
|
||||
<button v-if="canModifyStore(store)" @click="editStore(store)"> <!-- Edit -->
|
||||
<button v-if="canModifyStore(store)" @click="deleteStore(store)"> <!-- Delete -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Backend listStores_Admin — non-Big3 path (StoreController.php ~1333):**
|
||||
```php
|
||||
->each(function ($s) use ($allowedUserIds) {
|
||||
$s->user_can_manage = in_array($s->owner_id, $allowedUserIds) // dynamic property — may not serialize
|
||||
|| in_array($s->manager_id, $allowedUserIds)
|
||||
|| $s->managers->pluck('user_id')->intersect($allowedUserIds)->isNotEmpty();
|
||||
unset($s->managers);
|
||||
});
|
||||
```
|
||||
|
||||
Fix: use `setAttribute`:
|
||||
```php
|
||||
$s->setAttribute('user_can_manage',
|
||||
in_array($s->owner_id, $allowedUserIds)
|
||||
|| in_array($s->manager_id, $allowedUserIds)
|
||||
|| $s->managers->pluck('user_id')->intersect($allowedUserIds)->isNotEmpty()
|
||||
);
|
||||
```
|
||||
|
||||
Also fix the Big3 path (~line 1317):
|
||||
```php
|
||||
->each(fn($s) => $s->user_can_manage = true);
|
||||
// change to:
|
||||
->each(fn($s) => $s->setAttribute('user_can_manage', true));
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: none
|
||||
- linters: eslint, tsc (check with `npm run type-check` or `tsc --noEmit` if available)
|
||||
- constraints: Do not change route access control — store owners already have access to `/Admin/Stores/List`, `/Admin/Store/Delete`, `/Admin/Store/ToggleStatus` routes (middleware is only `module:stores`, not role-gated)
|
||||
- The `deleteStore` and `assignProducts` functions in ManageStoresAdmin already handle the STORE_OWNER case correctly via `canModifyStore` guard — no logic change needed there
|
||||
- `MyStores.vue` (at `/my-stores`) is a separate page not linked from HomeStoreOwner; ignore it for this task
|
||||
60
.claude/plans/98b3202e344f4ad777c9555b5e93fe70-complete.md
Normal file
60
.claude/plans/98b3202e344f4ad777c9555b5e93fe70-complete.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
task: Fix LoginController null dereference causing 500 on login with non-existent user
|
||||
cycles: 3
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T15:59:00Z
|
||||
finished: 2026-05-16T15:59:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- app/Http/Controllers/LoginController.php [lines 34-49] — bug site: Log::info reads $user->active and $user->acct_type->value BEFORE the null guard on line 44
|
||||
|
||||
## steps
|
||||
1. In `LoginController::authenticate` move the `Log::info('Login attempt', [...])` block (lines 36-42) to AFTER the `if (!$user)` early return on line 44. The log then only fires for users that actually exist, eliminating the null dereference.
|
||||
- Alternatively: keep the log in place but use null-safe operators — `$user?->active` and `$user?->acct_type?->value` — so PHP 8 does not throw when `$user` is null.
|
||||
- Preferred: move the log after the null check (cleaner, no PHP-version nuance).
|
||||
|
||||
## context
|
||||
```php
|
||||
// LoginController.php — current broken order (lines 34-49):
|
||||
$user = User::whereIn('mobile_number', $candidates)->first();
|
||||
|
||||
Log::info('Login attempt', [ // <-- fires BEFORE null check
|
||||
'mobile_number' => $credentials['mobile_number'],
|
||||
'candidates' => $candidates,
|
||||
'user_found' => !!$user,
|
||||
'active' => $user->active ?? null, // ERROR in PHP 8 when $user === null
|
||||
'acct_type' => $user->acct_type->value ?? null, // ERROR in PHP 8 when $user === null
|
||||
]);
|
||||
|
||||
if (!$user) { // <-- null check is TOO LATE
|
||||
return Response::json(['success' => false, 'message' => 'Account not found.'], 401);
|
||||
}
|
||||
```
|
||||
|
||||
Fix — move log after null guard:
|
||||
```php
|
||||
$user = User::whereIn('mobile_number', $candidates)->first();
|
||||
|
||||
if (!$user) {
|
||||
return Response::json(['success' => false, 'message' => 'Account not found.'], 401);
|
||||
}
|
||||
|
||||
Log::info('Login attempt', [
|
||||
'mobile_number' => $credentials['mobile_number'],
|
||||
'candidates' => $candidates,
|
||||
'user_found' => true,
|
||||
'active' => $user->active,
|
||||
'acct_type' => $user->acct_type->value ?? null,
|
||||
]);
|
||||
```
|
||||
|
||||
Root cause: PHP 8.0+ changed property access on null from a Warning to a fatal Error. The `??` null-coalescing operator does NOT catch Errors — only null/undefined values. So `$user->active ?? null` still throws `Error: Attempt to read property "active" on null` when `$user` is null.
|
||||
|
||||
Symptom: `POST /post/loginnow` returns HTTP 500 (generic Server Error page) for any mobile number that has no matching user record, instead of returning 401 `{"success":false,"message":"Account not found."}`.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none
|
||||
- constraints: Hypervel (PHP 8.2). Do NOT use Illuminate classes. Use `Response::json()` facade, not `response()->json()`.
|
||||
148
.claude/plans/a13c7ea870dcf8f3de8e22185ead7dcf-complete.md
Normal file
148
.claude/plans/a13c7ea870dcf8f3de8e22185ead7dcf-complete.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
task: Create HomeStoreManager.vue — a dedicated Store Manager home dashboard — and wire it into Home.vue
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:02:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `resources/js/Pages/Home.vue` [lines 67-69] — STORE_MANAGER currently mapped to `HomeShared`; change to new `HomeStoreManager`
|
||||
- `resources/js/Pages/Fragments/Home/HomeStoreManager.vue` — NEW FILE to create
|
||||
- `resources/js/Pages/Fragments/Home/HomeStoreOwner.vue` — primary reference pattern to follow (BalanceBox + ServiceButtonGrid + openPos logic)
|
||||
- `resources/js/Pages/Fragments/Home/HomeShared.vue` [lines 56-93] — has `openPos()` store-select logic; copy this exact pattern
|
||||
- `resources/js/Components/Core/Stats/BalanceBox.vue` — props: `stats[]`, `footerItems[]`, `boxStyle`
|
||||
- `resources/js/Components/Core/Services/ServiceButtonGrid.vue` — prop: `items[]` with `icon`, `title`, `pagename|action`
|
||||
- `resources/js/Components/Core/Services/SideTextButtonList.vue` — quick text actions
|
||||
- `resources/js/Components/Core/Skeleton/HomeSkeleton.vue` — loading skeleton
|
||||
- `resources/js/composables/usePageData.js` — `fetchPageData(url, payload)`
|
||||
- `routes/web.php` [lines 76-135] — `/home-data` — already returns `transactions_today_no`, `cash_flow_today_php`; also returns `my_stores_no` scoped to store_managers pivot
|
||||
- `app/Http/Controllers/Support/VueRouteMap.php` — must confirm `ManageStoresAdmin`, `PosMain`, `PosHistory`, `PosAccessKeys`, `ManageProductsAdmin` allow `store manager`
|
||||
|
||||
## steps
|
||||
1. **`routes/web.php` @ `/home-data`** — The existing `$myStoresCount` for non-Big3 only counts `owner_id` stores (line ~113). Update to also count stores where the user is in the `store_managers` pivot, so STORE_MANAGER sees the correct count:
|
||||
```php
|
||||
$myStoresCount = !$isBig3 && $user
|
||||
? \App\Models\Market\Store::where('owner_id', $user->id)
|
||||
->orWhereHas('managers', fn($q) => $q->where('user_id', $user->id))
|
||||
->count()
|
||||
: $storeCount;
|
||||
```
|
||||
This change is backward-compatible (STORE_OWNER still works, now STORE_MANAGER also gets a nonzero count).
|
||||
|
||||
2. **Create `resources/js/Pages/Fragments/Home/HomeStoreManager.vue`** with this structure:
|
||||
|
||||
**Script (setup):**
|
||||
- Imports: `ref, onMounted, h`, `usePageData`, `useNavigate`, `useAuth`, `useModal`, `BalanceBox`, `ServiceButtonGrid`, `SideTextButtonList`, `HomeSkeleton`, `axios`
|
||||
- `stats` ref:
|
||||
```js
|
||||
[
|
||||
{ title: 'Transactions', number: 0, unit: 'Today', align: 'left', numberId: 'transactions_today_no' },
|
||||
{ title: 'Cash Flow', number: '0.00', unit: 'PHP Today', align: 'left', numberId: 'cash_flow_today_php' },
|
||||
{ title: 'My Stores', number: 0, unit: 'Assigned', align: 'right', numberId: 'my_stores_no' },
|
||||
]
|
||||
```
|
||||
- `balanceFooterItems` ref:
|
||||
```js
|
||||
[
|
||||
{ title: 'Open POS', icon: '<pos-icon-url>', action: 'openPos' },
|
||||
{ title: 'My Stores', icon: '<store-icon-url>', pagename: 'ManageStoresAdmin' },
|
||||
]
|
||||
```
|
||||
- `services` array (8 tiles):
|
||||
1. `{ icon:'<pos icon>', title:'Open POS', action:'openPos' }`
|
||||
2. `{ icon:'<inventory icon>', title:'Inventory', pagename:'ManageProductsAdmin' }`
|
||||
3. `{ icon:'<history icon>', title:'POS History', action:'openPosHistory' }`
|
||||
4. `{ icon:'<store icon>', title:'Manage Stores', pagename:'ManageStoresAdmin' }`
|
||||
5. `{ icon:'<product icon>', title:'Add Product', pagename:'CreateProductStoreOwner' }`
|
||||
6. `{ icon:'<key icon>', title:'POS Keys', pagename:'PosAccessKeys' }`
|
||||
7. `{ icon:'<customers icon>', title:'Customers', action:'viewCustomers' }`
|
||||
8. `{ icon:'<report icon>', title:'Reports', pagename:'ListReports' }`
|
||||
- `quickActions` array (SideTextButtonList items):
|
||||
- `{ text:'Onboard New User', pagename:'CreateUser', icon:'...' }`
|
||||
- `{ text:'My Personal Profile', pagename:'UserInfoEdit', icon:'...' }`
|
||||
- `{ text:'Add Transaction', pagename:'AddTransaction', icon:'...' }`
|
||||
- `openPos()` — identical copy of the logic in `HomeShared.vue` lines 77-92:
|
||||
- POST to `/ListStores/MyStores/data`
|
||||
- If 0 stores → modal warn
|
||||
- If 1 store → navigate to `PosMain` with `target: stores[0].hashkey`
|
||||
- If >1 → `showStoreSelectModal(stores)` with per-store buttons
|
||||
- `openPosHistory()` — same store-picker logic but navigates to `PosHistory` with the store's hashkey
|
||||
- `viewCustomers()` — navigate to `ManageStoresAdmin` (customers are visible per-store there)
|
||||
- `applyStats()` — standard pattern from HomeStoreOwner
|
||||
- `onMounted` → `fetchPageData('/home-data', {})` then `applyStats()`
|
||||
|
||||
**Template:**
|
||||
```html
|
||||
<div class="home-fragment pb-5">
|
||||
<HomeSkeleton v-if="loading" />
|
||||
<template v-else>
|
||||
<BalanceBox :stats="stats" :footerItems="balanceFooterItems" @footer-click="handleItemClick" />
|
||||
<div class="tf-container mt-4">
|
||||
<h5 class="fw_7 mb-3">Store Manager Tools</h5>
|
||||
<ServiceButtonGrid :items="services" @item-click="handleItemClick" />
|
||||
</div>
|
||||
<div class="tf-container mt-4">
|
||||
<h5 class="fw_7 mb-3">Quick Actions</h5>
|
||||
<SideTextButtonList :items="quickActions" @item-click="handleItemClick" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Style:** No `bg-white`/`bg-light`/`text-dark`. Theme-aware only.
|
||||
|
||||
3. **`resources/js/Pages/Home.vue`** — Add import line: `import HomeStoreManager from './Fragments/Home/HomeStoreManager.vue';`
|
||||
— Change lines 67-69 from `<HomeShared title="Store Manager" />` to `<HomeStoreManager />`.
|
||||
|
||||
4. **`app/Http/Controllers/Support/VueRouteMap.php`** — Confirm all pages used by this dashboard have `'store manager'` in their `allowedUserTypes`. Required pages: `ManageStoresAdmin`, `ManageProductsAdmin`, `CreateProductStoreOwner`, `PosAccessKeys`, `PosHistory`, `ListReports`, `AddTransaction`, `CreateUser`, `UserInfoEdit`. Add `'store manager'` to any that are missing it.
|
||||
|
||||
5. **`app/Http/Controllers/Helpers/Permissions/UserPermissions.php`** — Confirm `STORE_MANAGER` has these actions in `roles()`:
|
||||
- `ViewTransactions` (for Reports)
|
||||
- `ViewAccountingReports` (for ListReports)
|
||||
- `CreatePosAccessKey`, `DeletePosAccessKey`, `TogglePosAccessKey`
|
||||
- `CreateProductForOwnStore`, `AddProducttoOwnStore`
|
||||
Add any that are missing.
|
||||
|
||||
## context
|
||||
```
|
||||
// HomeShared.vue openPos() pattern (lines 77-92):
|
||||
const openPos = async () => {
|
||||
try {
|
||||
const { data: stores } = await axios.post('/ListStores/MyStores/data', {});
|
||||
if (!stores || stores.length === 0) {
|
||||
modal.quickDismiss({ title: 'No Store Found', body: 'You have no active stores assigned to your account.' });
|
||||
return;
|
||||
}
|
||||
if (stores.length === 1) {
|
||||
navigate({ page: 'PosMain', props: { target: stores[0].hashkey } });
|
||||
return;
|
||||
}
|
||||
showStoreSelectModal(stores);
|
||||
} catch (e) {
|
||||
modal.quickDismiss({ title: 'Error', body: 'Could not load your stores. Please try again.' });
|
||||
}
|
||||
};
|
||||
|
||||
// Home.vue lines 67-69 currently:
|
||||
// <template v-else-if="isStoreManager">
|
||||
// <HomeShared title="Store Manager" />
|
||||
// </template>
|
||||
|
||||
// /home-data stats keys available: transactions_today_no, cash_flow_today_php, my_stores_no
|
||||
// CDN icon URLs in use (from HomeStoreOwner.vue):
|
||||
// POS key: https://cdn.jsdelivr.net/gh/.../a/5b5ef88c0ad1.svg
|
||||
// Store: https://cdn.jsdelivr.net/gh/.../a/85605eacd4c8.bin
|
||||
// Product: https://cdn.jsdelivr.net/gh/.../a/f0a0193d728e.bin
|
||||
// Import: https://cdn.jsdelivr.net/gh/.../a/ef1a9a079a2d.svg
|
||||
// Reports: https://cdn.jsdelivr.net/gh/.../a/f87407046b18.bin
|
||||
// Profile: https://cdn.jsdelivr.net/gh/.../a/ac7a1cebe580.bin
|
||||
// AddTxn: https://cdn.jsdelivr.net/gh/.../a/c9fd442fe676.bin
|
||||
// Users: https://cdn.jsdelivr.net/gh/.../a/516ed2aaaa4c.bin
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: `ai-docs/dictionary.md`
|
||||
- linters: none detected
|
||||
- constraints: Do NOT give store manager access to global product editing or user-type creation beyond their hierarchy. The `openPos` logic must re-use the exact same store-picker modal pattern from HomeShared. Use CDN icon URLs already in the project — do not introduce new external URLs. No `bg-white` or hardcoded colors.
|
||||
44
.claude/plans/a5e5578620a10c7aab0cd01c89592421-complete.md
Normal file
44
.claude/plans/a5e5578620a10c7aab0cd01c89592421-complete.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
task: Fix Cancel button on Create Product (Store Owner) being clipped by the fixed bottom navigation bar
|
||||
cycles: 3
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T08:42:00Z
|
||||
finished: 2026-05-16T08:42:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/CreateProductStoreOwner.vue [lines 422-424, 500-511, 722-724] — root element uses `pb-5` (48px) which is insufficient to clear the fixed BottomNav; Cancel button in STEP.PICK is obscured
|
||||
- resources/js/Pages/Core/Fragments/BottomNav.vue [lines 59-73] — fixed bottom bar with `padding: 10px 0 env(safe-area-inset-bottom, 10px)`; total rendered height is ~60-70px plus safe-area inset
|
||||
|
||||
## steps
|
||||
1. In `CreateProductStoreOwner.vue` line 423, change the root `<div class="csop-page pb-5">` bottom padding to clear the fixed BottomNav. Replace `pb-5` with a scoped CSS class that sets `padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px))` so content is never obscured on any device.
|
||||
2. Add that scoped style rule to the `<style scoped>` block at the bottom of the file (after the existing `.csop-page` implicit styles). Target `.csop-page { padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px)); }` — remove `pb-5` from the class attribute at the same time.
|
||||
3. Verify all five steps (PICK, NEW_GLOBAL, DESCRIPTION, ASSIGN_STORES, PER_STORE) still have their bottom nav-bar / button rows visible; they all sit inside the same `.csop-page` root, so the padding fix covers all steps.
|
||||
|
||||
## context
|
||||
```
|
||||
// CreateProductStoreOwner.vue:422-424
|
||||
<template>
|
||||
<div class="csop-page pb-5"> ← pb-5 = 48px; not enough to clear ~65px BottomNav
|
||||
|
||||
// STEP.PICK Cancel button block: lines 506-510
|
||||
<div class="text-center mt-3">
|
||||
<button class="btn-text" @click="navigate({ page: 'Home' })">
|
||||
<i class="fas fa-chevron-left me-2"></i> Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
// BottomNav.vue:59-73 — fixed bar
|
||||
.bottom-navigation-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
padding: 10px 0 env(safe-area-inset-bottom, 10px);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none
|
||||
- constraints: Do NOT add `bg-white` or `bg-light` to the page. Do NOT hardcode dark-mode overrides for the padding rule — padding does not need theme overrides.
|
||||
121
.claude/plans/ac00417f5d6982249ae0e9cd1467da15-complete.md
Normal file
121
.claude/plans/ac00417f5d6982249ae0e9cd1467da15-complete.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
task: Enhance HomeOperator.vue (Corporation dashboard) — fix dark-mode bg-white violations, add multi-store performance stats, and improve corporate presentation for demo screenshots
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:03:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `resources/js/Pages/Fragments/Home/HomeOperator.vue` — primary target; has `bg-white` hardcoded in 4 places, missing dark-mode support, missing multi-store corporate stats
|
||||
- `resources/js/Pages/Fragments/Home/HomeSuperOperator.vue` — secondary reference; check what stats/layout it uses
|
||||
- `routes/web.php` [lines 76-135] — `/home-data` endpoint; needs OPERATOR-scoped stats: store count under management, today's revenue across all managed stores, active POS sessions
|
||||
- `app/Http/Controllers/Support/VueRouteMap.php` — confirm OPERATOR has access to all pages used in the dashboard
|
||||
|
||||
## steps
|
||||
1. **`routes/web.php` @ `/home-data` closure** — Add OPERATOR-specific stats after the existing `$myStoresCount` block:
|
||||
```php
|
||||
$isOperator = $acctType === \App\Enums\UserTypes::OPERATOR;
|
||||
$operatorStoreIds = [];
|
||||
if ($isOperator && $user) {
|
||||
$operatorStoreIds = \App\Models\Market\Store::where('owner_id', $user->id)
|
||||
->orWhereHas('managers', fn($q) => $q->where('user_id', $user->id))
|
||||
->pluck('id')->toArray();
|
||||
}
|
||||
$activePosSessionsNo = $isOperator
|
||||
? \App\Models\Market\PosSession::whereIn('store_id', $operatorStoreIds)->where('status','PENDING')->count()
|
||||
: \App\Models\Market\PosSession::where('status','PENDING')->count();
|
||||
$managedStoresNo = $isOperator ? count($operatorStoreIds) : $storeCount;
|
||||
$todayRevenueOperator = $isOperator
|
||||
? \App\Models\GlobalTransaction::whereDate('created_at', today())
|
||||
->whereIn('store_id', $operatorStoreIds)
|
||||
->where('flow', \App\Enums\Market\TransactionFlow::INCOME->value)
|
||||
->sum('amount')
|
||||
: $projectedIncomeToday;
|
||||
```
|
||||
Add to `$props['props']['stats']`:
|
||||
- `'active_pos_sessions_no' => $activePosSessionsNo`
|
||||
- `'managed_stores_no' => $managedStoresNo`
|
||||
- `'today_revenue_php' => number_format((float) $todayRevenueOperator, 2)`
|
||||
|
||||
2. **`HomeOperator.vue` — Fix all `bg-white` violations** (lines ~146, 172, 191, 192):
|
||||
- Replace `bg-white` class with `style="background: var(--bg-card);"` or remove the class entirely (global bootstrap override handles it)
|
||||
- Line ~146: `class="card border-0 shadow-sm rounded-4 bg-white p-3"` → remove `bg-white`
|
||||
- Line ~172: `class="card border-0 shadow-sm rounded-20 bg-white overflow-hidden p-0"` → remove `bg-white`
|
||||
- Line ~191: `class="activity-section card border-0 shadow-sm rounded-4 bg-white overflow-hidden"` → remove `bg-white`
|
||||
- Line ~192: `class="card-header bg-white border-0 ..."` → remove `bg-white`
|
||||
- Any `color: #333` or `color: #856404` hardcoded inline → replace with `color: var(--text-primary)` and `color: var(--text-warning)` respectively
|
||||
|
||||
3. **`HomeOperator.vue` — Update stats ref** to include the new corporate KPIs:
|
||||
```js
|
||||
const stats = ref([
|
||||
{ title: "Stores", number: 0, unit: "Managed", align: "left", numberId: "managed_stores_no" },
|
||||
{ title: "Revenue", number: '0.00', unit: "PHP Today", align: "left", numberId: "today_revenue_php" },
|
||||
{ title: "POS Live", number: 0, unit: "Active", align: "right", numberId: "active_pos_sessions_no" },
|
||||
]);
|
||||
```
|
||||
(Replaces the current generic `total_transactions_no`, `total_transactions_php`, `projected_income_today`)
|
||||
|
||||
4. **`HomeOperator.vue` — Add corporate identity header section** before the BalanceBox in the template:
|
||||
```html
|
||||
<div class="tf-container mt-3 mb-1">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="corp-avatar d-flex align-items-center justify-content-center rounded-3 bg-primary text-white fw_7"
|
||||
style="width:48px;height:48px;font-size:1.2rem;flex-shrink:0;">
|
||||
<i class="fas fa-building"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw_7" style="font-size:1rem;color:var(--text-primary);">{{ user?.name }}</div>
|
||||
<div class="small text-muted">Operator Account · Corporation View</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
Insert this block right after `<HomeSkeleton v-if="loading && !data" />` and `<div v-if="showStaleIndicator">`, before `<div v-if="data">` opens.
|
||||
|
||||
5. **`HomeOperator.vue` — Add POS Quick Launch button** to `balanceFooterItems` ref:
|
||||
Current footer has `AddTransaction` and `ListReports`. Add:
|
||||
- `{ title: 'POS Monitor', icon: '<pos-icon-url>', pagename: 'PosHistory' }` — or use a store-select flow like HomeShared
|
||||
|
||||
6. **`HomeOperator.vue` — Add `services` tile for Corporation-specific items** that are missing:
|
||||
Ensure the grid includes:
|
||||
- `{ title: 'Batch Products', pagename: 'BatchAddProducts' }` if not already there
|
||||
- `{ title: 'POS Keys', pagename: 'PosAccessKeys' }` — already present, keep
|
||||
- `{ title: 'Cooperatives', pagename: 'CooperativeList' }` — already present, keep
|
||||
|
||||
7. **`HomeOperator.vue` — Fix the stale-notice style** to use theme vars (currently hardcoded yellow):
|
||||
Replace `background-color: #fff3cd; color: #856404;` with:
|
||||
`background-color: var(--bs-warning-bg-subtle, #fff3cd); color: var(--bs-warning-text-emphasis, #856404);`
|
||||
|
||||
8. **VueRouteMap audit** — Confirm OPERATOR (`'operator'`) is listed in `allowedUserTypes` for: `BatchAddProducts`, `ManageStoresAdmin`, `ManageProductsAdmin`, `PosAccessKeys`, `PosHistory`, `ListReports`, `AccountingDashboard`, `CooperativeList`, `FarmerProfileEdit`, `VerificationDashboard`, `LandingPageEditor`, `CreateOrganization`.
|
||||
|
||||
## context
|
||||
```
|
||||
// HomeOperator.vue stats currently:
|
||||
const stats = ref([
|
||||
{ title: "Transactions", number: 0, unit: "Total", align: "left", numberId: "total_transactions_no" },
|
||||
{ title: "Value", number: 0, unit: "PHP", align: "left", numberId: "total_transactions_php" },
|
||||
{ title: "Projected", number: 0, unit: "Income", align: "right", numberId: "projected_income_today" },
|
||||
]);
|
||||
|
||||
// HomeOperator.vue services already includes (lines 59-72):
|
||||
// Users, Stores, Products, Transactions, Reports, POS Keys, Market, Accounting, Shipments, Farmers, Verification, Cooperatives
|
||||
|
||||
// HomeOperator.vue template bg-white locations (approximate lines):
|
||||
// ~146: OrgHierarchyExplorer card wrapper
|
||||
// ~172: Cooperative Hub card
|
||||
// ~191: Recent Activity card
|
||||
// ~192: Activity card-header
|
||||
|
||||
// /home-data new stats to add:
|
||||
// 'active_pos_sessions_no', 'managed_stores_no', 'today_revenue_php'
|
||||
|
||||
// POS session model: App\Models\Market\PosSession, status field, store_id FK
|
||||
// GlobalTransaction model: flow field = TransactionFlow::INCOME->value (1), store_id FK
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: `ai-docs/dictionary.md`
|
||||
- linters: none detected
|
||||
- constraints: Dark mode compliance is mandatory — no `bg-white`/`bg-light`/`text-dark`. The corporate header must feel premium (icon + name + role badge). Do not break the existing Cooperative Hub tabs (Overview/Docs/Resolutions) — they are valuable for demo. Stats change is scoped to OPERATOR only via the if-block so other roles are unaffected.
|
||||
104
.claude/plans/badf0c6b720aa1c2804f73f5a6eb090a-complete.md
Normal file
104
.claude/plans/badf0c6b720aa1c2804f73f5a6eb090a-complete.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
task: In ManageStoresAdmin, show Edit Store and Assign Store (to cooperative) action buttons for store_owner and store_manager accounts when they own/manage the store. Currently only the View button renders because user_can_manage is not returning true reliably for these account types.
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T08:19:52Z
|
||||
finished: 2026-05-16T08:20:30Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/ManageStoresAdmin.vue [lines 1-260] — frontend; canModifyStore checks store.user_can_manage; edit/assign-products/delete buttons already exist behind that guard; needs "Assign to Cooperative" button added
|
||||
- app/Http/Controllers/Market/StoreController.php [lines 1297-1353] — listStores_Admin; sets user_can_manage dynamically; non-Big3 branch may silently produce false for store_manager accounts linked only through pivot table
|
||||
- app/Http/Controllers/Support/VueRouteMap.php [lines 137-180] — EditStoreUltimate allowedUserTypes includes 'store owner' but NOT 'store manager'; AddProductsToStore includes both; ManageStoresAdmin includes both
|
||||
- routes/web.php [lines 519-523] — /Admin/Stores/List route
|
||||
|
||||
## steps
|
||||
|
||||
### 1. Fix user_can_manage for store_manager accounts (StoreController.php ~line 1319)
|
||||
The non-Big3 branch fetches managers with `'managers:id,store_id,user_id'` then computes:
|
||||
```php
|
||||
$s->user_can_manage = in_array($s->owner_id, $allowedUserIds)
|
||||
|| in_array($s->manager_id, $allowedUserIds)
|
||||
|| $s->managers->pluck('user_id')->intersect($allowedUserIds)->isNotEmpty();
|
||||
```
|
||||
A store_manager logged in as $user will have $user->id in $allowedUserIds but may only be in the managers pivot (not in manager_id). Verify the `managers` relation is defined on Store model and returns rows correctly. If `$s->managers` is empty due to a missing/wrong relationship, the third condition fails and user_can_manage is false even though the store is shown (it passed the WHERE clause). Fix: ensure the Store model has `managers()` hasMany relationship to store_managers table. If it's missing or uses wrong foreign key, add/fix it.
|
||||
|
||||
### 2. Confirm user_can_manage serializes in JSON response
|
||||
`$s->user_can_manage = value` sets a dynamic Eloquent attribute. Confirm it appears in the JSON response by checking if Store model has a `$visible` whitelist that would block it. If yes, add 'user_can_manage' to $appends or remove the whitelist restriction.
|
||||
|
||||
### 3. Add "Assign to Cooperative" button in ManageStoresAdmin.vue (line ~247)
|
||||
"Assign Store" in context means assigning the store to a cooperative (the Cooperatives column is already displayed in the table). Add a new action button that navigates to EditStoreUltimate (which already has cooperative assignment UI) OR opens a dedicated cooperative-assignment modal. Use `navigate({ page: 'EditStoreUltimate', props: { target: store.hashkey, focus: 'cooperatives' } })` or simply navigate to edit page. Only show if `canModifyStore(store)`. Use icon `fas fa-building` or `fas fa-link` with title "Assign to Cooperative".
|
||||
|
||||
```html
|
||||
<button v-if="canModifyStore(store)"
|
||||
@click="navigate({ page: 'EditStoreUltimate', props: { target: store.hashkey } })"
|
||||
class="btn btn-sm btn-icon btn-outline-warning"
|
||||
title="Assign to Cooperative">
|
||||
<i class="fas fa-building"></i>
|
||||
</button>
|
||||
```
|
||||
|
||||
### 4. Fix EditStoreUltimate allowedUserTypes to include store_manager (VueRouteMap.php line 139)
|
||||
Currently `allowedUserTypes: ['ult', 'super operator', 'operator', 'store owner']` — missing 'store manager'. A store_manager navigating to EditStoreUltimate will be blocked. Add 'store manager' to the allowedUserTypes array. Confirm the EditStoreUltimate backend route/controller also permits store_manager to edit stores they manage.
|
||||
|
||||
### 5. Verify EditStoreUltimate backend allows store_manager
|
||||
Check the backend controller that handles store edits (likely `StoreController@update` or similar). Ensure it doesn't reject store_manager accounts when they own/manage the target store.
|
||||
|
||||
## context
|
||||
|
||||
**canModifyStore (ManageStoresAdmin.vue:25-27):**
|
||||
```js
|
||||
const canModifyStore = (store) => {
|
||||
return !!store.user_can_manage
|
||||
}
|
||||
```
|
||||
|
||||
**listStores_Admin non-Big3 branch (StoreController.php:1316-1335):**
|
||||
```php
|
||||
$allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray());
|
||||
|
||||
$stores = Store::with(['cooperatives:organizations.id,organizations.hashkey,organizations.name', 'managers:id,store_id,user_id'])
|
||||
->select(['id','hashkey','name','category','subcategory','is_active','status','photourl','address','owner_id','manager_id'])
|
||||
->where(function ($q) use ($allowedUserIds) {
|
||||
$q->whereIn('owner_id', $allowedUserIds)
|
||||
->orWhereIn('manager_id', $allowedUserIds)
|
||||
->orWhereHas('managers', function ($mq) use ($allowedUserIds) {
|
||||
$mq->whereIn('user_id', $allowedUserIds);
|
||||
});
|
||||
})
|
||||
->orderBy('id', 'desc')
|
||||
->get()
|
||||
->each(function ($s) use ($allowedUserIds) {
|
||||
$s->user_can_manage = in_array($s->owner_id, $allowedUserIds)
|
||||
|| in_array($s->manager_id, $allowedUserIds)
|
||||
|| $s->managers->pluck('user_id')->intersect($allowedUserIds)->isNotEmpty();
|
||||
unset($s->managers);
|
||||
});
|
||||
```
|
||||
|
||||
**VueRouteMap allowedUserTypes:**
|
||||
- ManageStoresAdmin: `['ult', 'super operator', 'operator', 'store owner', 'store manager']` ✓
|
||||
- EditStoreUltimate: `['ult', 'super operator', 'operator', 'store owner']` ← missing 'store manager'
|
||||
- AddProductsToStore: `['ult', 'super operator', 'operator', 'store owner', 'store manager']` ✓
|
||||
|
||||
**Existing action buttons in template (ManageStoresAdmin.vue:245-256):**
|
||||
```html
|
||||
<button @click="viewStore(store)" class="btn btn-sm btn-icon btn-outline-info" title="View in Market">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button v-if="canModifyStore(store)" @click="navigate({ page: 'AddProductsToStore', props: { target: store.hashkey } })" class="btn btn-sm btn-icon btn-outline-success" title="Assign Products to Store">
|
||||
<i class="fas fa-boxes"></i>
|
||||
</button>
|
||||
<button v-if="canModifyStore(store)" @click="editStore(store)" class="btn btn-sm btn-icon btn-outline-primary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button v-if="canModifyStore(store)" @click="deleteStore(store)" class="btn btn-sm btn-icon btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: phpcs, eslint (check with tsc if TypeScript present — project is mostly JS/Vue/PHP)
|
||||
- constraints: Store model table name is `str` per project dictionary (use in raw queries/validation). Definition-of-done checklist in CLAUDE.md must be verified before closing.
|
||||
173
.claude/plans/c9f6ceb438a078dca529b327a64ffda9-complete.md
Normal file
173
.claude/plans/c9f6ceb438a078dca529b327a64ffda9-complete.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
task: Seed real agricultural demo products, create a named demo store, assign products to the store, and set up a POS access key — all via the performance seeder API so no server-side code changes are needed
|
||||
cycles: 3
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:04:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `app/Http/Controllers/Market/PerformanceController.php` — seeder endpoints already exist; use these instead of writing new code
|
||||
- `routes/api.php` — perf endpoints: `POST /api/perf/seed/stores`, `POST /api/perf/seed/products`, `POST /api/perf/pos/simulate`
|
||||
- `.env` — needs `PERF_API_TOKEN` and `PERF_ACTOR_HASH` set (check if already configured)
|
||||
- `resources/js/Pages/ManageProductsAdmin.vue` — can verify products after seed
|
||||
- `resources/js/Pages/PosMain.vue` — target for POS demo verification
|
||||
|
||||
## steps
|
||||
### Step 0 — Verify perf API is enabled
|
||||
1. Check `.env` for `PERF_API_TOKEN` and `PERF_ACTOR_HASH`. If missing:
|
||||
- Set `PERF_API_TOKEN=bukid-demo-seed-2026` in `.env`
|
||||
- Set `PERF_ACTOR_HASH` to the hashkey of the ULTIMATE user account (query: `SELECT hashkey FROM users WHERE acct_type='ULTIMATE' LIMIT 1` via UltimateConsole or direct DB)
|
||||
2. Test with: `curl -s -H "X-Perf-Token: bukid-demo-seed-2026" http://localhost:9522/api/perf/ping`
|
||||
- Expect `{"ok":true}`. If 403, token mismatch. If connection refused, server is down (task must wait for server).
|
||||
|
||||
### Step 1 — Create a demo store owner account (if the `099` store owner account doesn't own any store yet)
|
||||
- Via the app UI or via Ultimate console SQL: confirm the store owner `099` (mobile) has a store. If not, proceed to create one.
|
||||
|
||||
### Step 2 — Create the demo store via perf API
|
||||
```bash
|
||||
curl -s -X POST http://localhost:9522/api/perf/seed/stores \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Perf-Token: bukid-demo-seed-2026" \
|
||||
-d '{
|
||||
"count": 1,
|
||||
"owner_hash": "<STORE_OWNER_099_HASHKEY>",
|
||||
"category": "Agri-Market",
|
||||
"prefix": "BukidBounty Fresh Market"
|
||||
}'
|
||||
```
|
||||
Note the returned store hashkey as `DEMO_STORE_HASH`.
|
||||
|
||||
### Step 3 — Seed 25 real agricultural products via perf API
|
||||
```bash
|
||||
curl -s -X POST http://localhost:9522/api/perf/seed/products \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Perf-Token: bukid-demo-seed-2026" \
|
||||
-d '{
|
||||
"count": 25,
|
||||
"target_store_hash": "<DEMO_STORE_HASH>",
|
||||
"attach_to_store": true,
|
||||
"prefix": ""
|
||||
}'
|
||||
```
|
||||
|
||||
**However**, the perf seeder creates synthetic product names like "Product-0001". For demo quality, the products need realistic agricultural names. Therefore, instead of the perf seeder for products, **write a standalone Node.js or PHP artisan seed script** that creates the following 25 products directly via the app's own API endpoints (POST `/Products/Admin/New/` and `POST /Products/AssignToStore/`):
|
||||
|
||||
**Product list (agricultural, Bukidnon/Philippines context):**
|
||||
1. Organic White Rice (25kg sack) — ₱1,200 — unit: sack
|
||||
2. Organic Brown Rice (5kg) — ₱280 — unit: kg
|
||||
3. White Corn Grits — ₱85 — unit: kg
|
||||
4. Yellow Corn (fresh ears) — ₱35 — unit: piece
|
||||
5. Fresh Kangkong (Water Spinach) — ₱25 — unit: bundle
|
||||
6. Pechay (Bok Choy) — ₱30 — unit: bundle
|
||||
7. Ampalaya (Bitter Gourd) — ₱60 — unit: kg
|
||||
8. Sitaw (String Beans) — ₱55 — unit: bundle
|
||||
9. Kamote (Sweet Potato) — ₱40 — unit: kg
|
||||
10. Gabi (Taro Root) — ₱50 — unit: kg
|
||||
11. Sayote (Chayote) — ₱45 — unit: kg
|
||||
12. Labanos (Radish) — ₱30 — unit: bundle
|
||||
13. Fresh Tomatoes — ₱70 — unit: kg
|
||||
14. Carabao Mango (green) — ₱90 — unit: kg
|
||||
15. Banana (Latundan) — ₱65 — unit: hand
|
||||
16. Pineapple (Bukidnon) — ₱80 — unit: piece
|
||||
17. Native Chicken (live) — ₱350 — unit: piece
|
||||
18. Free-range Eggs — ₱12 — unit: piece
|
||||
19. Fresh Tilapia — ₱130 — unit: kg
|
||||
20. Bangus (Milkfish) — ₱180 — unit: kg
|
||||
21. Carabao Milk (1L) — ₱120 — unit: liter
|
||||
22. Coconut Oil (Virgin, 1L) — ₱250 — unit: bottle
|
||||
23. Organic Peanuts (roasted) — ₱95 — unit: 250g pack
|
||||
24. Muscovado Sugar — ₱110 — unit: 250g pack
|
||||
25. Fresh Turmeric (Luyang Dilaw) — ₱60 — unit: 100g pack
|
||||
|
||||
**Implementation approach for the seed script:**
|
||||
|
||||
Write a PHP artisan command `php artisan demo:seed-products` in `app/Console/Commands/SeedDemoProducts.php`:
|
||||
- The command should authenticate as the ULTIMATE user (via `Auth::loginUsingId($ultimateUserId)`)
|
||||
- For each product: call `ProductController@createNew_Admin` logic directly (or use Eloquent to create `Product` + attach to `prd_str`)
|
||||
- Use categories: Fresh Produce, Livestock & Poultry, Seafood, Grains & Cereals, Processed Goods
|
||||
- Set `is_active=true`, realistic prices, realistic `available` stock (50–500 units)
|
||||
- After creating each product globally, attach to `DEMO_STORE_HASH` via the prd_str pivot: `Store::find($store->id)->products()->attach($product->id, ['price'=>$price, 'available'=>$stock, 'is_active'=>true])`
|
||||
- Print summary table on completion
|
||||
|
||||
### Step 4 — Set up POS Access Key for the demo store
|
||||
After the store exists, create a POS access key via the UI (`/pos-access-keys`) or directly:
|
||||
```sql
|
||||
INSERT INTO pos_access_keys (hashkey, access_key, terminal_name, store_id, is_active, created_by, updated_by, created_at, updated_at)
|
||||
SELECT
|
||||
CONCAT(UUID(), REPEAT(MD5(RAND()), 3)) as hashkey,
|
||||
CONCAT('PK-DEMO-', UPPER(LEFT(MD5(RAND()), 8))) as access_key,
|
||||
'Demo Counter 1' as terminal_name,
|
||||
(SELECT id FROM str WHERE hashkey = '<DEMO_STORE_HASH>') as store_id,
|
||||
1 as is_active,
|
||||
(SELECT id FROM users WHERE acct_type='ULTIMATE' LIMIT 1) as created_by,
|
||||
(SELECT id FROM users WHERE acct_type='ULTIMATE' LIMIT 1) as updated_by,
|
||||
NOW() as created_at,
|
||||
NOW() as updated_at;
|
||||
```
|
||||
Record the `access_key` value (starts with `PK-`) for use in POS demo.
|
||||
|
||||
### Step 5 — Simulate a POS session to generate demo history
|
||||
```bash
|
||||
curl -s -X POST http://localhost:9522/api/perf/pos/simulate \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Perf-Token: bukid-demo-seed-2026" \
|
||||
-d '{
|
||||
"store_hash": "<DEMO_STORE_HASH>",
|
||||
"items": 5,
|
||||
"cycles": 10,
|
||||
"complete": true
|
||||
}'
|
||||
```
|
||||
This creates 10 completed POS transactions with 5 items each, giving the POS History page real data.
|
||||
|
||||
### Step 6 — Verify via UltimateConsole SQL
|
||||
Run these queries in UltimateConsole to confirm:
|
||||
- `SELECT COUNT(*) FROM prd_items WHERE is_active=1` — expect ≥ 25
|
||||
- `SELECT COUNT(*) FROM prd_str WHERE store_id=(SELECT id FROM str WHERE hashkey='<DEMO_STORE_HASH>')` — expect 25
|
||||
- `SELECT COUNT(*) FROM pos_access_keys WHERE store_id=(SELECT id FROM str WHERE hashkey='<DEMO_STORE_HASH>')` — expect ≥ 1
|
||||
- `SELECT COUNT(*) FROM pos_sessions WHERE store_id=(SELECT id FROM str WHERE hashkey='<DEMO_STORE_HASH>')` — expect ≥ 10
|
||||
|
||||
### Step 7 — Update presentation credentials note
|
||||
Record for Playwright capture script (`/tmp/bukid-presentation/capture.js`):
|
||||
- Store owner login: mobile `099`, password `polomiko32!`
|
||||
- Demo store name: `BukidBounty Fresh Market` (first result)
|
||||
- POS access key: `PK-DEMO-XXXXXXXX` (recorded in Step 4)
|
||||
- Store hashkey: `<DEMO_STORE_HASH>`
|
||||
|
||||
## context
|
||||
```
|
||||
// Perf API endpoints (from PerformanceController.php / routes/api.php):
|
||||
// GET /api/perf/ping — health check
|
||||
// POST /api/perf/seed/stores — { count, owner_hash?, category?, prefix? }
|
||||
// POST /api/perf/seed/products — { count, target_store_hash?, attach_to_store?, prefix? }
|
||||
// POST /api/perf/pos/simulate — { store_hash, items?, cycles?, complete? }
|
||||
// Auth: X-Perf-Token: <PERF_API_TOKEN env>
|
||||
|
||||
// Table names (canonical):
|
||||
// str = stores
|
||||
// prd_items = products
|
||||
// prd_str = product-store pivot (price, available, is_active, description)
|
||||
// pos_access_keys = POS terminal keys (prefix PK- for real terminals)
|
||||
// pos_sessions = POS sessions
|
||||
|
||||
// Product model: App\Models\Market\Product
|
||||
// Store model: App\Models\Market\Store
|
||||
// Product↔Store attach: $store->products()->attach($product->id, ['price'=>..., 'available'=>..., 'is_active'=>true])
|
||||
// ModelSavingListener auto-sets hashkey, created_by, updated_by for Eloquent saves
|
||||
|
||||
// Artisan command skeleton:
|
||||
// php artisan make:command SeedDemoProducts
|
||||
// class SeedDemoProducts extends Command { protected $signature = 'demo:seed-products {store_hash}'; }
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: `ai-docs/dictionary.md`
|
||||
- linters: none — this is a data seeding task, no Vue/PHP linting needed
|
||||
- constraints:
|
||||
- Server must be running before executing curl commands (user confirmed server is currently down — this task runs later)
|
||||
- The artisan command approach is preferred over direct SQL inserts for products because ModelSavingListener auto-populates hashkey/created_by/updated_by
|
||||
- For pos_access_keys and SQL steps, manually include all required fields (created_by, updated_by, created_at, updated_at, hashkey) since raw SQL bypasses Eloquent events
|
||||
- Perf seeder prefix names are synthetic ("BukidBounty Fresh Market-001") — use the prefix param to set the store name close to desired then rename via UI if needed
|
||||
- Do not truncate existing data before seeding; additive only
|
||||
98
.claude/plans/d42061a3fa5b1ccc8fd9cb99ecc7ea58-complete.md
Normal file
98
.claude/plans/d42061a3fa5b1ccc8fd9cb99ecc7ea58-complete.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
task: Fix broken layout on /add-products-to-store--h:<storeHash> — products render outside their cards, sometimes don't load, and the card boxes appear huge but empty.
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-17T00:00:00Z
|
||||
finished: 2026-05-17T00:00:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/AddProductsToStore.vue [lines 202-393, 395-459] — page template + scoped CSS for `.product-pick-card`, `.product-thumb`, `.sticky-bottom-bar`. Product grid uses `.row g-2` with `col-12 col-sm-6 col-lg-4`. Step 1 picker is where boxes look huge/empty.
|
||||
- resources/js/Components/Core/CardSimple.vue [lines 37-47, 79-97] — wrapper used 3× on this page. Has `height: 100%; display: flex; flex-direction: column;` + `.is-premium` (default true) adds white translucent background + hover `transform: translateY(-2px)`. Hover transform on the surrounding card (when product list is *inside* it) makes the whole grid jitter.
|
||||
- resources/js/Components/Core/FileImage.vue [lines 1-38] — emits `<img>` directly. Page wraps it in `.product-thumb` with `:deep(img) { width: 100%; height: 100%; object-fit: cover }`. If `photourl` is `null`/missing, `p.photourl && p.photourl[0]` passes empty string → FileImage falls back. But if `photourl` is a *string* (not array), `p.photourl[0]` returns the first character — produces a 1-char src that 404s, briefly showing a blank box before `@error` swaps in the fallback. This is the "sometimes does not load" symptom.
|
||||
- resources/js/Pages/BatchAddProducts.vue — sister page; reference for known-good layout patterns (cross-check spacing, container, image handling).
|
||||
- ai-docs/dictionary.md — read first per repo CLAUDE.md. Update with any new findings about `FileImage` array-vs-string handling and `CardSimple` hover side-effects.
|
||||
|
||||
## steps
|
||||
1. Read `ai-docs/dictionary.md` first (repo rule). Then open `resources/js/Pages/AddProductsToStore.vue` and reproduce mentally: the picker grid at lines 258–286 is **not** wrapped in `CardSimple`, so it sits directly inside `.tf-container`. Confirm by inspecting structure.
|
||||
2. **Image robustness (root of "sometimes doesn't load" + tiny/broken thumbs):** In `AddProductsToStore.vue` lines 264 and 349, the `:src` binding is `p.photourl && p.photourl[0] ? p.photourl[0] : ''`. Replace both occurrences with a safe accessor:
|
||||
```js
|
||||
:src="Array.isArray(p.photourl) ? (p.photourl[0] || '') : (p.photourl || '')"
|
||||
```
|
||||
Add a small helper `const firstPhoto = (v) => Array.isArray(v) ? (v[0] || '') : (v || '');` in `<script setup>` and use `firstPhoto(p.photourl)` / `firstPhoto(r.photourl)` in the template.
|
||||
3. **Thumb sizing (the "huge empty box"):** The `.product-thumb` is 56×56 and `.product-thumb-sm` is 40×40 — these are fine in isolation, but `.product-pick-card` uses `d-flex gap-3 align-items-center` with `.flex-grow-1 min-w-0` *and* a `form-check` on the right. On narrow viewports (`col-12`), if `p.name` is long and `text-truncate` is bypassed by Bootstrap's flex defaults, the row grows tall. Audit:
|
||||
- Ensure `.product-pick-card` has `min-height` set (e.g. `min-height: 84px;`) so cards have a consistent floor and don't appear "empty huge" while image loads.
|
||||
- Add `align-items: center; min-width: 0;` to the inner `.d-flex` wrapper (Bootstrap already does this, but verify).
|
||||
- Constrain the card body so the thumb cannot stretch: ensure `.product-thumb` keeps `flex-shrink: 0` (already set) and confirm it isn't being stretched by `align-items: stretch` from any parent.
|
||||
4. **Row equal-height (cards "huge but empty" when description is short):** Bootstrap `.row.g-2` makes cols equal-height by default (`align-items: stretch`). Each col contains a single `.product-pick-card` that fills the column. When ONE card in a row has a long name (wraps to 2 lines) the others stretch to match — those look "empty huge". Fix by either:
|
||||
- Adding `align-self: start` to `.product-pick-card`, OR
|
||||
- Adding `.row.g-2 { align-items: flex-start; }` scoped to the page (preferred — keeps cards as tall as their own content).
|
||||
5. **Products outside the box:** Inspect `.tf-container` (global class). If it provides `max-width` + `margin: 0 auto`, the picker grid should sit inside it. Confirm the grid is INSIDE the `<div class="tf-container mt-4">` block — currently it is. But the `.sticky-bottom-bar` at line 288 uses `position: sticky; bottom: 12px;` with its own background — on small screens the sticky bar can overlap product cards if the parent has `overflow: hidden`. Check `CardSimple` — `.card-custom` has `overflow: hidden` (line 42). The sticky bar is OUTSIDE CardSimple so that's fine, but verify by browsing the rendered page that no card has `overflow: hidden` cropping the checkbox/title.
|
||||
6. **CardSimple hover transform causing visual "popping out":** `.is-premium:hover { transform: translateY(-2px) }` (CardSimple line 94-97) applies to the *page-level* CardSimples (target store header, search bar). When the user mouses over them they lift, which can look like products jumping out of the box. Either pass `:is-premium="false"` on the search-bar/header CardSimples on this page (they shouldn't lift), or scope the hover lift to a class that's only added when used as a clickable card.
|
||||
7. **Empty state height:** Lines 254-257 — `text-center py-5 text-muted` for the empty state. Ensure `py-5` is enough that the empty state doesn't collapse into a thin strip after the search input. Likely fine; verify visually.
|
||||
8. **Loading state:** Line 251-253 — `text-center py-5` for the spinner. Same — fine.
|
||||
9. **Theming compliance (per CLAUDE.md):** No `bg-white`, `bg-light`, or `text-dark` in this file — confirmed clean. Do not introduce them.
|
||||
10. **Sticky-top header offset:** Page does not use `sticky-top` on a child header — N/A.
|
||||
11. **VueRouteMap:** Verify `AddProductsToStore` is registered in `app/Http/Controllers/Support/VueRouteMap.php` with appropriate `allowedUserTypes`. If missing (the URL `/add-products-to-store--h:…` is reachable, so likely present), no change. Otherwise add it in the same commit.
|
||||
12. After edits, run `npm run build` (or `npm run dev` if Vite is hot-reloading) and visually verify in browser at `/add-products-to-store--h:<a real store hash>`. Confirm: (a) cards are uniform, hugging their content; (b) thumbnails always render (fallback kicks in when photourl is empty/string); (c) nothing overflows the page container; (d) sticky bottom bar stays inside the container.
|
||||
13. Update `ai-docs/dictionary.md` with: (a) note that `FileImage` expects `string|array|null` for `src` and pages must pass via a safe accessor — never `arr[0]` directly on an unknown shape; (b) note that `CardSimple` is `is-premium` by default and lifts on hover — pass `:is-premium="false"` for non-interactive panels.
|
||||
|
||||
## context
|
||||
|
||||
### AddProductsToStore.vue — product card (Step 1 picker), lines 258-286
|
||||
```html
|
||||
<div v-else class="row g-2">
|
||||
<div v-for="p in filteredProducts" :key="p.hashkey" class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="product-pick-card" :class="{ picked: selected[p.hashkey] }"
|
||||
@click="toggleProduct(p.hashkey)">
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<div class="product-thumb">
|
||||
<FileImage :src="p.photourl && p.photourl[0] ? p.photourl[0] : ''"
|
||||
class="img-fluid rounded" alt="Product"
|
||||
fallback="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/146710fe9ece.bin" />
|
||||
</div>
|
||||
<div class="flex-grow-1 min-w-0"> … name / category / price … </div>
|
||||
<div class="form-check"><input type="checkbox" … /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### AddProductsToStore.vue — scoped CSS, lines 395-459 (key blocks)
|
||||
```css
|
||||
.product-pick-card { border: 1px solid …; border-radius: 12px; padding: 12px; cursor: pointer; }
|
||||
.product-thumb { width: 56px; height: 56px; flex-shrink: 0; overflow: hidden; border-radius: 8px; }
|
||||
.product-thumb :deep(img) { width: 100%; height: 100%; object-fit: cover; }
|
||||
.sticky-bottom-bar { position: sticky; bottom: 12px; … border-radius: 14px; z-index: 5; }
|
||||
```
|
||||
|
||||
### FileImage.vue — relevant logic, lines 14-33
|
||||
```js
|
||||
const processedSrc = computed(() => {
|
||||
if (!props.src) return props.fallback;
|
||||
if (Array.isArray(props.src)) return props.src[0]; // safe array handling here
|
||||
if (props.src.startsWith("http") || props.src.startsWith("/") || props.src.startsWith("data:")) return props.src;
|
||||
if (props.src.length > 20 && !props.src.includes(".") && !props.src.includes("/")) return `/RequestData/File/${props.src}`;
|
||||
return props.src;
|
||||
});
|
||||
```
|
||||
Key insight: `FileImage` itself already handles arrays. The bug is the *caller* unsafely doing `p.photourl[0]` — if `photourl` is a string, this yields one character.
|
||||
|
||||
### CardSimple.vue — hover transform, lines 80-97
|
||||
```css
|
||||
.is-premium { background: rgba(255,255,255,0.6); backdrop-filter: blur(12px); border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 8px 32px 0 rgba(31,38,135,0.07); }
|
||||
.is-premium:hover { border-color: var(--accent-color, rgba(83,61,234,0.3)); transform: translateY(-2px); }
|
||||
```
|
||||
`is-premium` defaults to `true`. Non-interactive panels (target store header, search bar) shouldn't lift.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md — MUST read first and update after (repo CLAUDE.md mandate).
|
||||
- linters: eslint (via npm), no phpcs, tsc, pylint relevant for this Vue file. Run `npm run build` or `npm run lint` if available.
|
||||
- constraints:
|
||||
- Repo CLAUDE.md: no `bg-white`/`bg-light`/`text-dark` hardcoded. Theme via CSS vars. This file is currently compliant — keep it so.
|
||||
- Vue page edits don't require touching `routes/web.php`, `UserActions`, `UserPermissions`, or `VueRouteMap` unless registration is missing.
|
||||
- Do NOT modify legacy Blade. All work is in `.vue` files + scoped CSS.
|
||||
- Test direct URL access (reload at `/add-products-to-store--h:<hash>`) after fix.
|
||||
- Keep edits minimal — fix the layout regressions, do not refactor the component.
|
||||
73
.claude/plans/d478a19b3dade245b1c97f86422285c3-complete.md
Normal file
73
.claude/plans/d478a19b3dade245b1c97f86422285c3-complete.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
task: Add print button for POS scan code and sales statistics (sold today + sold to date) on BuyViewProductMarket page
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T19:01:29Z
|
||||
finished: 2026-05-16T19:02:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `resources/js/Pages/BuyViewProductMarket.vue` [lines 159-167] — existing POS QR section; add print button + statistics display
|
||||
- `app/Http/Controllers/Market/ProductController.php` [lines 223-360] — `viewProductDetails` static method; add `sold_today` and `store_sold_today` to `$data` array
|
||||
|
||||
## steps
|
||||
1. In `ProductController.php`, add `use App\Models\Market\ProductTransaction;` to the import block (after line 14) if not already present.
|
||||
2. In `ProductController.php` inside `viewProductDetails`, after line 290 (`$storeProductSold = $storeProduct->pivot->sold;`), add:
|
||||
```php
|
||||
$storeSoldToday = ProductTransaction::where('product_id', $product->id)
|
||||
->where('store_id', $targetStore->id)
|
||||
->whereDate('created_at', today())
|
||||
->sum('quantity');
|
||||
```
|
||||
3. After the `if ($targetStore)` block (before line 312 `$data = [`), add a global sold_today query:
|
||||
```php
|
||||
$soldToday = ProductTransaction::where('product_id', $product->id)
|
||||
->whereDate('created_at', today())
|
||||
->sum('quantity');
|
||||
```
|
||||
4. In `$data` array (lines 312-338), add after `'sold' => $product->sold,`:
|
||||
```php
|
||||
'sold_today' => (int) $soldToday,
|
||||
'store_sold_today' => isset($storeSoldToday) ? (int) $storeSoldToday : null,
|
||||
```
|
||||
5. In `BuyViewProductMarket.vue`, add a `printPosCode()` function in `<script setup>` that opens a new window rendering the QR image + product name + barcode value formatted for printing, then calls `window.print()` and closes.
|
||||
6. In `BuyViewProductMarket.vue`, replace the existing `pos-qr-section` block (lines 160-167) with an enhanced version that:
|
||||
- Keeps the existing QR image
|
||||
- Adds a "Print" button (`fas fa-print`) below the QR image (visible to `['ult','superoperator','operator']` roles OR always — per user preference; default: always visible)
|
||||
- Adds a stats row below showing: `Sold Today: X` and `Total Sold: Y` using `product.sold_today`, `product.store_sold`, `product.sold`, `product.store_sold_today` — prefer store-specific values when `product.is_from_store` is true
|
||||
7. Use theme variables (`var(--bg-card)`, `var(--text-primary)`) for the new stats section; no hardcoded `bg-white`/`text-dark`.
|
||||
|
||||
## context
|
||||
### Backend — ProductController.php
|
||||
- `viewProductDetails` static method at line 223; receives `$target` (hashkey) and optional `$withStores` flag
|
||||
- `$targetStore` is set only when a store hash was in request (`$req->input('data.store_hash')`)
|
||||
- Pivot `sold` already extracted: `$storeProductSold = $storeProduct->pivot->sold;` (line 290)
|
||||
- Returned in `$data`: `'store_sold' => $storeProductSold` (line 326), `'sold' => $product->sold` (line 327)
|
||||
- `ProductTransaction` model: table `prd_trx`, fields `product_id`, `store_id`, `quantity` (int), `created_at`; model at `app/Models/Market/ProductTransaction.php`
|
||||
- Response format: `ResponseHelper::returnSuccessResponse($data, $data['hashkey'])` → `{ success: true, data: {...} }`
|
||||
|
||||
### Frontend — BuyViewProductMarket.vue
|
||||
- `product` is a computed ref from `productStore.currentProduct` (line 26)
|
||||
- Existing QR section (line 160-167): `v-if="product.pos_qrcode"`, shows QR via `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=...`
|
||||
- `pos_qrcode` value is `md5(product_hashkey + store_hashkey)` for store products, or `barcode` or `hashkey` for standalone products
|
||||
- `role` from `useAuth()` — values: `'ult'`, `'superoperator'`, `'operator'`, `'customer'`, etc.
|
||||
- Print function pattern — open new window with minimal HTML:
|
||||
```js
|
||||
const printPosCode = () => {
|
||||
const w = window.open('', '_blank', 'width=400,height=500');
|
||||
w.document.write(`<html><body style="text-align:center;font-family:sans-serif">
|
||||
<h3>${product.value.name}</h3>
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(product.value.pos_qrcode)}" />
|
||||
<p style="font-size:12px">${product.value.pos_qrcode}</p>
|
||||
<script>window.onload=()=>{window.print();window.close();}<\/script>
|
||||
</body></html>`);
|
||||
w.document.close();
|
||||
};
|
||||
```
|
||||
- Stats display: show `store_sold_today ?? sold_today` for "Today" and `store_sold ?? sold` for "Total" when `is_from_store` is true; otherwise use global values
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: eslint, tsc
|
||||
- constraints: Use canonical table name `prd_trx` in any raw SQL (not needed here — using Eloquent). No `bg-white`/`text-dark` hardcoded. No new routes needed — extends existing `/View/Product/Details/data` response. Definition-of-done checklist: no new page/route, so only theming + audit-field rules apply.
|
||||
66
.claude/plans/d649faffd424aace60ec46a1fde5ff83-complete.md
Normal file
66
.claude/plans/d649faffd424aace60ec46a1fde5ff83-complete.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
task: Fix the bulk-apply "Apply" buttons on AddProductsToStore Step 2 (mobile layout broken)
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T17:49:31Z
|
||||
finished: 2026-05-16T17:49:35Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/AddProductsToStore.vue [lines 303-332] — Step 2 bulk-apply card with broken input-group layout
|
||||
|
||||
## steps
|
||||
1. In `AddProductsToStore.vue` at lines ~308-313 (price bulk-apply), replace `<div class="input-group">` with `<div class="d-flex gap-2 align-items-center">` and change the button class from `btn btn-primary rounded-end-pill` to `btn btn-primary rounded-pill flex-shrink-0`
|
||||
2. In `AddProductsToStore.vue` at lines ~319-324 (availability bulk-apply), apply the identical replacement — `input-group` → `d-flex gap-2 align-items-center`, button class → `btn btn-primary rounded-pill flex-shrink-0`
|
||||
|
||||
## context
|
||||
Bulk apply card (lines 303-332):
|
||||
```html
|
||||
<CardSimple class="mb-3">
|
||||
<div class="fw_6 mb-3">Bulk apply</div>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-sm-5">
|
||||
<label class="form-label smallest text-muted mb-1">Set all prices (₱)</label>
|
||||
<div class="input-group"> <!-- CHANGE THIS -->
|
||||
<input v-model="bulkPrice" type="number" min="0" step="0.01" class="form-control"
|
||||
placeholder="e.g. 50" />
|
||||
<button class="btn btn-primary rounded-end-pill" @click="applyBulkPrice"> <!-- AND THIS -->
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-5">
|
||||
<label class="form-label smallest text-muted mb-1">Set all availability</label>
|
||||
<div class="input-group"> <!-- CHANGE THIS -->
|
||||
<input v-model="bulkAvailable" type="number" min="0" step="1" class="form-control"
|
||||
placeholder="e.g. 100" />
|
||||
<button class="btn btn-primary rounded-end-pill" @click="applyBulkAvailable"> <!-- AND THIS -->
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-2 d-flex align-items-end">
|
||||
<button class="btn btn-outline-secondary rounded-pill w-100" @click="backToPick">
|
||||
<i class="fas fa-arrow-left me-1"></i> Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardSimple>
|
||||
```
|
||||
|
||||
Target (apply to both price and availability rows):
|
||||
```html
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<input v-model="bulkPrice" type="number" min="0" step="0.01" class="form-control"
|
||||
placeholder="e.g. 50" />
|
||||
<button class="btn btn-primary rounded-pill flex-shrink-0" @click="applyBulkPrice">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none detected
|
||||
- constraints: mobile-first fix only — no backend changes, no new files, no route changes
|
||||
112
.claude/plans/db3f8841a2ce6fbe5c0279ccba883ec9-complete.md
Normal file
112
.claude/plans/db3f8841a2ce6fbe5c0279ccba883ec9-complete.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
task: Redesign HomeCooperative.vue into a proper Coordinator/Cooperative-user dashboard and wire it into Home.vue
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T00:00:00Z
|
||||
finished: 2026-05-16T00:01:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- `resources/js/Pages/Home.vue` [lines 57-58] — COORDINATOR currently maps to `HomeShared`; must import and use `HomeCooperative` instead
|
||||
- `resources/js/Pages/Fragments/Home/HomeCooperative.vue` — full rewrite: currently only a chapter/geo-map component, needs full dashboard
|
||||
- `resources/js/Pages/Fragments/Home/HomeStoreOwner.vue` — reference pattern: BalanceBox + ServiceButtonGrid + HomeSkeleton
|
||||
- `resources/js/Components/Core/Stats/BalanceBox.vue` — stat card component (props: `stats[]`, `footerItems[]`, emits `footer-click`)
|
||||
- `resources/js/Components/Core/Services/ServiceButtonGrid.vue` — icon grid (prop: `items[]` with `icon`, `title`, `pagename` or `action`)
|
||||
- `resources/js/Components/Core/Services/SideTextButtonList.vue` — text list of quick actions
|
||||
- `resources/js/Components/Core/Skeleton/HomeSkeleton.vue` — loading skeleton
|
||||
- `resources/js/composables/usePageData.js` — `fetchPageData(url, payload)` pattern
|
||||
- `routes/web.php` [lines 76-135] — `/home-data` GET endpoint; must add COORDINATOR-scoped cooperative stats
|
||||
- `app/Http/Controllers/Market/CooperativeController.php` — available methods: list, show, registerMember, publicRegisterMember
|
||||
|
||||
## steps
|
||||
1. **`routes/web.php` @ `/home-data` closure** — After the existing `$myStoresCount` block (line ~114), add a COORDINATOR-specific stats block:
|
||||
- Check `$acctType === UserTypes::COORDINATOR`
|
||||
- Query `Organization::where('type','COOPERATIVE')->count()` → `cooperative_total_no`
|
||||
- Query `CooperativeMember::whereHas('organization', fn($q)=>$q->where('created_by',$user->id))->count()` → `cooperative_members_no`
|
||||
- Query `Organization::where('type','COOPERATIVE')->where('created_by',$user->id)->withCount('members')->count()` → `my_cooperatives_no`
|
||||
- Add `pending_members_no`: count of `cooperative_members` with `created_at >= now()->subDays(7)` under the coordinator's cooperatives
|
||||
- Include all four keys in `$props['props']['stats']` unconditionally (0 for non-coordinators is fine)
|
||||
|
||||
2. **`resources/js/Pages/Home.vue`** — Add import at top: `import HomeCooperative from './Fragments/Home/HomeCooperative.vue';`
|
||||
— Change line 57-58 from `<HomeShared title="Coordinator" />` to `<HomeCooperative />`
|
||||
|
||||
3. **`resources/js/Pages/Fragments/Home/HomeCooperative.vue`** — Full rewrite. Preserve the chapter map as a collapsible section at the bottom. New structure:
|
||||
|
||||
**Script:**
|
||||
- Imports: `ref, onMounted, computed, h`, `usePageData`, `useNavigate`, `useAuth`, `useModal`, `BalanceBox`, `ServiceButtonGrid`, `SideTextButtonList`, `HomeSkeleton`, and the existing `useChapters` (keep for map section)
|
||||
- Stats ref: `[{ title:'Cooperatives', number:0, unit:'Total', numberId:'cooperative_total_no' }, { title:'Members', number:0, unit:'Enrolled', numberId:'cooperative_members_no' }, { title:'New (7d)', number:0, unit:'Members', numberId:'pending_members_no' }]`
|
||||
- Footer items: `[{ title:'Cooperatives', icon:'<people icon url>', pagename:'CooperativeList' }, { title:'My Profile', icon:'<profile icon url>', pagename:'UserInfoEdit' }]`
|
||||
- `services` array: 8 items:
|
||||
1. `{ icon:'<people svg>', title:'Cooperatives', pagename:'CooperativeList' }`
|
||||
2. `{ icon:'<person-plus svg>', title:'Register Member', pagename:'CooperativeMemberRegister' }` (needs active coop hash — use `action:'registerMember'` to prompt)
|
||||
3. `{ icon:'<building svg>', title:'My Cooperative', action:'viewMyCoop' }`
|
||||
4. `{ icon:'<bar-chart svg>', title:'Reports', pagename:'ListReports' }`
|
||||
5. `{ icon:'<file-text svg>', title:'Documents', action:'viewDocs' }`
|
||||
6. `{ icon:'<check-square svg>', title:'Resolutions', action:'viewResolutions' }`
|
||||
7. `{ icon:'<people svg>', title:'Members', action:'viewMembers' }`
|
||||
8. `{ icon:'<map svg>', title:'Chapter Map', action:'toggleMap' }`
|
||||
- `quickActions` list items:
|
||||
- `{ text:'Create Cooperative', pagename:'CreateCooperative', icon:'...' }`
|
||||
- `{ text:'Member Ledger', pagename:'AccountingDashboard', icon:'...' }`
|
||||
- `{ text:'Add Transaction', pagename:'AddTransaction', icon:'...' }`
|
||||
- `{ text:'My Personal Profile', pagename:'UserInfoEdit', icon:'...' }`
|
||||
- `activeOrgHash`: `computed(() => user.value?.settings?.cooperatives?.[0] ?? null)`
|
||||
- `showMap`: `ref(false)` — toggled by `toggleMap` action
|
||||
- `viewMyCoop()`: if `activeOrgHash.value`, navigate to `CooperativeDetail` with `target: activeOrgHash.value`; else `modal.quickDismiss({title:'No Cooperative', body:'You have not joined a cooperative yet.'})`
|
||||
- `handleItemClick(item)`: dispatch on `item.action` or `navigate({ page: item.pagename })`
|
||||
- On `onMounted`: `fetchPageData('/home-data', {})` then `applyStats()`
|
||||
|
||||
**Template:**
|
||||
- `<HomeSkeleton v-if="loading" />`
|
||||
- BalanceBox with cooperative stats and footer items, `@footer-click="handleItemClick"`
|
||||
- `<h5>Cooperative Services</h5>` + `<ServiceButtonGrid :items="services" @item-click="handleItemClick" />`
|
||||
- `<h5>Quick Actions</h5>` + `<SideTextButtonList :items="quickActions" @item-click="handleItemClick" />`
|
||||
- Collapsible map section: `<button @click="showMap=!showMap">Chapter Map</button>` + `<div v-show="showMap">` containing the existing Leaflet map code (preserve all existing refs and lifecycle hooks, but only `initMap()` when `showMap` becomes true via `watch`)
|
||||
|
||||
**Style:** No `bg-white`/`bg-light`/`text-dark`. Use `var(--bg-card)`, `var(--text-primary)`. `:global(.dark-mode) .chapter-row { background: var(--bg-card) !important; }`.
|
||||
|
||||
4. **Icon URLs** — Reuse CDN URLs already in the project. Map to Font Awesome alternatives in the `icon` field or pick from existing `/assets/micons/` SVGs already used in `HomeUltimate.vue`. Acceptable: use `fas fa-*` class names as `icon` if `ServiceButtonGrid` supports icon class strings — check the component prop; if it only accepts URLs, use the same CDN icon URLs already referenced in `HomeShared.vue` and `HomeStoreOwner.vue` (people: `a/04d0e432a298.bin`, profile: `a/ac7a1cebe580.bin`, chart: `a/f87407046b18.bin`, docs: `a/c9fd442fe676.bin`).
|
||||
|
||||
5. **VueRouteMap check** — Confirm `CooperativeList`, `CooperativeDetail`, `CreateCooperative`, `CooperativeMemberRegister` all have `coordinator` in `allowedUserTypes`. If any are missing, add `'coordinator'` to their arrays in `app/Http/Controllers/Support/VueRouteMap.php`.
|
||||
|
||||
6. **UserPermissions check** — Confirm `UserTypes::COORDINATOR` has `ViewCooperatives`, `CreateCooperative`, `JoinCooperative`, `ViewAccountingReports` in `UserPermissions::roles()`. Add any missing ones.
|
||||
|
||||
## context
|
||||
```
|
||||
// routes/web.php ~line 92
|
||||
$acctType = $user?->acct_type instanceof \App\Enums\UserTypes
|
||||
? $user->acct_type
|
||||
: \App\Enums\UserTypes::tryFrom($user?->acct_type ?? '');
|
||||
// stats keys currently returned: total_users_no, active_stores_no, pending_orders_no,
|
||||
// total_balance_php, total_transactions_no, total_transactions_php, projected_income_today,
|
||||
// transactions_today_no, cash_flow_today_php, my_stores_no
|
||||
|
||||
// BalanceBox props:
|
||||
// stats: Array<{ title, number, unit, align?, numberId? }>
|
||||
// footerItems: Array<{ title, icon?, pagename?, action? }>
|
||||
// boxStyle: string (CSS)
|
||||
|
||||
// ServiceButtonGrid: prop items[] = [{ icon: URL|string, title, pagename?, action? }]
|
||||
// SideTextButtonList: prop items[] = [{ text, pagename?, action?, icon? }]
|
||||
// usePageData: { data, loading, error, fetchPageData(url, payload) }
|
||||
// applyStats pattern from HomeStoreOwner.vue:
|
||||
// stats.value = stats.value.map(s => {
|
||||
// const v = data.value?.stats[s.numberId];
|
||||
// return v !== undefined ? { ...s, number: v } : s;
|
||||
// });
|
||||
|
||||
// Home.vue line 57-58 currently:
|
||||
// <template v-else-if="isCoordinator">
|
||||
// <HomeShared title="Coordinator" />
|
||||
// </template>
|
||||
|
||||
// Organization model: type='COOPERATIVE', created_by=user.id
|
||||
// cooperative_members table: organization_id, user_id, created_at
|
||||
// active coop hash from: user.value?.settings?.cooperatives[0]
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: `ai-docs/dictionary.md`
|
||||
- linters: none detected
|
||||
- constraints: No `bg-white`/`bg-light`/`text-dark` hardcoding; use theme CSS vars. Leaflet map is lazy (only init when `showMap=true`). All icon URLs must use CDN pattern already established in the project. COORDINATOR must appear in `allowedUserTypes` for coop pages.
|
||||
59
.claude/plans/e2b7c6e2fbf042840b482563f920724d-complete.md
Normal file
59
.claude/plans/e2b7c6e2fbf042840b482563f920724d-complete.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
task: Fix public marketplace /list-products-market showing 0 products for unauthenticated users — POST /Market/Products/List has auth middleware but the page is public
|
||||
cycles: 3
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T16:02:42Z
|
||||
finished: 2026-05-16T16:03:15Z
|
||||
---
|
||||
|
||||
## files
|
||||
- routes/web.php [line ~412] — `POST /Market/Products/List` has `'middleware' => 'auth'` but the page is designed to be public
|
||||
- app/Http/Controllers/Market/ProductController.php [lines 561-607] — `listProductsData()` already has guest-safe fallback logic but it's never reached due to auth middleware
|
||||
- app/Http/Controllers/Support/VueRouteMap.php [line ~58] — `/list-products-market` has `loginRequired: false` (correctly public)
|
||||
|
||||
## steps
|
||||
1. In `routes/web.php`, on the `POST /Market/Products/List` route, remove `'middleware' => 'auth'` (or change it to optional auth that attaches user if present but doesn't block guests).
|
||||
- The controller already handles the guest case: `else { $products = Product::where('is_active', true)->get(); }` on line ~605.
|
||||
- With auth middleware removed, `Auth::user()` returns null for guests → `$UltOpsSupOps = false` → falls through to the `else` branch showing all active products. This is the correct behaviour.
|
||||
2. No changes needed to `ProductController::listProductsData()` — it already handles null user correctly.
|
||||
3. Verify: after removing auth middleware, visiting `/list-products-market` without login shows the full active product catalog.
|
||||
|
||||
## context
|
||||
Current state (confirmed by Playwright):
|
||||
- `/list-products-market` loads (loginRequired: false ✓)
|
||||
- Page calls `POST /Market/Products/List` → 401 Unauthenticated (auth middleware blocks guest)
|
||||
- Page shows "0 Products — No products found" for guests even though products exist
|
||||
|
||||
Controller fallback code (line ~602-607) already written for guests:
|
||||
```php
|
||||
if ($targetStore) {
|
||||
$products = $targetStore->products()->where('prd_str.is_active', true)->get();
|
||||
} elseif ($UltOpsSupOps) {
|
||||
$products = Product::select([...])->get();
|
||||
} else {
|
||||
// THIS runs for guests (no store, no big-3 role) — but currently unreachable due to auth middleware
|
||||
$products = Product::where('is_active', true)->get();
|
||||
}
|
||||
```
|
||||
|
||||
Route fix (web.php):
|
||||
```php
|
||||
// BEFORE (blocks guests):
|
||||
Route::post('/Market/Products/List', [
|
||||
'as' => 'products.list.data',
|
||||
'uses' => ProductController::class . '@listProductsData',
|
||||
'middleware' => 'auth',
|
||||
]);
|
||||
|
||||
// AFTER (allows guests):
|
||||
Route::post('/Market/Products/List', [
|
||||
'as' => 'products.list.data',
|
||||
'uses' => ProductController::class . '@listProductsData',
|
||||
]);
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md (products table = `prd_items`, product-store pivot = `prd_str`)
|
||||
- linters: none
|
||||
- constraints: The controller's `Auth::user()` returns null gracefully for guests in Hypervel — no additional null checks needed.
|
||||
50
.claude/plans/e45c0c127d270347cb4763699642f6af-complete.md
Normal file
50
.claude/plans/e45c0c127d270347cb4763699642f6af-complete.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
task: In ListProductsMarket.vue, hide the "+ New Product" button for all non-Big-3 account types — currently v-if="isUltimate", should be v-if="isUltimate || isSuperOperator || isOperator"
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-28T16:46:18Z
|
||||
finished: 2026-05-28T16:46:23Z
|
||||
---
|
||||
|
||||
## files
|
||||
- resources/js/Pages/ListProductsMarket.vue [lines 18-72] — contains the useAuth destructure (line 19) and the button v-if guard (line 69)
|
||||
- resources/js/composables/Core/useAuth.js [lines 102-124] — exports isUltimate, isSuperOperator, isOperator
|
||||
|
||||
## steps
|
||||
1. In `resources/js/Pages/ListProductsMarket.vue` line 19, change:
|
||||
`const { isUltimate } = useAuth();`
|
||||
to:
|
||||
`const { isUltimate, isSuperOperator, isOperator } = useAuth();`
|
||||
2. On line 69 of the same file, change:
|
||||
`v-if="isUltimate"`
|
||||
to:
|
||||
`v-if="isUltimate || isSuperOperator || isOperator"`
|
||||
3. Run `npm run build` to rebuild frontend assets.
|
||||
4. Run `docker restart bukidbountyapp` to apply the new build.
|
||||
|
||||
## context
|
||||
```
|
||||
// resources/js/Pages/ListProductsMarket.vue
|
||||
// line 19 (current)
|
||||
const { isUltimate } = useAuth();
|
||||
|
||||
// line 69 (current)
|
||||
<button v-if="isUltimate" @click="navigate({ page: 'CreateProductUltimate' })"
|
||||
class="btn btn-sm btn-primary rounded-pill px-3 py-1">
|
||||
<i class="fas fa-plus me-1"></i> New Product
|
||||
</button>
|
||||
|
||||
// resources/js/composables/Core/useAuth.js
|
||||
// lines 102-124
|
||||
const isUltimate = computed(() => role.value === UserTypes.ULTIMATE);
|
||||
const isSuperOperator = computed(() => role.value === UserTypes.SUPER_OPERATOR);
|
||||
const isOperator = computed(() => role.value === UserTypes.OPERATOR);
|
||||
// ...
|
||||
return { isUltimate, isSuperOperator, isOperator, ... };
|
||||
```
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none
|
||||
- constraints: Big 3 = ULTIMATE, SUPER_OPERATOR, OPERATOR per dictionary. STORE_OWNER and below must NOT see the button. No backend changes required — frontend-only visibility guard.
|
||||
469
.claude/plans/e8e0be1ce348a4e47641f59cdf136890-complete.md
Normal file
469
.claude/plans/e8e0be1ce348a4e47641f59cdf136890-complete.md
Normal file
@@ -0,0 +1,469 @@
|
||||
---
|
||||
task: Cooperative officer/member homepages, chapter org chart, member search, create user, assign officer to child chapter, create chapter, share registration link
|
||||
cycles: 5
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-29T00:00:00Z
|
||||
finished: 2026-05-29T00:01:00Z
|
||||
---
|
||||
|
||||
## prerequisite
|
||||
This plan DEPENDS on plan `4fc3b455fb62b15dfb06790a1f421c96` (COOP_MEMBER/COOP_OFFICER types + migrations). Execute that plan first. Assumes the following already exist after that plan runs:
|
||||
- `users.acct_type` values `'coop officer'` and `'coop member'` (UserTypes enum)
|
||||
- `chapters.cooperative_id` column + `chapters.level` enum includes `'municipal'`
|
||||
- `chapter_members.role` column
|
||||
- `UserActions::ViewChapterOrgChart`, `ManageChapterMembers`, `ViewScopedMemberReports`, `AssignChapterOfficer`
|
||||
- `UserTypes::COOP_OFFICER` and `UserTypes::COOP_MEMBER` in UserPermissions::roles()
|
||||
- `isCoopOfficer` and `isCoopMember` in `useAuth.js`
|
||||
- `Home.vue` fragments wired for COOP_OFFICER and COOP_MEMBER
|
||||
|
||||
## files
|
||||
- `app/Http/Controllers/Support/ChapterController.php` — add orgChart(), officerAssign(), createChapter(), memberSearch() methods
|
||||
- `app/Http/Controllers/Market/CooperativeController.php` [line 410] — extend publicRegisterMember to accept chapter_hash; auto-assign to chapter
|
||||
- `routes/web.php` [lines 761-770] — register new chapter endpoints + public chapter route
|
||||
- `app/Http/Controllers/Support/VueRouteMap.php` [lines 278-311] — register new SPA pages; add coop officer/member to existing coop allowedUserTypes
|
||||
- `resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue` — NEW: level-aware officer dashboard
|
||||
- `resources/js/Pages/Fragments/Home/HomeCoopMember.vue` — NEW: read-only member card
|
||||
- `resources/js/Pages/ChapterOrgChart.vue` — NEW: org chart with child chapter toggle
|
||||
- `resources/js/Pages/CoopMemberSearch.vue` — NEW: name-only member search for officers
|
||||
- `resources/js/Pages/CreateCoopUser.vue` — NEW: full user creation with coop+chapter auto-assign
|
||||
- `resources/js/Pages/AssignChapterOfficer.vue` — NEW: move member to child chapter as officer
|
||||
- `resources/js/Pages/CreateChapter.vue` — NEW: create chapter one level below current officer's chapter
|
||||
- `resources/js/composables/useChapters.js` — add fetchOfficerScope(), fetchOrgChart(), searchMembers(), assignOfficer(), createChapter()
|
||||
- `routes/web.php` [line ~754] — add public chapter info + registration endpoints
|
||||
|
||||
## level hierarchy constants
|
||||
```
|
||||
LEVEL_ORDER = ['national','region','province','city','municipal','barangay']
|
||||
CHILD_LEVELS = {
|
||||
'national' => ['region'],
|
||||
'region' => ['province'],
|
||||
'province' => ['city','municipal'],
|
||||
'city' => ['barangay'],
|
||||
'municipal' => ['barangay'],
|
||||
'barangay' => [],
|
||||
}
|
||||
```
|
||||
All PHP code using level must use `in_array($level, ['city','municipal'])` never equality check.
|
||||
|
||||
## steps
|
||||
|
||||
### 1. ChapterController — getOrgChart()
|
||||
Add `public function getOrgChart(Request $request)` to `ChapterController.php`:
|
||||
- Auth: caller must be logged in
|
||||
- Determine caller's chapter: `ChapterMember::with('chapter')->where('user_id', Auth::id())->where('is_active', true)->orderByRaw("FIELD(chapters.level, 'region','province','city','municipal','barangay')")->join('chapters','chapters.id','chapter_members.chapter_id')->first()`
|
||||
- For COOP_OFFICER: load own chapter + its officers (chapter_members WHERE role IS NOT NULL AND role != 'MEMBER') + direct child chapters (WHERE parent_id = own chapter id AND cooperative_id = own cooperative_id) each with `active_members_count` and their own officers (collapsed by default)
|
||||
- For COOP_MEMBER: load own chapter name + officers only (names + roles) — do NOT return member list
|
||||
- For Big3/COORDINATOR: accept optional `cooperative_id` param, return full coop chapter tree
|
||||
- Response shape:
|
||||
```json
|
||||
{
|
||||
"own_chapter": { "id", "hashkey", "name", "level", "location_key", "officers": [{"name","role","position"}] },
|
||||
"children": [{ "id", "hashkey", "name", "level", "member_count": 42, "officers": [] }]
|
||||
}
|
||||
```
|
||||
Children `officers` array starts empty (populated by frontend on toggle via second call to same endpoint with `chapter_id` param pointing to that child).
|
||||
|
||||
### 2. ChapterController — assignOfficer()
|
||||
Add `public function assignOfficer(Request $request)`:
|
||||
- Validate: `member_user_hashkey` (string, required), `child_chapter_id` (integer, required), `role` (string, required: PRESIDENT/VICE_PRESIDENT/SECRETARY/TREASURER/AUDITOR/BOARD_MEMBER)
|
||||
- Permission check: caller must be COOP_OFFICER (or Big3/COORDINATOR)
|
||||
- Eligibility check A — the selected user: must have a chapter_members row in caller's OWN chapter (`is_active = true`), AND role IS NULL OR role = 'MEMBER' (not already an officer there)
|
||||
- Eligibility check B — child_chapter_id: must have `parent_id = caller's own chapter id` AND `cooperative_id = caller's cooperative_id`
|
||||
- Effect:
|
||||
1. Delete the member's existing chapter_members row in the parent chapter
|
||||
2. Insert/update chapter_members row: `{user_id, chapter_id: child_chapter_id, role, position: role label, is_manual_override: true, assigned_by: Auth::id(), is_active: true, created_by, updated_by, hashkey}`
|
||||
3. Upgrade `users.acct_type = 'coop officer'` if currently `'coop member'`
|
||||
- Return `{ success: true, message: '...' }`
|
||||
|
||||
### 3. ChapterController — createChapter()
|
||||
Add `public function createChapter(Request $request)`:
|
||||
- Validate: `name` (string, required), `location_key` (string, required), `lat` (nullable decimal), `lng` (nullable decimal)
|
||||
- Permission check: caller must be COOP_OFFICER (or Big3/COORDINATOR)
|
||||
- Determine caller's chapter level → compute allowed child level using CHILD_LEVELS map
|
||||
- If caller is at BARANGAY level, return 422 "Cannot create sub-chapters at barangay level"
|
||||
- Determine caller's cooperative_id from their chapter's cooperative_id
|
||||
- Create Chapter: `{ name, level: childLevel, parent_id: callerChapterId, cooperative_id: callerCoopId, location_key: strtolower(trim()), lat, lng, is_active: true, created_by, updated_by, hashkey }`
|
||||
- Return `{ success: true, chapter: { hashkey, name, level } }`
|
||||
|
||||
### 4. ChapterController — memberSearch()
|
||||
Add `public function memberSearch(Request $request)`:
|
||||
- Validate: `query` (string, required, min 2)
|
||||
- Permission check: caller must have `ManageChapterMembers` action (COOP_OFFICER, COORDINATOR, Big3)
|
||||
- Scope: collect all chapter IDs in caller's subtree (own chapter + all descendants recursively via `parent_id` chain, scoped to same `cooperative_id`)
|
||||
- Query: `User::join('chapter_members','users.id','chapter_members.user_id')->whereIn('chapter_members.chapter_id', $scopeIds)->where('chapter_members.is_active', true)->where('users.name', 'LIKE', "%{$query}%")->select('users.name', 'chapter_members.role', 'chapters.name as chapter_name')->join('chapters','chapters.id','chapter_members.chapter_id')->distinct()->limit(30)->get()`
|
||||
- Return ONLY: `[{ name, role, chapter_name }]` — NO hashkey, NO mobile_number, NO username, NO other fields
|
||||
|
||||
### 5. ChapterController — getOfficerScope()
|
||||
Add `public function getOfficerScope(Request $request)`:
|
||||
- Returns caller's own chapter info + list of direct child chapters (for dropdowns in AssignChapterOfficer and CreateChapter pages)
|
||||
- Response: `{ own_chapter: {id, hashkey, name, level}, child_chapters: [{id, hashkey, name, level, active_members_count}], cooperative: {hashkey, name}, eligible_members: [{user_hashkey, name, role}] }`
|
||||
- `eligible_members`: chapter_members WHERE chapter_id = own chapter AND is_active = true AND (role IS NULL OR role = 'MEMBER') → join users → return {user_hashkey: users.hashkey, name: users.name OR users.fullname, role}
|
||||
|
||||
### 6. Public chapter registration endpoint
|
||||
In `routes/web.php` add after existing public coop routes (~line 755):
|
||||
```php
|
||||
Route::get('/api/public/chapter/{hkey}', [\App\Http\Controllers\Support\ChapterController::class, 'publicGetChapter']);
|
||||
Route::post('/api/public/chapter/register', [\App\Http\Controllers\Support\ChapterController::class, 'publicRegisterToChapter']);
|
||||
```
|
||||
|
||||
Add `publicGetChapter(Request $request, string $hkey)` to ChapterController:
|
||||
- No auth required
|
||||
- Return chapter name, level, cooperative name — nothing else
|
||||
- Response: `{ success: true, chapter: { name, level }, cooperative: { name } }`
|
||||
|
||||
Add `publicRegisterToChapter(Request $request)` to ChapterController:
|
||||
- No auth required
|
||||
- Params: `chapter_hash` (string), `name`, `username`, `mobile_number`, `password`
|
||||
- Validate same as publicRegisterMember
|
||||
- Load chapter by hashkey; load cooperative from chapter.cooperative_id
|
||||
- Create user: acct_type = 'coop member', parentuid = chapter.created_by (fallback: first COORDINATOR)
|
||||
- Insert cooperative_members row linking user to cooperative
|
||||
- Insert chapter_members row: `{user_id, chapter_id: chapter.id, is_manual_override: false, is_active: true, created_by: user.id, updated_by: user.id, hashkey}`
|
||||
- Update user.settings.cooperatives = [cooperative.hashkey]
|
||||
- Return `{ success: true, user_hashkey, message }`
|
||||
|
||||
### 7. routes/web.php — register new chapter endpoints
|
||||
In the Chapter routes block (lines 761–770), add:
|
||||
```php
|
||||
Route::post('/Chapters/OrgChart', [\App\Http\Controllers\Support\ChapterController::class, 'getOrgChart'], ['middleware' => 'auth']);
|
||||
Route::post('/Chapters/Officer/Assign', [\App\Http\Controllers\Support\ChapterController::class, 'assignOfficer'], ['middleware' => 'auth']);
|
||||
Route::post('/Chapters/Create', [\App\Http\Controllers\Support\ChapterController::class, 'createChapter'], ['middleware' => 'auth']);
|
||||
Route::post('/Chapters/Members/Search', [\App\Http\Controllers\Support\ChapterController::class, 'memberSearch'], ['middleware' => 'auth']);
|
||||
Route::post('/Chapters/Officer/Scope', [\App\Http\Controllers\Support\ChapterController::class, 'getOfficerScope'], ['middleware' => 'auth']);
|
||||
```
|
||||
|
||||
### 8. home-data — COOP_OFFICER and COOP_MEMBER branches
|
||||
In the `/home-data` closure in `routes/web.php` (after the COORDINATOR block, ~line 153):
|
||||
```php
|
||||
if ($acctType === \App\Enums\UserTypes::COOP_OFFICER && $user) {
|
||||
$myChapterMember = \App\Models\ChapterMember::join('chapters','chapters.id','chapter_members.chapter_id')
|
||||
->where('chapter_members.user_id', $user->id)->where('chapter_members.is_active', true)
|
||||
->orderByRaw("FIELD(chapters.level,'region','province','city','municipal','barangay')")
|
||||
->select('chapter_members.*','chapters.name as chapter_name','chapters.level as chapter_level','chapters.hashkey as chapter_hashkey','chapters.cooperative_id')
|
||||
->first();
|
||||
if ($myChapterMember) {
|
||||
$myChapterId = $myChapterMember->chapter_id;
|
||||
$chapterMemberCount = \App\Models\ChapterMember::where('chapter_id', $myChapterId)->where('is_active', true)->count();
|
||||
$childChapterCount = \App\Models\Chapter::where('parent_id', $myChapterId)->where('is_active', true)->count();
|
||||
$newMembersCount = \App\Models\ChapterMember::where('chapter_id', $myChapterId)->where('is_active', true)->where('created_at', '>=', now()->subDays(7))->count();
|
||||
$cooperativeHash = \App\Models\Market\Organization::where('id', $myChapterMember->cooperative_id)->value('hashkey');
|
||||
$props['props']['chapter_info'] = [
|
||||
'chapter_name' => $myChapterMember->chapter_name,
|
||||
'chapter_level' => $myChapterMember->chapter_level,
|
||||
'chapter_hashkey' => $myChapterMember->chapter_hashkey,
|
||||
'cooperative_hash' => $cooperativeHash,
|
||||
];
|
||||
$props['props']['stats']['chapter_member_count'] = $chapterMemberCount;
|
||||
$props['props']['stats']['child_chapter_count'] = $childChapterCount;
|
||||
$props['props']['stats']['new_members_7d'] = $newMembersCount;
|
||||
}
|
||||
}
|
||||
if ($acctType === \App\Enums\UserTypes::COOP_MEMBER && $user) {
|
||||
$myChapterMember = \App\Models\ChapterMember::join('chapters','chapters.id','chapter_members.chapter_id')
|
||||
->where('chapter_members.user_id', $user->id)->where('chapter_members.is_active', true)
|
||||
->orderByRaw("FIELD(chapters.level,'barangay','city','municipal','province','region','national')")
|
||||
->select('chapter_members.*','chapters.name as chapter_name','chapters.level as chapter_level','chapters.id as chapter_id','chapters.cooperative_id')
|
||||
->first();
|
||||
if ($myChapterMember) {
|
||||
$chapterMemberCount = \App\Models\ChapterMember::where('chapter_id', $myChapterMember->chapter_id)->where('is_active', true)->count();
|
||||
$cooperativeName = \App\Models\Market\Organization::where('id', $myChapterMember->cooperative_id)->value('name');
|
||||
$props['props']['chapter_info'] = [
|
||||
'chapter_name' => $myChapterMember->chapter_name,
|
||||
'chapter_level' => $myChapterMember->chapter_level,
|
||||
'cooperative_name' => $cooperativeName,
|
||||
'member_count' => $chapterMemberCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. VueRouteMap.php — register new pages + update existing allowedUserTypes
|
||||
Add to the routes array (near the cooperatives block, ~line 308):
|
||||
```php
|
||||
'/chapter-org-chart' => [
|
||||
'component' => 'ChapterOrgChart',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer','coop member'],
|
||||
'module' => 'cooperatives',
|
||||
],
|
||||
'/coop-member-search' => [
|
||||
'component' => 'CoopMemberSearch',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
|
||||
'module' => 'cooperatives',
|
||||
],
|
||||
'/create-coop-user' => [
|
||||
'component' => 'CreateCoopUser',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
|
||||
'module' => 'cooperatives',
|
||||
],
|
||||
'/assign-chapter-officer' => [
|
||||
'component' => 'AssignChapterOfficer',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
|
||||
'module' => 'cooperatives',
|
||||
],
|
||||
'/create-chapter' => [
|
||||
'component' => 'CreateChapter',
|
||||
'loginRequired' => true,
|
||||
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
|
||||
'module' => 'cooperatives',
|
||||
],
|
||||
'/register-chapter' => [
|
||||
'component' => 'RegisterChapter',
|
||||
'loginRequired' => false,
|
||||
'allowedUserTypes'=> ['*'],
|
||||
'module' => 'cooperatives',
|
||||
],
|
||||
```
|
||||
Also update existing `/cooperative-list`, `/cooperative-detail`, `/cooperative-member-register` entries to include `'coop officer'` and `'coop member'` in their `allowedUserTypes`.
|
||||
|
||||
### 10. useChapters.js — add new methods
|
||||
Append to the `useChapters()` composable return object:
|
||||
```js
|
||||
const fetchOrgChart = async ({ chapterId = null } = {}) => { /* POST /Chapters/OrgChart */ }
|
||||
const fetchOfficerScope = async () => { /* POST /Chapters/Officer/Scope */ }
|
||||
const searchMembers = async (query) => { /* POST /Chapters/Members/Search */ }
|
||||
const assignOfficer = async ({ memberUserHashkey, childChapterId, role }) => { /* POST /Chapters/Officer/Assign */ }
|
||||
const createChapter = async ({ name, locationKey, lat = null, lng = null }) => { /* POST /Chapters/Create */ }
|
||||
```
|
||||
Follow same loading/error pattern as existing methods.
|
||||
|
||||
### 11. HomeCoopOfficer.vue — NEW
|
||||
`resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue`
|
||||
|
||||
Pattern: mirror `HomeCooperative.vue` structure (imports: usePageData, useNavigate, useAuth, useModal, BalanceBox, ServiceButtonGrid, SideTextButtonList, HomeSkeleton).
|
||||
|
||||
`onMounted`: call `fetchPageData('/home-data')`. Extract `data.value?.chapter_info` for chapter name/level/hashkey/cooperative_hash. Extract `data.value?.stats` for member count, child chapters, new members 7d.
|
||||
|
||||
Stats cards:
|
||||
```js
|
||||
[
|
||||
{ title: 'Members', number: chapter_member_count, unit: 'In Chapter' },
|
||||
{ title: 'Sub-Chapters', number: child_chapter_count, unit: 'Direct' },
|
||||
{ title: 'New (7d)', number: new_members_7d, unit: 'Members' },
|
||||
]
|
||||
```
|
||||
|
||||
Chapter badge: show chapter level (capitalize) + chapter name. e.g. "REGIONAL — Bukidnon Chapter".
|
||||
|
||||
Services grid (icon: use FontAwesome `fas fa-*`, NOT micons for new components):
|
||||
```js
|
||||
[
|
||||
{ icon: 'fas fa-sitemap', title: 'Org Chart', pagename: 'ChapterOrgChart' },
|
||||
{ icon: 'fas fa-search', title: 'Search Members', pagename: 'CoopMemberSearch' },
|
||||
{ icon: 'fas fa-user-plus', title: 'Create User', pagename: 'CreateCoopUser' },
|
||||
{ icon: 'fas fa-user-tie', title: 'Assign Officer', pagename: 'AssignChapterOfficer' },
|
||||
{ icon: 'fas fa-map-marker-alt', title: 'Create Chapter', pagename: 'CreateChapter' },
|
||||
{ icon: 'fas fa-share-alt', title: 'Share Invite', action: 'shareChapterLink' },
|
||||
]
|
||||
```
|
||||
|
||||
SideTextButtonList:
|
||||
```js
|
||||
[
|
||||
{ text: 'My Cooperative', action: 'viewMyCoop' },
|
||||
{ text: 'My Profile', pagename: 'UserInfoEdit' },
|
||||
{ text: 'Member Ledger', pagename: 'AccountingDashboard' },
|
||||
]
|
||||
```
|
||||
|
||||
`shareChapterLink()` handler:
|
||||
```js
|
||||
const shareChapterLink = () => {
|
||||
const coopHash = chapterInfo.value?.cooperative_hash;
|
||||
const chapterHash = chapterInfo.value?.chapter_hashkey;
|
||||
if (!coopHash || !chapterHash) return;
|
||||
const encoded = btoa(JSON.stringify({ coop_hash: coopHash, chapter_hash: chapterHash }));
|
||||
const url = `${window.location.origin}/register-chapter--e:${encoded}`;
|
||||
if (navigator.share) {
|
||||
navigator.share({ title: 'Join our cooperative chapter', url }).catch(() => {});
|
||||
} else {
|
||||
navigator.clipboard?.writeText(url);
|
||||
modal.quickDismiss({ title: 'Link Copied', body: 'Registration link copied to clipboard.' });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
No `bg-white`, `bg-light`, or `text-dark` hardcoded. Use `var(--bg-card)`, `var(--text-primary)`. Sticky header: `top: 66px; z-index: 1020;` if used.
|
||||
|
||||
### 12. HomeCoopMember.vue — NEW
|
||||
`resources/js/Pages/Fragments/Home/HomeCoopMember.vue`
|
||||
|
||||
Fetch `/home-data`. Extract `chapter_info` (chapter_name, chapter_level, cooperative_name, member_count).
|
||||
|
||||
Display:
|
||||
- Chapter name card: chapter_level (capitalized) + chapter_name + cooperative_name
|
||||
- Member count badge: "X members in this chapter" — count only, NO names list
|
||||
- Org chart strip: fetch `/Chapters/OrgChart` (own chapter's officers) → display name + role, e.g. "Pres. Juan dela Cruz". Do NOT display member names.
|
||||
|
||||
Services grid:
|
||||
```js
|
||||
[
|
||||
{ icon: 'fas fa-sitemap', title: 'Org Chart', pagename: 'ChapterOrgChart' },
|
||||
{ icon: 'fas fa-handshake', title: 'My Cooperative', action: 'viewMyCoop' },
|
||||
{ icon: 'fas fa-user-circle', title: 'My Profile', pagename: 'UserInfoEdit' },
|
||||
{ icon: 'fas fa-wallet', title: 'My Wallet', pagename: 'MyWallet' },
|
||||
]
|
||||
```
|
||||
No search button, no create user, no assign officer.
|
||||
|
||||
### 13. ChapterOrgChart.vue — NEW
|
||||
Route: `/chapter-org-chart` (no hash needed — scoped server-side to caller).
|
||||
|
||||
`onMounted`: call `POST /Chapters/OrgChart` → get `{ own_chapter, children }`.
|
||||
|
||||
Render:
|
||||
1. **Header card**: own_chapter.name + level badge
|
||||
2. **Officers section** (own chapter): flat list of `officer.name + officer.role` badges. If no officers: "No officers assigned yet."
|
||||
3. **Child chapters list**: for each child in `children`:
|
||||
- Row: `[level badge] Chapter Name ——— X members [▶ toggle icon]`
|
||||
- Toggle (`showOfficers[child.id]` ref): on click, if `child.officers` is empty, call `POST /Chapters/OrgChart` with `{ chapter_id: child.id }` to fetch and populate `child.officers`. Show officer names + roles below the row.
|
||||
4. COOP_MEMBER: show only own_chapter.name + officer list. No children drill-down. No member names anywhere.
|
||||
|
||||
No `bg-white`/`bg-light`. Use theme variables.
|
||||
|
||||
### 14. CoopMemberSearch.vue — NEW
|
||||
Route: `/coop-member-search`
|
||||
|
||||
Simple search page:
|
||||
- Input: text field with `v-model="query"`, debounced 400ms, min 2 chars
|
||||
- On each debounced change: call `POST /Chapters/Members/Search { query }` → `[{ name, role, chapter_name }]`
|
||||
- Results: list of cards showing `name`, `chapter_name`, role badge (if officer). NO mobile, NO hashkey link, NO other fields.
|
||||
- Empty state if < 2 chars: "Type at least 2 characters to search."
|
||||
- No results: "No members found matching '{{query}}'."
|
||||
|
||||
### 15. CreateCoopUser.vue — NEW
|
||||
Route: `/create-coop-user`
|
||||
|
||||
`onMounted`: call `POST /Chapters/Officer/Scope` to get `{ own_chapter, cooperative, cooperative_list (if multiple) }`.
|
||||
If officer has multiple cooperatives → show cooperative picker dropdown first.
|
||||
|
||||
Form fields (full user creation):
|
||||
- `name` (display name), `username`, `mobile_number` (format 09XXXXXXXXX), `password`, `fullname`, `firstname`, `middlename`, `lastname` (optional extras for UserInfo)
|
||||
- Real-time duplicate check via existing `/admin/check/mobile` and `/admin/check/username` endpoints
|
||||
- Chapter assignment: auto-set to officer's own chapter (read-only display: "Will be added to: [chapter_name]")
|
||||
- Cooperative: auto-set (read-only if single, picker if multiple)
|
||||
|
||||
On submit: `POST /api/public/chapter/register` with `{ chapter_hash, name, username, mobile_number, password }`. On success show confirmation.
|
||||
|
||||
Cooperative picker: use `user.value?.settings?.cooperatives` array → resolve names via `POST /Cooperatives/Get` for each hashkey → show select modal. Selected cooperative must have a chapter that matches.
|
||||
|
||||
### 16. AssignChapterOfficer.vue — NEW
|
||||
Route: `/assign-chapter-officer`
|
||||
|
||||
`onMounted`: call `POST /Chapters/Officer/Scope` → get `eligible_members` (members of own chapter, not yet officers) + `child_chapters`.
|
||||
|
||||
Step 1 — pick member: searchable list of `eligible_members` (name only). Tap to select.
|
||||
Step 2 — pick child chapter: list of `child_chapters` from scope. Tap to select.
|
||||
Step 3 — pick role: dropdown `[PRESIDENT, VICE_PRESIDENT, SECRETARY, TREASURER, AUDITOR, BOARD_MEMBER]`
|
||||
Step 4 — confirm: "Assign [name] as [role] to [child_chapter_name]? This will MOVE them from [own_chapter_name] to [child_chapter_name]."
|
||||
On confirm: `POST /Chapters/Officer/Assign { member_user_hashkey, child_chapter_id, role }`.
|
||||
On success: navigate back to home.
|
||||
|
||||
### 17. CreateChapter.vue — NEW
|
||||
Route: `/create-chapter`
|
||||
|
||||
`onMounted`: call `POST /Chapters/Officer/Scope` → get own_chapter (name, level). Compute `childLevel` from CHILD_LEVELS map. If own_chapter.level === 'barangay' → show error "Cannot create sub-chapters at barangay level."
|
||||
|
||||
Form:
|
||||
- `name` (string, required)
|
||||
- `location_key` (auto-lowercase from name, editable)
|
||||
- `lat`, `lng` (optional)
|
||||
- Read-only: "Level: [childLevel]", "Parent: [own_chapter.name]", "Cooperative: [cooperative.name]"
|
||||
|
||||
On submit: `POST /Chapters/Create { name, location_key, lat, lng }`.
|
||||
On success: show confirmation with new chapter name + level badge.
|
||||
|
||||
### 18. RegisterChapter.vue — NEW (public page)
|
||||
Route: `/register-chapter--e:<encoded>` (no auth required — `loginRequired: false`)
|
||||
|
||||
`onMounted`:
|
||||
1. Decode `target` prop (base64 JSON: `{ coop_hash, chapter_hash }`)
|
||||
2. Call `GET /api/public/chapter/{chapter_hash}` → show chapter name, level, cooperative name
|
||||
|
||||
Form:
|
||||
- `name`, `username`, `mobile_number`, `password`
|
||||
- Real-time duplicate checks via existing public endpoints
|
||||
- On submit: `POST /api/public/chapter/register { chapter_hash, name, username, mobile_number, password }`
|
||||
- On success: show "Welcome to [cooperative name] — [chapter name]! Your account has been created. Please login." with Login button.
|
||||
|
||||
No auth required. No sidebar/header — full-page public layout (mirror `RegisterCoop.vue` structure).
|
||||
|
||||
## context
|
||||
|
||||
### home-data closure location (routes/web.php lines 101-199)
|
||||
COORDINATOR branch at line 147. Insert COOP_OFFICER and COOP_MEMBER blocks after line 153 (after end of coordinator if block).
|
||||
`$acctType` already resolved at line 117. Use `\App\Enums\UserTypes::COOP_OFFICER` and `\App\Enums\UserTypes::COOP_MEMBER`.
|
||||
|
||||
### ChapterController existing methods (lines 27, 75, 108, 141, 194)
|
||||
- `hierarchy()` line 27 — GET chapters by parent
|
||||
- `mapData()` line 75 — lat/lng dots
|
||||
- `members()` line 108 — members for chapter_id (returns full user info — DO NOT reuse for search, too much data)
|
||||
- `assignMember()` line 141 — generic assign (does NOT move, does NOT upgrade acct_type — new `assignOfficer()` must handle move + upgrade)
|
||||
- `removeMember()` line 194 — remove by hashkey
|
||||
|
||||
### CooperativeController::publicRegisterMember (lines 410-460)
|
||||
Creates user with `acct_type = 'user'`, no chapter assignment. The new `publicRegisterToChapter` in ChapterController is the chapter-aware equivalent.
|
||||
|
||||
### useChapters.js existing methods (lines 8-127)
|
||||
Returns: fetchHierarchy, fetchMapData, fetchOrgHierarchy, fetchOrgMapData, fetchMembers, assignMember, removeMember, fetchPositions, syncAutoAssignments.
|
||||
ADD: fetchOrgChart (POST /Chapters/OrgChart), fetchOfficerScope (POST /Chapters/Officer/Scope), searchMembers (POST /Chapters/Members/Search), assignOfficer (POST /Chapters/Officer/Assign), createChapter (POST /Chapters/Create).
|
||||
|
||||
### RegisterCoop.vue (existing public page — mirror for RegisterChapter.vue)
|
||||
File: `resources/js/Pages/RegisterCoop.vue`. Route: `/register-coop--h:HASHKEY`. No auth. Calls `GET /api/public/cooperative/{hkey}` then `POST /api/public/cooperative/register`. Mirror this structure for RegisterChapter.vue.
|
||||
|
||||
### Share link encoding
|
||||
Payload: `btoa(JSON.stringify({ coop_hash, chapter_hash }))`. Frontend decodes with `JSON.parse(atob(target))` where `target` is the prop injected by VueRouteMap from the `--e:` URL segment. This follows the existing encoded payload pattern already used in VueRouteMap.
|
||||
|
||||
### Web Share API pattern (already in CooperativeDetail.vue)
|
||||
```js
|
||||
if (navigator.share) {
|
||||
navigator.share({ title, url }).catch(() => {});
|
||||
} else {
|
||||
navigator.clipboard?.writeText(url);
|
||||
// show copied notice
|
||||
}
|
||||
```
|
||||
|
||||
### LEVEL_ORDER for PHP childLevel lookup
|
||||
```php
|
||||
const CHILD_LEVELS = [
|
||||
'national' => ['region'],
|
||||
'region' => ['province'],
|
||||
'province' => ['city', 'municipal'],
|
||||
'city' => ['barangay'],
|
||||
'municipal' => ['barangay'],
|
||||
'barangay' => [],
|
||||
];
|
||||
```
|
||||
Helper: `$childLevels = CHILD_LEVELS[$chapter->level] ?? []; if (empty($childLevels)) { return 422; }`
|
||||
|
||||
### Hypervel migration imports (for any new migration if needed)
|
||||
```php
|
||||
use Hyperf\Database\Schema\Blueprint;
|
||||
use Hypervel\Database\Migrations\Migration;
|
||||
use Hypervel\Support\Facades\Schema;
|
||||
```
|
||||
NO Illuminate classes.
|
||||
|
||||
### UserActions in $RoleswithNoTargetUser
|
||||
`ViewChapterOrgChart`, `ManageChapterMembers`, `ViewScopedMemberReports`, `AssignChapterOfficer` must be in `UserPermissions::$RoleswithNoTargetUser` (added by prerequisite plan). Verify before adding routes.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none detected
|
||||
- constraints:
|
||||
- PREREQUISITE: execute plan 4fc3b455fb62b15dfb06790a1f421c96 first (adds COOP_OFFICER/COOP_MEMBER types, migrations, permissions, Home.vue fragments)
|
||||
- All chapter level checks use in_array(level, ['city','municipal']) not equality — city and municipal are peers
|
||||
- memberSearch NEVER returns mobile_number, hashkey, username or any field other than name/role/chapter_name
|
||||
- HomeCoopMember NEVER shows fellow member names — member count only; officer names ARE shown in org chart strip
|
||||
- assignOfficer MOVES the member (delete parent row, insert child row) — not a copy
|
||||
- createChapter links to officer's cooperative via cooperative_id — chapters are coop-scoped
|
||||
- share link uses encoded payload (btoa JSON) not hashkey — decode in RegisterChapter.vue onMounted
|
||||
- No bg-white, bg-light, text-dark in any new Vue component — use var(--bg-card), var(--text-primary)
|
||||
- Sticky headers must use top: 66px; z-index: 1020
|
||||
- Raw DB queries must include created_by/updated_by (Auth::id()) and timestamps (now())
|
||||
- All new routes use Hypervel-style Route::post(..., ..., ['middleware' => 'auth'])
|
||||
62
.claude/plans/fffa63a98d16f82e93074ce23d9adbf0-complete.md
Normal file
62
.claude/plans/fffa63a98d16f82e93074ce23d9adbf0-complete.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
task: Fix GET /api/public/landing-page returning HTTP 500 — wrong response helper + pending migration
|
||||
cycles: 3
|
||||
context: true
|
||||
private: false
|
||||
started: 2026-05-16T15:59:26Z
|
||||
finished: 2026-05-16T16:00:00Z
|
||||
---
|
||||
|
||||
## files
|
||||
- app/Http/Controllers/Admin/LandingPageController.php [lines 241-262] — uses `response()->json()` instead of `Response::json()` facade; also the only public (no-auth) method in the controller
|
||||
- database/migrations/2026_04_11_000001_create_landing_pages_table.php — migration may not have been run on production; if `landing_pages` table is absent the query throws and returns 500
|
||||
- routes/web.php [line 689] — `Route::get('/api/public/landing-page', ...)` — no middleware, correctly public
|
||||
|
||||
## steps
|
||||
1. In `LandingPageController::getActiveLandingPage()` replace both `response()->json([...])` calls with `Response::json([...])` using the Hypervel facade (`use Hypervel\Support\Facades\Response;` is already imported at the top of the file — add it if missing).
|
||||
2. Add a `try/catch` around `LandingPage::getActive()` so a missing table or DB error returns a graceful `{"success":false,"message":"..."}` 500 instead of the bare Server Error page, until the migration is confirmed run.
|
||||
3. Confirm (or trigger via Ultimate Console → Migrate button, or `php artisan migrate --force` in the container) that the `landing_pages` migration has been applied on production.
|
||||
|
||||
## context
|
||||
```php
|
||||
// BROKEN — uses Laravel global helper response() which may not exist / behave differently in Hypervel:
|
||||
public function getActiveLandingPage()
|
||||
{
|
||||
$page = LandingPage::getActive();
|
||||
if (!$page) {
|
||||
return response()->json([...]); // <-- wrong helper
|
||||
}
|
||||
return response()->json([...]); // <-- wrong helper
|
||||
}
|
||||
|
||||
// FIX — use Hypervel facade:
|
||||
use Hypervel\Support\Facades\Response;
|
||||
|
||||
public function getActiveLandingPage()
|
||||
{
|
||||
try {
|
||||
$page = LandingPage::getActive();
|
||||
} catch (\Throwable $e) {
|
||||
return Response::json(['success' => false, 'data' => null, 'has_landing_page' => false]);
|
||||
}
|
||||
|
||||
if (!$page) {
|
||||
return Response::json(['success' => true, 'data' => null, 'has_landing_page' => false]);
|
||||
}
|
||||
|
||||
return Response::json([
|
||||
'success' => true,
|
||||
'data' => ['title' => $page->title, 'html_content' => $page->html_content, 'description' => $page->description],
|
||||
'has_landing_page' => true,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Impact: `HomePublic.vue` fetches this endpoint on load to show the landing page to guest users. A 500 response causes the guest homepage to render empty/blank. Also breaks the cooperative public registration flow that depends on the public API layer being healthy.
|
||||
|
||||
Route: `GET /api/public/landing-page` — no middleware, no auth required.
|
||||
|
||||
## notes
|
||||
- dictionary: ai-docs/dictionary.md
|
||||
- linters: none
|
||||
- constraints: Use `Response::json()` facade (not `response()->json()`). Use Hypervel migration imports (`Hyperf\Database\Schema\Blueprint`, `Hypervel\Database\Migrations\Migration`, `Hypervel\Support\Facades\Schema`) if adding a new migration.
|
||||
37
.claude/sessions/20260516-000000-BukidBountyApp-7tasks.md
Normal file
37
.claude/sessions/20260516-000000-BukidBountyApp-7tasks.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
date: 2026-05-16T00:00:00Z
|
||||
repo: BukidBountyApp
|
||||
branch: test
|
||||
total: 7
|
||||
completed: 7
|
||||
failed: 0
|
||||
commit: fix POS store resolution, store owner/manager access, and UI improvements
|
||||
hash: 223adf8102802df7b0ac2f603d5fe76a417047e9
|
||||
---
|
||||
|
||||
## plans
|
||||
| file | task | status | cycles |
|
||||
|------|------|--------|--------|
|
||||
| 0a9389d04151b0f575326efa633467fd | Fix POS Main not showing products assigned to store; update dictionary | complete | 1 |
|
||||
| 0208e8092af75016a915ce1759e68bb5 | Fix "Assign Product" button in ViewStoreMarket | complete | 1 |
|
||||
| badf0c6b720aa1c2804f73f5a6eb090a | ManageStoresAdmin: Edit/Assign buttons for store_owner/store_manager | complete | 1 |
|
||||
| a5e5578620a10c7aab0cd01c89592421 | Fix Cancel button clipped by BottomNav in CreateProductStoreOwner | complete | 1 |
|
||||
| 0f8a3ffd129c8d6000dfd18d01432000 | Fix Open POS on HomeStoreOwner/HomeShared to pass store hashkey | complete | 1 |
|
||||
| 3b3af2f37a16a11851d950fa33df090e | Add Open POS button in PosAccessKeys.vue | complete | 1 |
|
||||
| 1321cb985147e2ce3a341d045288e5f5 | Enable store owner to delete and edit stores in ManageStoresAdmin | complete | 1 |
|
||||
|
||||
## files-changed
|
||||
- ai-docs/dictionary.md
|
||||
- app/Http/Controllers/Market/ProductController.php
|
||||
- app/Http/Controllers/Market/StoreController.php
|
||||
- app/Http/Controllers/Support/VueRouteMap.php
|
||||
- resources/js/Pages/CreateProductStoreOwner.vue
|
||||
- resources/js/Pages/Fragments/Home/HomeShared.vue
|
||||
- resources/js/Pages/Fragments/Home/HomeStoreOwner.vue
|
||||
- resources/js/Pages/ManageStoresAdmin.vue
|
||||
- resources/js/Pages/PosAccessKeys.vue
|
||||
- resources/js/Pages/ViewStoreMarket.vue
|
||||
|
||||
## commit
|
||||
name: fix POS store resolution, store owner/manager access, and UI improvements
|
||||
hash: 223adf8102802df7b0ac2f603d5fe76a417047e9
|
||||
44
.claude/sessions/20260516-162816-BukidBountyApp-10tasks.md
Normal file
44
.claude/sessions/20260516-162816-BukidBountyApp-10tasks.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
date: 2026-05-16T16:28:16Z
|
||||
repo: BukidBountyApp
|
||||
branch: test
|
||||
total: 10
|
||||
completed: 10
|
||||
failed: 0
|
||||
commit: add role-specific home dashboards, fix 500 errors, and expand RBAC
|
||||
hash: 7b18636ad7d5de8a48ca75df856c5fd71979b3d1
|
||||
---
|
||||
|
||||
## plans
|
||||
| file | task | status | cycles |
|
||||
|------|------|--------|--------|
|
||||
| db3f8841 | Redesign HomeCooperative.vue | complete | 1 |
|
||||
| a13c7ea8 | Create HomeStoreManager.vue | complete | 1 |
|
||||
| 98b3202e | Fix LoginController null dereference | complete | 1 |
|
||||
| fffa63a9 | Fix GET /api/public/landing-page 500 | complete | 1 |
|
||||
| ac004175 | Enhance HomeOperator.vue | complete | 1 |
|
||||
| 11d5e96e | Fix POS UI empty state | complete | 1 |
|
||||
| 22aa928c | Fix GET /api/public/cooperative/:hash 500 | complete | 1 |
|
||||
| c9f6ceb4 | Seed demo products artisan command | complete | 1 |
|
||||
| 2e79878f | Enable accounting reports for store owner/manager | complete | 1 |
|
||||
| e2b7c6e2 | Fix public marketplace auth middleware | complete | 1 |
|
||||
|
||||
## files-changed
|
||||
app/Console/Commands/SeedDemoProducts.php (new)
|
||||
app/Http/Controllers/Admin/LandingPageController.php
|
||||
app/Http/Controllers/Helpers/Permissions/UserPermissions.php
|
||||
app/Http/Controllers/LoginController.php
|
||||
app/Http/Controllers/Market/CooperativeController.php
|
||||
app/Http/Controllers/Support/VueRouteMap.php
|
||||
resources/js/Pages/AccountingDashboard.vue
|
||||
resources/js/Pages/Fragments/Home/HomeCooperative.vue
|
||||
resources/js/Pages/Fragments/Home/HomeOperator.vue
|
||||
resources/js/Pages/Fragments/Home/HomeStoreManager.vue (new)
|
||||
resources/js/Pages/Fragments/Home/HomeStoreOwner.vue
|
||||
resources/js/Pages/Home.vue
|
||||
resources/js/Pages/PosMain.vue
|
||||
routes/web.php
|
||||
|
||||
## commit
|
||||
name: add role-specific home dashboards, fix 500 errors, and expand RBAC
|
||||
hash: 7b18636ad7d5de8a48ca75df856c5fd71979b3d1
|
||||
25
.claude/sessions/20260516-174353-BukidBountyApp-1tasks.md
Normal file
25
.claude/sessions/20260516-174353-BukidBountyApp-1tasks.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
date: 2026-05-16T17:43:53Z
|
||||
repo: BukidBountyApp
|
||||
branch: test
|
||||
total: 1
|
||||
completed: 1
|
||||
failed: 0
|
||||
commit: fix stale role persisting across account-type switches on homepage
|
||||
hash: eb91431f4b4f6a1ca0bcde6a6c3cd1bca7d038e2
|
||||
---
|
||||
|
||||
## plans
|
||||
| file | task | status | cycles |
|
||||
|------|------|--------|--------|
|
||||
| 926a10dd | Fix homepage fragment persisting from previous login | complete | 1 |
|
||||
|
||||
## files-changed
|
||||
resources/js/composables/Core/useAuth.js
|
||||
resources/js/Pages/Auth/Login.vue
|
||||
resources/js/Pages/Home.vue
|
||||
resources/js/stores/user.js
|
||||
|
||||
## commit
|
||||
name: fix stale role persisting across account-type switches on homepage
|
||||
hash: eb91431f
|
||||
25
.claude/sessions/20260516-184113-BukidBountyApp-2tasks.md
Normal file
25
.claude/sessions/20260516-184113-BukidBountyApp-2tasks.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
date: 2026-05-16T18:41:13Z
|
||||
repo: BukidBountyApp
|
||||
branch: test
|
||||
total: 2
|
||||
completed: 2
|
||||
failed: 0
|
||||
commit: fix bulk-apply layout and AssignProductToStore URL persistence
|
||||
hash: 1ab3e82b927090695214e24aa379cb895f1bff09
|
||||
---
|
||||
|
||||
## plans
|
||||
| file | task | status | cycles |
|
||||
|------|------|--------|--------|
|
||||
| d649faffd424aace60ec46a1fde5ff83 | Fix bulk-apply "Apply" buttons on AddProductsToStore Step 2 (mobile layout broken) | complete | 1 |
|
||||
| 03a9c3aab75c3bf53e7aadb66f6fd475 | Fix AssignProductToStore — navigation only encodes store hash into URL, product hash is lost on direct access | complete | 1 |
|
||||
|
||||
## files-changed
|
||||
- resources/js/Pages/AddProductsToStore.vue
|
||||
- resources/js/Pages/AssignProductToStore.vue
|
||||
- resources/js/Pages/ListProductsMarket.vue
|
||||
|
||||
## commit
|
||||
name: fix bulk-apply layout and AssignProductToStore URL persistence
|
||||
hash: 1ab3e82b927090695214e24aa379cb895f1bff09
|
||||
26
.claude/sessions/20260516-215637-BukidBountyApp-2tasks.md
Normal file
26
.claude/sessions/20260516-215637-BukidBountyApp-2tasks.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
date: 2026-05-16T21:56:37Z
|
||||
repo: BukidBountyApp
|
||||
branch: experimental
|
||||
total: 2
|
||||
completed: 2
|
||||
failed: 0
|
||||
commit: fix AddProductsToStore layout and add POS print + sold-today stats
|
||||
hash: f8514890096b38470c4259ad04e9fd894d3fa503
|
||||
---
|
||||
|
||||
## plans
|
||||
| file | task | status | cycles |
|
||||
|------|------|--------|--------|
|
||||
| d42061a3fa5b1ccc8fd9cb99ecc7ea58 | Fix broken layout on /add-products-to-store--h:<storeHash> | complete | 0 |
|
||||
| d478a19b3dade245b1c97f86422285c3 | Add print button for POS scan code + sold-today / total-sold stats on BuyViewProductMarket | complete | 0 |
|
||||
|
||||
## files-changed
|
||||
- ai-docs/dictionary.md
|
||||
- app/Http/Controllers/Market/ProductController.php
|
||||
- resources/js/Pages/AddProductsToStore.vue
|
||||
- resources/js/Pages/BuyViewProductMarket.vue
|
||||
|
||||
## commit
|
||||
name: fix AddProductsToStore layout and add POS print + sold-today stats
|
||||
hash: f8514890096b38470c4259ad04e9fd894d3fa503
|
||||
23
.claude/sessions/20260517-run-tasks-1tasks.md
Normal file
23
.claude/sessions/20260517-run-tasks-1tasks.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
date: 2026-05-17
|
||||
repo: BukidBountyApp
|
||||
branch: test
|
||||
total: 1
|
||||
completed: 1
|
||||
failed: 0
|
||||
commit: show Delete and Assign Products for store owners on ManageStoresAdmin
|
||||
hash: babb06814a182b6dc285348133a08c8ffd2b3fac
|
||||
---
|
||||
|
||||
## plans
|
||||
| file | task | status | cycles |
|
||||
|------|------|--------|--------|
|
||||
| 9407eb3f723064594aef12202c29e320 | ManageStoresAdmin — show Delete/Assign Products for STORE_OWNER | complete | 0 |
|
||||
|
||||
## files-changed
|
||||
- app/Http/Controllers/Market/StoreController.php
|
||||
- resources/js/Pages/ManageStoresAdmin.vue
|
||||
|
||||
## commit
|
||||
name: show Delete and Assign Products for store owners on ManageStoresAdmin
|
||||
hash: babb06814a182b6dc285348133a08c8ffd2b3fac
|
||||
41
.claude/settings.json
Normal file
41
.claude/settings.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git -C * status)",
|
||||
"Bash(git -C * log *)",
|
||||
"Bash(git -C * diff *)",
|
||||
"Bash(git -C * show *)",
|
||||
"Bash(git -C * branch *)",
|
||||
"Bash(git -C * ls-files *)",
|
||||
"Bash(git -C * rev-parse *)",
|
||||
"Bash(git -C * remote *)",
|
||||
"Bash(command -v *)",
|
||||
"Bash(curl *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(curl -sI *)",
|
||||
"Bash(curl -s http*)",
|
||||
"Bash(git *)",
|
||||
"Bash(for *)",
|
||||
"Bash(php *)",
|
||||
"Bash(php artisan *)",
|
||||
"Bash(python3 *)",
|
||||
"Bash(docker exec *)",
|
||||
"Bash(gemini *)"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 -c \"\nimport json,sys\ndata=json.load(sys.stdin)\ncmd=data.get('tool_input',{}).get('command','')\nif 'php artisan' in cmd and 'docker exec' not in cmd:\n print(json.dumps({'decision':'block','reason':'In this repo, php artisan commands must be run inside Docker. Use: docker exec -it bukidapp php artisan ...'}))\n sys.exit(2)\n\"",
|
||||
"statusMessage": "Checking for bare php artisan commands..."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
83
.claude/skills/dictionary/SKILL.md
Normal file
83
.claude/skills/dictionary/SKILL.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: dictionary
|
||||
description: "Project dictionary lookup and maintenance. Reads ai-docs/dictionary.md to understand project-specific terms, file mappings, and domain definitions before responding. Updates the dictionary with new discoveries after each session and commits/pushes the change. Capabilities: term lookup, definition enrichment, file mapping, domain context. Actions: read dictionary, update dictionary, commit dictionary. Keywords: dictionary, definition, term, glossary, meaning, what is, where is. Use when: starting any task (proactively load context), user references unfamiliar terms, user asks about project-specific concepts, after discovering new mappings or definitions worth saving."
|
||||
---
|
||||
|
||||
# Dictionary Skill
|
||||
|
||||
## Purpose
|
||||
|
||||
Maintain and consult a living project dictionary at `ai-docs/dictionary.md` that accumulates domain knowledge, term definitions, and file mappings across sessions.
|
||||
|
||||
## Lifecycle — run in this order every session
|
||||
|
||||
### 1. Bootstrap (always first)
|
||||
|
||||
Check if `ai-docs/dictionary.md` exists. If not, create it:
|
||||
|
||||
```markdown
|
||||
# Project Dictionary
|
||||
|
||||
A living reference of project-specific terms, file mappings, and domain definitions.
|
||||
|
||||
## Terms & Definitions
|
||||
|
||||
<!-- Add entries below -->
|
||||
|
||||
## File Mappings
|
||||
|
||||
<!-- Format: term → file path + description -->
|
||||
```
|
||||
|
||||
### 2. Read & Apply
|
||||
|
||||
Read `ai-docs/dictionary.md` in full. Extract:
|
||||
- **Term definitions** → use to interpret ambiguous words in the user's request
|
||||
- **File mappings** → know which files to look at for a given concept
|
||||
- **Domain context** → understand project conventions before making decisions
|
||||
|
||||
If the dictionary contains relevant entries, apply them silently (no need to narrate the lookup unless it resolves an ambiguity).
|
||||
|
||||
### 3. Update (after helping the user)
|
||||
|
||||
Add entries discovered during the session:
|
||||
- New domain terms with clear definitions
|
||||
- File paths tied to specific concepts or features
|
||||
- Naming conventions or patterns observed
|
||||
- Acronyms or shorthand used in the codebase
|
||||
|
||||
**Only add entries that are non-obvious and reusable across future sessions.** Do not add ephemeral task details.
|
||||
|
||||
#### Entry format
|
||||
|
||||
```markdown
|
||||
## Terms & Definitions
|
||||
|
||||
### <Term>
|
||||
<Definition>. Context: <where/how it's used in this project>.
|
||||
|
||||
## File Mappings
|
||||
|
||||
- **<concept>** → `<file path>` — <brief description>
|
||||
```
|
||||
|
||||
### 4. Commit & Push
|
||||
|
||||
After updating the dictionary, commit and push **only** `ai-docs/dictionary.md`:
|
||||
|
||||
```bash
|
||||
git add ai-docs/dictionary.md
|
||||
git commit -m "<summary of what was added to the dictionary>"
|
||||
git push
|
||||
```
|
||||
|
||||
The commit message should describe the dictionary changes, e.g.:
|
||||
- `"dictionary: add POS offline sync terms and file mappings"`
|
||||
- `"dictionary: define BukidBounty transaction states"`
|
||||
|
||||
## Rules
|
||||
|
||||
- Always read the dictionary before answering, even if the request seems simple.
|
||||
- Never commit other files alongside `ai-docs/dictionary.md`.
|
||||
- Skip the commit step if nothing new was learned.
|
||||
- Keep entries concise — one definition paragraph max per term.
|
||||
68
.claude/skills/gemini-ship-it/SKILL.md
Normal file
68
.claude/skills/gemini-ship-it/SKILL.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: gemini-ship-it
|
||||
description: "Delegate ship-it (stage, commit, push) to Gemini CLI and report only SUCCESS or ERROR. Use when the user types /gemini-ship-it or says 'gemini ship it'. Manual-only — never auto-invoke."
|
||||
allowed-tools:
|
||||
- Bash
|
||||
---
|
||||
|
||||
# gemini-ship-it
|
||||
|
||||
Runs the ship-it workflow via Gemini CLI with suppressed output — reports only a single SUCCESS or ERROR line.
|
||||
|
||||
## Trigger
|
||||
|
||||
Only activate when the user explicitly types `/gemini-ship-it` or says "gemini ship it". Never auto-invoke.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Build the ship-it prompt
|
||||
|
||||
Construct a one-shot prompt that instructs Gemini to:
|
||||
- Run `git status --short` to see what's dirty
|
||||
- Stage all modified/untracked files by explicit path (no `git add -A`)
|
||||
- Write a single-line commit message (imperative mood, ≤72 chars, no AI attribution)
|
||||
- Push to every remote via `git remote`
|
||||
|
||||
### 2. Execute via Gemini CLI
|
||||
|
||||
```bash
|
||||
RESULT=$(gemini --yolo -m gemini-2.5-flash -o json "
|
||||
You are running in the BukidBountyApp git repo at $(pwd).
|
||||
|
||||
Task: commit and push all currently changed files.
|
||||
Steps:
|
||||
1. Run: git status --short
|
||||
2. Stage each modified file by explicit path (never use git add -A or git add .)
|
||||
3. Commit with a single imperative-mood subject line (≤72 chars). No body, no trailers, no AI attribution.
|
||||
4. Run: for remote in \$(git remote); do git push \"\$remote\" \$(git branch --show-current) || git push -u \"\$remote\" \$(git branch --show-current); done
|
||||
|
||||
Do this immediately without asking for confirmation.
|
||||
" 2>&1)
|
||||
EXIT=$?
|
||||
```
|
||||
|
||||
### 3. Report only SUCCESS or ERROR
|
||||
|
||||
Parse the exit code and Gemini JSON output:
|
||||
|
||||
```bash
|
||||
if [ $EXIT -eq 0 ]; then
|
||||
# Extract commit SHA from output if possible
|
||||
SHA=$(echo "$RESULT" | grep -oP '[0-9a-f]{7,40}' | head -1)
|
||||
echo "SUCCESS${SHA:+ — $SHA}"
|
||||
else
|
||||
# Extract first error line
|
||||
ERR=$(echo "$RESULT" | grep -i "error\|fatal\|rejected" | head -1)
|
||||
echo "ERROR${ERR:+ — $ERR}"
|
||||
fi
|
||||
```
|
||||
|
||||
Output **nothing else** — no file lists, no diff stats, no Gemini planning text, no explanations.
|
||||
|
||||
## Rules
|
||||
|
||||
1. Output is exactly one line: `SUCCESS` (optionally `SUCCESS — <sha>`) or `ERROR` (optionally `ERROR — <reason>`).
|
||||
2. Never print Gemini's raw output or intermediate steps.
|
||||
3. Never force-push, never skip hooks (`--no-verify`), never touch unrelated files.
|
||||
4. No AI attribution in the commit message.
|
||||
5. If Gemini is not installed or not authenticated, output: `ERROR — gemini CLI not available`.
|
||||
14
.cline/kanban/config.json
Normal file
14
.cline/kanban/config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "npm-docker",
|
||||
"command": "npm run build && docker compose -f docker-compose-local.yml restart",
|
||||
"icon": "play"
|
||||
},
|
||||
{
|
||||
"label": "portforward-cline-kanban",
|
||||
"command": "netsh interface portproxy add v4tov4 listenport=3484 listenaddress=0.0.0.0 connectport=3484 connectaddress=127.0.0.1",
|
||||
"icon": "play"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
.clinerules/read-dictionary.md
Normal file
1
.clinerules/read-dictionary.md
Normal file
@@ -0,0 +1 @@
|
||||
read the dictionary.md at ai-docs/dictionary.md for you referrence
|
||||
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
vendor
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
storage/logs/*
|
||||
storage/framework/cache/*
|
||||
storage/framework/sessions/*
|
||||
storage/framework/views/*
|
||||
55
.env copy
Normal file
55
.env copy
Normal file
@@ -0,0 +1,55 @@
|
||||
APP_NAME=BukidBountyApp
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:Zo3/gOTX3Ciqa4QjDytMt7WL0pxNa9RUnwnBBDojWbQ=
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_CHANNELS=single
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# DB_CONNECTION=postgres
|
||||
|
||||
# DB_PG_DRIVER=pgsql
|
||||
# DB_PG_HOST=host.docker.internal
|
||||
# DB_PG_PORT=5432
|
||||
# DB_PG_DATABASE=bukid
|
||||
# DB_PG_USERNAME=postgres
|
||||
# DB_PG_PASSWORD='EnvisionControl21!'
|
||||
# DB_PG_CHARSET=utf8
|
||||
# # DB_PG_PREFIX=
|
||||
# DB_PG_SCHEMA=bukidbounty
|
||||
# DB_PG_SSLMODE=prefer
|
||||
|
||||
DB_CONNECTION = mysql
|
||||
|
||||
DB_HOST=host.docker.internal
|
||||
DB_DATABASE=bukid
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=936585
|
||||
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_CONNECTION=default
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
SESSION_COOKIE='bukidbountyapp:cache'
|
||||
SESSION_SECURE_COOKIE=false
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
|
||||
CACHE_DRIVER=redis
|
||||
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
REDIS_HOST=host.docker.internal
|
||||
REDIS_AUTH=Milipede021!
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
|
||||
JWT_SECRET='uXuy1kgr7TKH+wQoGA2QbVrK5HO27hlFNDmyU0F+Rw0sDcB/12aZR4nMguaGRVo3iy42nnILpLa4V5OCVD+Cmg=='
|
||||
54
.env.example
Normal file
54
.env.example
Normal file
@@ -0,0 +1,54 @@
|
||||
APP_NAME=Hypervel
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
APP_KEY=
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_CHANNELS=single
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=localhost
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=hypervel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
|
||||
CACHE_DRIVER=redis
|
||||
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_AUTH=(null)
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
|
||||
JWT_SECRET=
|
||||
|
||||
# Module Configuration
|
||||
MODULES_SYSTEM_ENABLED=true
|
||||
MODULE_POS_ENABLED=true
|
||||
MODULE_PRODUCTS_ENABLED=true
|
||||
MODULE_STORES_ENABLED=true
|
||||
MODULE_SHIPMENTS_ENABLED=true
|
||||
MODULE_FARMERS_ENABLED=true
|
||||
MODULE_COOPERATIVES_ENABLED=true
|
||||
MODULE_ACCOUNTING_ENABLED=true
|
||||
MODULE_TRANSACTIONS_ENABLED=true
|
||||
MODULE_PROPERTIES_ENABLED=true
|
||||
MODULE_CART_ENABLED=true
|
||||
MODULE_CREDITS_ENABLED=true
|
||||
MODULE_LANDING_PAGES_ENABLED=true
|
||||
MODULE_ANNOUNCEMENTS_ENABLED=true
|
||||
MODULE_BATCH_ENABLED=true
|
||||
49
.env.local
Normal file
49
.env.local
Normal file
@@ -0,0 +1,49 @@
|
||||
APP_NAME=BukidBountyApp
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:Zo3/gOTX3Ciqa4QjDytMt7WL0pxNa9RUnwnBBDojWbQ=
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_CHANNELS=single
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=postgres
|
||||
|
||||
DB_PG_DRIVER=pgsql
|
||||
DB_PG_HOST=host.docker.internal
|
||||
DB_PG_PORT=5432
|
||||
DB_PG_DATABASE=bukid
|
||||
DB_PG_USERNAME=postgres
|
||||
DB_PG_PASSWORD='root'
|
||||
DB_PG_CHARSET=utf8
|
||||
# DB_PG_PREFIX=
|
||||
DB_PG_SCHEMA=bukidbounty
|
||||
DB_PG_SSLMODE=prefer
|
||||
|
||||
|
||||
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_CONNECTION=default
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
SESSION_COOKIE='bukidbountyapp:cache'
|
||||
SESSION_SECURE_COOKIE=false
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
|
||||
CACHE_DRIVER=redis
|
||||
|
||||
QUEUE_CONNECTION=database
|
||||
d
|
||||
REDIS_HOST=193.160.119.172
|
||||
REDIS_AUTH='5TadXd19cffDflE5MLxHs6bUTEYJ2bRo5Ba5ePKT9r6F7DUvbkA7VpEHXWj0zZ8p'
|
||||
REDIS_PORT=5433
|
||||
REDIS_DB=0
|
||||
|
||||
JWT_SECRET='uXuy1kgr7TKH+wQoGA2QbVrK5HO27hlFNDmyU0F+Rw0sDcB/12aZR4nMguaGRVo3iy42nnILpLa4V5OCVD+Cmg=='
|
||||
49
.env_def
Normal file
49
.env_def
Normal file
@@ -0,0 +1,49 @@
|
||||
APP_NAME=BukidBountyApp
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:Zo3/gOTX3Ciqa4QjDytMt7WL0pxNa9RUnwnBBDojWbQ=
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_CHANNELS=single
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=postgres
|
||||
|
||||
DB_PG_DRIVER=pgsql
|
||||
DB_PG_HOST=host.docker.internal
|
||||
DB_PG_PORT=5432
|
||||
DB_PG_DATABASE=bukid
|
||||
DB_PG_USERNAME=postgres
|
||||
DB_PG_PASSWORD='root'
|
||||
DB_PG_CHARSET=utf8
|
||||
# DB_PG_PREFIX=
|
||||
DB_PG_SCHEMA=bukidbounty
|
||||
DB_PG_SSLMODE=prefer
|
||||
|
||||
|
||||
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_CONNECTION=default
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
SESSION_COOKIE='bukidbountyapp:cache'
|
||||
SESSION_SECURE_COOKIE=false
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
|
||||
CACHE_DRIVER=redis
|
||||
|
||||
QUEUE_CONNECTION=database
|
||||
d
|
||||
REDIS_HOST=host.docker.internal
|
||||
REDIS_AUTH=Milipede021!
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
|
||||
JWT_SECRET='uXuy1kgr7TKH+wQoGA2QbVrK5HO27hlFNDmyU0F+Rw0sDcB/12aZR4nMguaGRVo3iy42nnILpLa4V5OCVD+Cmg=='
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
ko_fi: hypervel
|
||||
39
.github/ISSUE_TEMPLATE/Bug_report.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/Bug_report.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Bug Report
|
||||
description: "Only report the bug of the framework skeleton here. For bugs in the framework, you should click the link below:"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Please read [our full contribution guide](https://hypervel.org/docs/contributions#bug-reports) before submitting bug reports. If you notice improper DocBlock, PHPStan, or IDE warnings while using Hypervel, do not create a GitHub issue. Instead, please submit a pull request to fix the problem."
|
||||
- type: input
|
||||
attributes:
|
||||
label: PHP Version
|
||||
description: Provide the PHP version that you are using.
|
||||
placeholder: 8.4.3
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Swoole Extension Version
|
||||
description: Provide the Swoole extension version that you are using (You can get it by running `php --ri swoole`).
|
||||
placeholder: 5.1.6
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Database Driver & Version
|
||||
description: If applicable, provide the database driver and version you are using.
|
||||
placeholder: "MySQL 8.0.31 for macOS 13.0 on arm64 (Homebrew)"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Provide a detailed description of the issue you are facing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Provide detailed steps to reproduce your issue. If necessary, please provide a GitHub repository to demonstrate your issue.
|
||||
validations:
|
||||
required: true
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Bug Report (Framework)
|
||||
url: https://github.com/hypervel/components/issues
|
||||
about: "Report something that's broken in the framework."
|
||||
- name: Feature Request
|
||||
url: https://github.com/hypervel/components/discussions
|
||||
about: 'For ideas or feature requests, start a new discussion.'
|
||||
- name: Support Questions & Others
|
||||
url: https://github.com/hypervel/components/discussions
|
||||
about: 'This repository is only for reporting bugs. If you have a question or need help using the library, click:'
|
||||
- name: Documentation Issues
|
||||
url: https://github.com/hypervel/hypervel.org
|
||||
about: For documentation issues, open a pull request at the hypervel.org repository.
|
||||
25
.github/release.yml
vendored
Normal file
25
.github/release.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
- github-actions
|
||||
authors:
|
||||
- octocat
|
||||
- dependabot
|
||||
categories:
|
||||
- title: Breaking Changes 🛠
|
||||
labels:
|
||||
- breaking-change
|
||||
- title: New Features 🎉
|
||||
labels:
|
||||
- feature
|
||||
- title: Improvements 🔧
|
||||
labels:
|
||||
- enhancement
|
||||
- improvement
|
||||
- title: Bug fixes 🐛
|
||||
labels:
|
||||
- bug
|
||||
- title: Other Changes 🔄
|
||||
labels:
|
||||
- "*"
|
||||
39
.github/workflows/tests.yml
vendored
Normal file
39
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
linux_tests:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- php: "8.2"
|
||||
swoole: "5.1.6"
|
||||
- php: "8.3"
|
||||
swoole: "5.1.6"
|
||||
- php: "8.4"
|
||||
swoole: "6.0"
|
||||
|
||||
name: PHP ${{ matrix.php }} (swoole-${{ matrix.swoole }})
|
||||
|
||||
container:
|
||||
image: phpswoole/swoole:${{ matrix.swoole }}-php${{ matrix.php }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-dist -n -o
|
||||
|
||||
- name: Execute tests
|
||||
run: |
|
||||
PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
vendor/bin/phpunit -c phpunit.xml.dist
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
.buildpath
|
||||
/.settings
|
||||
.project
|
||||
*.patch
|
||||
/.idea
|
||||
/.git
|
||||
/runtime
|
||||
/vendor
|
||||
/.phpintel
|
||||
.env
|
||||
.DS_Store
|
||||
.phpunit*
|
||||
*.cache
|
||||
/.vscode
|
||||
.phpactor.json
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/node_modules
|
||||
/.phpunit.cache
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
storage/app/private/files/*
|
||||
config/viewmap.php
|
||||
LMStudio.AppImage
|
||||
public/assets/target.png:Zone.Identifier
|
||||
config/cache.php:Zone.Identifier
|
||||
dist
|
||||
build
|
||||
out
|
||||
public/assets/desktop.ini
|
||||
public/DBCache/*
|
||||
.ssh_keys/
|
||||
.kilocode
|
||||
public/build/.vite/manifest.json
|
||||
96
.php-cs-fixer.php
Normal file
96
.php-cs-fixer.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Runner\Parallel\ParallelConfig;
|
||||
|
||||
$maxProcesses = function_exists('swoole_cpu_num') ? swoole_cpu_num() : 4;
|
||||
|
||||
return (new Config())
|
||||
->setParallelConfig(new ParallelConfig($maxProcesses, 10))
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PSR2' => true,
|
||||
'@Symfony' => true,
|
||||
'@DoctrineAnnotation' => true,
|
||||
'@PhpCsFixer' => true,
|
||||
'phpdoc_no_alias_tag' => false,
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'list_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'concat_space' => [
|
||||
'spacing' => 'one',
|
||||
],
|
||||
'global_namespace_import' => [
|
||||
'import_classes' => true,
|
||||
'import_constants' => true,
|
||||
'import_functions' => null,
|
||||
],
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => [
|
||||
'declare',
|
||||
],
|
||||
],
|
||||
'general_phpdoc_annotation_remove' => [
|
||||
'annotations' => [
|
||||
'author',
|
||||
],
|
||||
],
|
||||
'ordered_imports' => [
|
||||
'imports_order' => [
|
||||
'class',
|
||||
'function',
|
||||
'const',
|
||||
],
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'single_line_comment_style' => [
|
||||
'comment_types' => [],
|
||||
],
|
||||
'yoda_style' => [
|
||||
'always_move_variable' => false,
|
||||
'equal' => false,
|
||||
'identical' => false,
|
||||
],
|
||||
'phpdoc_align' => [
|
||||
'align' => 'left',
|
||||
],
|
||||
'multiline_whitespace_before_semicolons' => [
|
||||
'strategy' => 'no_multi_line',
|
||||
],
|
||||
'constant_case' => [
|
||||
'case' => 'lower',
|
||||
],
|
||||
'class_attributes_separation' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'declare_strict_types' => true,
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_unused_imports' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'not_operator_with_space' => false,
|
||||
'ordered_class_elements' => true,
|
||||
'php_unit_strict' => false,
|
||||
'phpdoc_separation' => false,
|
||||
'single_quote' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'multiline_comment_opening_closing' => true,
|
||||
'fully_qualified_strict_types' => false,
|
||||
// Since PHP 8.3, default null values can be declared as nullable.
|
||||
'nullable_type_declaration_for_default_null_value' => true,
|
||||
'single_line_empty_body' => false,
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
->exclude('public')
|
||||
->exclude('runtime')
|
||||
->exclude('storage')
|
||||
->exclude('vendor')
|
||||
->in(__DIR__)
|
||||
)
|
||||
->setUsingCache(false);
|
||||
38
.ralphy/AGENTS.md
Normal file
38
.ralphy/AGENTS.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Project Agents
|
||||
|
||||
This file defines the specialized AI personas and roles for this project. Use these to guide the development process.
|
||||
|
||||
## 👥 Essential Roles
|
||||
|
||||
### Lead Architect
|
||||
- **Focus**: Overall structure, technology choice, and design patterns.
|
||||
- **Responsibility**: Ensures consistency and long-term maintainability.
|
||||
- **Guideline**: Prioritize clarity and simplicity over complex abstractions.
|
||||
|
||||
### Feature Developer
|
||||
- **Focus**: Implementation of specific requirements.
|
||||
- **Responsibility**: Writing clean, tested, and efficient code.
|
||||
- **Guideline**: Follow existing patterns; don't reinvent the wheel.
|
||||
|
||||
### Quality Assurance (QA)
|
||||
- **Focus**: Validation, testing, and edge cases.
|
||||
- **Responsibility**: Ensuring the project meets requirements and is bug-free.
|
||||
- **Guideline**: Think like a user and a malicious actor.
|
||||
|
||||
## 🛠️ Communication Protocols
|
||||
|
||||
- **Conciseness**: Avoid verbosity; explain only when necessary.
|
||||
- **Proactiveness**: Suggest improvements, but follow the plan.
|
||||
- **Transparency**: Log all major decisions and file modifications.
|
||||
|
||||
## 🛡️ Modification Guidelines
|
||||
|
||||
- **Root Access**: Never modify system or sensitive files unless explicitly tasked.
|
||||
- **State Management**: Keep the `.agent` directory updated with task progress.
|
||||
- **Verification**: Always verify changes before marking a task complete.
|
||||
|
||||
## 📜 Template Instructions
|
||||
To add a new agent, use the following structure:
|
||||
- **[Agent Name]**: [Description]
|
||||
- **Expertise**: [Skill 1], [Skill 2]
|
||||
- **Persona**: [Formal/Technical/Creative]
|
||||
42
CLAUDE.md
Normal file
42
CLAUDE.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# BukidBountyApp — project instructions
|
||||
|
||||
## Dictionary-first workflow (MANDATORY)
|
||||
|
||||
This repo maintains a project dictionary at `ai-docs/dictionary.md` that maps domain terms, file locations, and concepts. It exists to save tokens on repeated lookups.
|
||||
|
||||
**At the start of every task in this repo:**
|
||||
1. Read `ai-docs/dictionary.md` before exploring the codebase. Use it to resolve terms, file paths, and concepts instead of grepping/globbing first.
|
||||
2. Only fall back to direct codebase exploration when the dictionary doesn't cover the term.
|
||||
|
||||
**During and after the task:**
|
||||
3. When you discover anything worth caching for next time — a new term, a moved/renamed file, a corrected definition, a non-obvious mapping (term → file/function/route), a domain concept — update `ai-docs/dictionary.md` in place.
|
||||
4. Keep entries concise: term, one-line definition, and the canonical file path(s).
|
||||
5. Remove or correct entries you find to be stale or wrong.
|
||||
|
||||
The `dictionary` skill encapsulates this behavior — invoke it (or follow its rules manually) on every non-trivial task.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done — New Page or Feature (MANDATORY)
|
||||
|
||||
Every new page or API endpoint is **not complete** until all of the following are verified. Check each before closing any task that adds a page, route, or feature:
|
||||
|
||||
### Backend
|
||||
- [ ] `UserActions::NewActionName` added to `app/Enums/UserActions.php`
|
||||
- [ ] `UserPermissions::roles()` updated — which roles get the action, and which don't
|
||||
- [ ] Route added to `routes/web.php` with the correct middleware (`auth`, `module:*`, etc.)
|
||||
- [ ] Any raw `DB::table()->insert()` or `->update()` includes `created_by`/`updated_by` (Auth::id()) and `created_at`/`updated_at` (now()) — Eloquent listeners don't fire on raw queries
|
||||
|
||||
### Frontend / SPA
|
||||
- [ ] Route registered in `app/Http/Controllers/Support/VueRouteMap.php` with `allowedUserTypes` — **must happen in the same commit as the new `.vue` file**, not as a follow-up fix
|
||||
- [ ] Direct URL access tested (reload the page at its URL, not just navigate to it in-app)
|
||||
|
||||
### Theming
|
||||
- [ ] No `bg-white`, `bg-light`, or `text-dark` hardcoded in the template — use CSS variables (`var(--bg-card)`, `var(--text-primary)`) or rely on the global Bootstrap overrides in `app.js`
|
||||
- [ ] No scoped-CSS dark mode fix needed — the global `.dark-mode .bg-white` / `.bg-light` / `.text-dark` overrides in `app.js` handle Bootstrap classes automatically
|
||||
|
||||
### Layout
|
||||
- [ ] If the page uses `sticky-top` on a header, set `top: 66px; z-index: 1020;` — the app has a fixed `TopHeader` at 66px and a sticky child will hide behind it without this offset
|
||||
|
||||
### Canonical Table Names
|
||||
- [ ] Any raw SQL or `unique:`/`exists:` validation rule uses the **abbreviated table name** from the dictionary (`str` not `stores`, `prd_items` not `products`, `cst` not `customers`, `prd_str` not `product_store`) — wrong names cause silent 400/500 errors
|
||||
35
Dockerfile.dokploy
Normal file
35
Dockerfile.dokploy
Normal file
@@ -0,0 +1,35 @@
|
||||
# Base image: Hyperf with PHP 8.3, Alpine, Swoole
|
||||
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole-v6
|
||||
|
||||
USER root
|
||||
|
||||
# Install Node.js (for Vite frontend build), PostgreSQL client, and PHP extensions
|
||||
RUN apk add --no-cache \
|
||||
nodejs \
|
||||
npm \
|
||||
postgresql-dev \
|
||||
php83-pdo_pgsql \
|
||||
php83-pgsql
|
||||
|
||||
# Enable extensions (only needed if they don't auto-enable)
|
||||
RUN echo "extension=pdo_pgsql.so" > /etc/php83/conf.d/01_pdo_pgsql.ini && \
|
||||
echo "extension=pgsql.so" > /etc/php83/conf.d/00_pgsql.ini
|
||||
|
||||
# Copy application code
|
||||
WORKDIR /var/app
|
||||
COPY . /var/app
|
||||
|
||||
# Install Composer dependencies
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Install Node dependencies and build frontend assets
|
||||
RUN npm ci --prefer-offline && npm run build && rm -rf node_modules
|
||||
|
||||
# Expose Swoole HTTP port
|
||||
EXPOSE 9501
|
||||
|
||||
# Healthcheck endpoint (optional but works with Compose)
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s CMD curl -f http://localhost:9501/health || exit 1
|
||||
|
||||
# Run Hyperf Swoole server
|
||||
CMD ["php", "artisan", "start"]
|
||||
41
Dockerfile.php
Normal file
41
Dockerfile.php
Normal file
@@ -0,0 +1,41 @@
|
||||
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole-v6
|
||||
|
||||
USER root
|
||||
|
||||
# Install system deps
|
||||
RUN apk add --no-cache \
|
||||
postgresql-dev \
|
||||
php83-pdo_pgsql \
|
||||
php83-pgsql \
|
||||
7zip \
|
||||
curl
|
||||
|
||||
# Install Node
|
||||
RUN curl -fsSL https://unofficial-builds.nodejs.org/download/release/v20.19.0/node-v20.19.0-linux-x64-musl.tar.gz \
|
||||
| tar -xz -C /usr/local --strip-components=1 \
|
||||
&& node -v \
|
||||
&& npm -v
|
||||
|
||||
# Enable PHP extensions
|
||||
RUN echo "extension=pdo_pgsql.so" > /etc/php83/conf.d/01_pdo_pgsql.ini && \
|
||||
echo "extension=pgsql.so" > /etc/php83/conf.d/00_pgsql.ini
|
||||
|
||||
WORKDIR /var/app
|
||||
|
||||
# ✅ Copy composer files FIRST (for caching + correctness)
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# ✅ Copy package files and install frontend deps
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# ✅ Copy the rest of the app
|
||||
COPY . .
|
||||
|
||||
# Build frontend
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 9501
|
||||
|
||||
CMD ["php", "artisan", "serve"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Hypervel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
218
MIGRATION_PLAN.md
Normal file
218
MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Migration Plan: Blade Templates to Vue Components
|
||||
|
||||
## Overview
|
||||
This document outlines the migration from Blade templates to Vue 3 components for the BukidBountyApp project.
|
||||
|
||||
---
|
||||
|
||||
## Already Migrated Templates (Excluded from this plan)
|
||||
|
||||
| Template | Destination |
|
||||
|----------|-------------|
|
||||
| `home.blade.php` | `Pages/Home.vue` |
|
||||
| `list_product.blade.php` | `Pages/Market/ListProductsMarket.vue` |
|
||||
| `list_store.blade.php` | `Pages/Market/ListStoreMarket.vue` |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Blade Templates to Migrate
|
||||
|
||||
### Category 1: Core Photos (2 templates)
|
||||
|
||||
| Template | Functionality | Notes |
|
||||
|----------|---------------|-------|
|
||||
| `view_photo_admin.blade.php` | Photo view with admin controls | Simple display, use Vue component structure |
|
||||
| `view_photo_admin_advanced.blade.php` | Advanced photo view | Similar to above |
|
||||
|
||||
**Status:** Pending
|
||||
|
||||
---
|
||||
|
||||
### Category 2: Market Shared (4 templates)
|
||||
|
||||
| Template | Functionality | Dependencies |
|
||||
|----------|---------------|--------------|
|
||||
| `buy_view_product_market.blade.php` | Product buying view with images, description, quantity | Already migrated - use existing Vue component |
|
||||
| `create_store_market.blade.php` | Store creation form with photo upload | New store Pinia store needed |
|
||||
| `edit_store_market.blade.php` | Store editing form | Existing store data loading |
|
||||
|
||||
**Status:** Pending (some already have Vue equivalents)
|
||||
|
||||
---
|
||||
|
||||
### Category 3: User Shared Admin (3 templates)
|
||||
|
||||
| Template | Functionality | Notes |
|
||||
|----------|---------------|-------|
|
||||
| `view_buy_product_admin.blade.php` | Product view from admin perspective | Use existing product component pattern |
|
||||
|
||||
**Status:** Pending
|
||||
|
||||
---
|
||||
|
||||
### Category 4: User Shared AdminAdvanced (9 templates)
|
||||
|
||||
| Template | Functionality | Dependencies |
|
||||
|----------|---------------|--------------|
|
||||
| `create_user.blade.php` | Create new user form with role selection | User Pinia store needed |
|
||||
| `edit_user.blade.php` | Edit existing user profile | User Pinia store needed |
|
||||
| `view_store_market.blade.php` | Store market view page | Already migrated |
|
||||
| `manage_product_admin.blade.php` | Product management card view | ViewBuilder → Vue reactivity |
|
||||
| `edit_product_ultimate.blade.php` | Product edit form (categories, subcategories) | Category API calls |
|
||||
| `edit_store_ultimate.blade.php` | Store edit with owner selection | User list dropdown |
|
||||
| `remove_product_from_store_admin.blade.php` | Remove product from store | Confirmation dialog needed |
|
||||
| `manage_global_transactions.blade.php` | Transaction history table | DataTables → Vue table |
|
||||
|
||||
**Status:** Pending
|
||||
|
||||
---
|
||||
|
||||
### Category 5: User Shared All (2 templates)
|
||||
|
||||
| Template | Functionality | Notes |
|
||||
|----------|---------------|-------|
|
||||
| `account_settings.blade.php` | User profile display | Simple HTML structure, convert to Vue component |
|
||||
| `transfermycredit.blade.php` | Credit transfer form with modals | Use Pinia store for state |
|
||||
|
||||
**Status:** Pending
|
||||
|
||||
---
|
||||
|
||||
### Category 6: User Ultimate (4 templates)
|
||||
|
||||
| Template | Functionality | Dependencies |
|
||||
|----------|---------------|--------------|
|
||||
| `home.blade.php` | Ultimate home page menu | Already migrated |
|
||||
| `ultimatefunction.blade.php` | Navigation menu with service buttons | Use Vue component array |
|
||||
| `userlist.blade.php` | Child users table with DataTables | API endpoint: /admin/users/list |
|
||||
| `usermodify.blade.php` | User management (enable/disable, roles, notes) | Multiple admin endpoints |
|
||||
|
||||
**Status:** Pending
|
||||
|
||||
---
|
||||
|
||||
## Pinia Stores Required
|
||||
|
||||
### Existing Store
|
||||
- **`stores/network.js`** - Network status and loading states ✅
|
||||
|
||||
### New Stores to Create
|
||||
|
||||
#### 1. `stores/product.js`
|
||||
```javascript
|
||||
export const useProductStore = defineStore('product', {
|
||||
state: () => ({
|
||||
products: [],
|
||||
currentProduct: null,
|
||||
categories: [],
|
||||
subcategories: []
|
||||
}),
|
||||
actions: {
|
||||
async fetchProducts() { ... },
|
||||
async fetchProductById(id) { ... },
|
||||
async updateProduct(data) { ... }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 2. `stores/store.js`
|
||||
```javascript
|
||||
export const useStoreStore = defineStore('store', {
|
||||
state: () => ({
|
||||
stores: [],
|
||||
currentStore: null,
|
||||
storeProducts: []
|
||||
}),
|
||||
actions: {
|
||||
async fetchStores() { ... },
|
||||
async createStore(data) { ... }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. `stores/user.js`
|
||||
```javascript
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
users: [],
|
||||
currentUser: null,
|
||||
roles: []
|
||||
}),
|
||||
actions: {
|
||||
async fetchUsers() { ... },
|
||||
async updateUser(data) { ... }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vue Component Patterns to Follow
|
||||
|
||||
### Navigation
|
||||
```vue
|
||||
<script setup>
|
||||
import { useNavigate } from '../composables/Core/useNavigate'
|
||||
|
||||
const { navigate } = useNavigate()
|
||||
navigate({ page: 'PageName', props: { id: 123 } })
|
||||
</script>
|
||||
```
|
||||
|
||||
### Modals
|
||||
```vue
|
||||
<script setup>
|
||||
import { useModal } from '../composables/Core/useModal'
|
||||
|
||||
const modal = useModal()
|
||||
modal.show('title', 'body content')
|
||||
</script>
|
||||
```
|
||||
|
||||
### API Calls (using axios)
|
||||
```javascript
|
||||
import axios from 'axios'
|
||||
|
||||
const response = await axios.post('/api/endpoint', data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Priority Order
|
||||
|
||||
### Phase 1: High Priority (Core Features)
|
||||
1. `create_user.blade.php` - User creation (admin feature)
|
||||
2. `edit_product_ultimate.blade.php` - Product editing
|
||||
3. `remove_product_from_store_admin.blade.php` - Product removal
|
||||
4. `transfermycredit.blade.php` - Credit transfer
|
||||
|
||||
### Phase 2: Medium Priority
|
||||
5. `account_settings.blade.php` - User settings
|
||||
6. `manage_global_transactions.blade.php` - Transaction history
|
||||
7. `userlist.blade.php` - User listing
|
||||
|
||||
### Phase 3: Lower Priority
|
||||
8. All admin advanced templates (9 files)
|
||||
9. Core photo templates (2 files)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create Pinia stores for products, stores, and users
|
||||
2. Convert each Blade template following Vue component structure:
|
||||
- `<template>` section with Vue directives
|
||||
- `<script setup>` with Composition API
|
||||
- `<style scoped>` for component-specific styles
|
||||
3. Replace `gotoPage()` with `navigate()` calls
|
||||
4. Replace jQuery/Ajax with axios or composables
|
||||
5. Test each migrated component
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All migrated templates should be placed in `resources/js/Pages/`
|
||||
- Use the existing `VueMigration.md` for reference patterns
|
||||
- Maintain consistent styling with the rest of the Vue app
|
||||
- Ensure all routes are properly configured in routing system
|
||||
174
README.md
Normal file
174
README.md
Normal file
@@ -0,0 +1,174 @@
|
||||
TODO
|
||||
|
||||
|
||||
# Member Information Record
|
||||
|
||||
This document contains the member details collected for registration or profile purposes.
|
||||
|
||||
---
|
||||
|
||||
## 🧍 Personal Information
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **First Name** | Rex Moran |
|
||||
| **Middle Name** | - |
|
||||
| **Last Name** | Loba |
|
||||
| **Suffix** | - |
|
||||
| **Gender** | - |
|
||||
| **Date of Birth** | - |
|
||||
| **Email** | rexm.loba@gmail.com |
|
||||
| **Phone** | - |
|
||||
|
||||
---
|
||||
|
||||
## 📱 Social Accounts
|
||||
|
||||
| Platform | Account Name / Number |
|
||||
|-----------|-----------------------|
|
||||
| **Facebook** | - |
|
||||
| **Messenger** | - |
|
||||
| **Viber** | - |
|
||||
| **TikTok** | - |
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Emergency Contact Information
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Name** | - |
|
||||
| **Address** | - |
|
||||
| **Contact Number** | - |
|
||||
| **Relation** | - |
|
||||
|
||||
---
|
||||
|
||||
## 🏛 Membership Details for Cooperatives
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Membership Type** | - |
|
||||
| **Organization Name** | - |
|
||||
| **Membership Level** | - |
|
||||
| **Officer Position** | - |
|
||||
| **Officer Level** | - |
|
||||
| **Concurrent Position** | - |
|
||||
| **Concurrent Level** | - |
|
||||
| **Cooperative Name** | - |
|
||||
| **Cooperative Position** | - |
|
||||
| **Year Beginning** | - |
|
||||
|
||||
---
|
||||
|
||||
## 🏠 Address Information
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Address** | - |
|
||||
| **Region** | - |
|
||||
| **Province** | - |
|
||||
| **City / Municipality** | - |
|
||||
| **Barangay** | - |
|
||||
|
||||
---
|
||||
|
||||
## 👪 Family & Education
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Civil Status** | - |
|
||||
| **Number of Children** | - |
|
||||
| **Highest Educational Attainment** | - |
|
||||
| **Course** | - |
|
||||
| **School Attended** | - |
|
||||
| **Year Last Attended** | - |
|
||||
|
||||
---
|
||||
|
||||
## 💼 Employment & Livelihood
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Current Source of Livelihood** | - |
|
||||
| **Company Last Employed** | - |
|
||||
| **Position in the Company** | - |
|
||||
| **Year Last Employed** | - |
|
||||
|
||||
---
|
||||
|
||||
## 🆔 Government Information
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Tax Identification Number (TIN)** | - |
|
||||
| **PhilHealth ID Number** | - |
|
||||
| **Government Issued ID** | - |
|
||||
|
||||
---
|
||||
|
||||
## 🖼 Supporting Documents
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Photograph** | - |
|
||||
| **Date Submitted** | - |
|
||||
| **Date Approved** | - |
|
||||
|
||||
---
|
||||
|
||||
### 🗒 Notes
|
||||
- Fields marked with “–” are currently unfilled.
|
||||
- Update this file as new information becomes available.
|
||||
- Store sensitive data responsibly and avoid committing private identifiers in public repositories.
|
||||
|
||||
---
|
||||
|
||||
📧 **Contact:** rexm.loba@gmail.com
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p align="center"><a href="https://hypervel.org" target="_blank"><img src="https://hypervel.org/logo.png" width="400"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/hypervel/hypervel/actions"><img src="https://github.com/hypervel/hypervel/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/hypervel/framework"><img src="https://img.shields.io/packagist/dt/hypervel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/hypervel/hypervel"><img src="https://img.shields.io/packagist/v/hypervel/hypervel" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/hypervel/hypervel"><img src="https://img.shields.io/packagist/l/hypervel/hypervel" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## Introduction
|
||||
|
||||
**Hypervel** is a Laravel-style PHP framework with native coroutine support for ultra-high performance.
|
||||
|
||||
Hypervel ports many core components from Laravel while maintaining familiar usage patterns, making it instantly accessible to Laravel developers. The framework combines the elegant and expressive development experience of Laravel with the powerful performance benefits of coroutine-based programming. If you're a Laravel developer, you'll feel right at home with this framework, requiring minimal learning curve.
|
||||
|
||||
This is an ideal choice for building microservices, API gateways, and high-concurrency applications where traditional PHP frameworks often encounter performance constraints.
|
||||
|
||||
## Why Hypervel?
|
||||
|
||||
While Laravel Octane impressively enhances your Laravel application's performance, it's crucial to understand the nature of modern web applications. In most cases, the majority of latency stems from I/O operations, such as file operations, database queries, and API requests.
|
||||
|
||||
However, Laravel doesn't support coroutines - the entire framework is designed for a blocking I/O environment. Applications heavily dependent on I/O operations will still face performance bottlenecks. Consider this scenario:
|
||||
|
||||
Imagine building an AI-powered chatbot where each conversation API takes 3-5 seconds to respond. With 10 workers in Laravel Octane receiving 10 concurrent requests, all workers would be blocked until these requests complete.
|
||||
|
||||
> You can see [benchmark comparison](https://hypervel.org/docs/introduction.html#benchmark) between Laravel Octane and Hypervel
|
||||
|
||||
Even with Laravel Octane's improvements, your application's concurrent request handling capacity remains constrained by I/O operation duration. Hypervel addresses this limitation through coroutines, enabling efficient handling of concurrent I/O operations without blocking workers. This approach significantly enhances performance and concurrency for I/O-intensive applications.
|
||||
|
||||
> See [this issue](https://github.com/laravel/octane/issues/765) for more discussions.
|
||||
|
||||
## Documentation
|
||||
|
||||
[https://hypervel.org/docs](https://hypervel.org/docs)
|
||||
|
||||
Hypervel provides comprehensive and user-friendly documentation that allows you to quickly get started. From this documentation, you can learn how to use various components in Hypervel and understand the differences between this framework and Laravel.
|
||||
|
||||
> Most of the content in this documentation is referenced from the official Laravel documentation. We appreciate the Laravel community's contributions.
|
||||
|
||||
## License
|
||||
|
||||
The Hypervel framework is open-sourced software licensed under the [MIT](https://opensource.org/licenses/MIT) license.
|
||||
220
VueMigration.md
Normal file
220
VueMigration.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Project Structure Analysis: Vue.js Migration from Blade
|
||||
|
||||
## Overview
|
||||
The BukidBountyApp has been fully migrated from Blade templates to a **Vue 3 SPA (Single Page Application)** with Pinia for state management.
|
||||
|
||||
---
|
||||
|
||||
## 1. Application Architecture
|
||||
|
||||
### Entry Point (`resources/js/app.js`)
|
||||
- **Vue 3** app created with `createApp()`
|
||||
- **Pinia** state manager initialized via `createPinia()`
|
||||
- Auto-imports all Vue components from `./Pages/**/*.vue` (excluding Fragments)
|
||||
- Main layout structure:
|
||||
- `TopHeader` (fixed header at top)
|
||||
- `main-content` area for page routing
|
||||
- `BottomNav` (fixed bottom navigation bar)
|
||||
- `BaseModal` (global modal component)
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Layout Components
|
||||
| Component | Location | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `TopHeader.vue` | Pages/Core/Fragments/ | Fixed header with back button, title, and settings action |
|
||||
| `BottomNav.vue` | Pages/Core/Fragments/ | Bottom navigation with Home, Cart, Properties links |
|
||||
| `BaseModal.vue` | Components/Core/BaseModal.vue | Global modal using Teleport to body |
|
||||
|
||||
---
|
||||
|
||||
## 2. Pinia Stores Usage
|
||||
|
||||
### Current Store: `resources/js/stores/network.js`
|
||||
|
||||
The project currently has one Pinia store for network status management:
|
||||
|
||||
```javascript
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useNetworkStore = defineStore('network', {
|
||||
state: () => ({
|
||||
isOnline: navigator.onLine,
|
||||
isLoading: false
|
||||
}),
|
||||
actions: {
|
||||
setOnline(status) {
|
||||
this.isOnline = status
|
||||
if (status) this.isLoading = false
|
||||
},
|
||||
startLoading() { this.isLoading = true },
|
||||
stopLoading() { this.isLoading = false }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### How to Use Pinia Stores in Components
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useNetworkStore } from '../stores/network'
|
||||
|
||||
// Call the store factory function
|
||||
const network = useNetworkStore()
|
||||
|
||||
// Access reactive state
|
||||
console.log(network.isOnline) // true/false
|
||||
console.log(network.isLoading) // true/false
|
||||
|
||||
// Call actions
|
||||
network.setOnline(true)
|
||||
network.startLoading()
|
||||
network.stopLoading()
|
||||
</script>
|
||||
```
|
||||
|
||||
### Creating a New Store
|
||||
|
||||
```javascript
|
||||
// stores/example.js
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useExampleStore = defineStore('example', {
|
||||
state: () => ({
|
||||
count: 0,
|
||||
items: []
|
||||
}),
|
||||
getters: {
|
||||
doubledCount: (state) => state.count * 2
|
||||
},
|
||||
actions: {
|
||||
increment() { this.count++ },
|
||||
addItem(item) { this.items.push(item) }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Then use in component:
|
||||
```vue
|
||||
<script setup>
|
||||
import { useExampleStore } from '../stores/example'
|
||||
const store = useExampleStore()
|
||||
store.increment()
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Composables (Vue Composition API)
|
||||
|
||||
### Key Composables
|
||||
|
||||
| Composable | Location | Purpose |
|
||||
|------------|----------|---------|
|
||||
| `useNavigate` | composables/Core/useNavigate.js | SPA navigation between pages |
|
||||
| `useModal` | composables/Core/useModal.js | Global modal management |
|
||||
| `useAuth` | composables/Core/useAuth.js | User authentication/role state |
|
||||
|
||||
### Navigation Pattern
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useNavigate } from '../composables/Core/useNavigate'
|
||||
|
||||
const { navigate } = useNavigate()
|
||||
|
||||
// Navigate to another page
|
||||
navigate({ page: 'CartProductMarket' })
|
||||
navigate({
|
||||
page: 'ViewStoreMarket',
|
||||
props: { storeId: 123 }
|
||||
})
|
||||
|
||||
// Reload current page
|
||||
navigate.reloadPage()
|
||||
</script>
|
||||
```
|
||||
|
||||
### Global `$navigate` Helper
|
||||
|
||||
The app exposes `window.$navigateHelper` which can be accessed via:
|
||||
- In components: `this.$navigate`
|
||||
- Globally: `window.$navigateHelper({ page, props })`
|
||||
|
||||
---
|
||||
|
||||
## 4. Page Routing System
|
||||
|
||||
### How It Works
|
||||
1. **Initial page** is defined in `index.html`: `data-page='{"component":"Home","props":{}}'`
|
||||
2. Pages are auto-imported from `resources/js/Pages/` directory
|
||||
3. Component names use dot notation (e.g., `Auth.Login`, `AccountSettings`)
|
||||
4. URLs use path notation: `/`, `/login`, `/auth/login`
|
||||
|
||||
### Page File Structure
|
||||
```
|
||||
Pages/
|
||||
├── Home.vue → /
|
||||
├── Auth/
|
||||
│ └── Login.vue → /auth/login
|
||||
├── AccountSettings.vue → /account/settings
|
||||
└── Market/
|
||||
├── ListProductsMarket.vue
|
||||
└── ViewStoreMarket.vue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"vue": "^3.5.27",
|
||||
"pinia": "^3.0.4",
|
||||
"axios": "^1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Summary: How to Access Pinia Stores
|
||||
|
||||
### From a Vue Component (Composition API)
|
||||
```vue
|
||||
<script setup>
|
||||
import { useNetworkStore } from '../stores/network'
|
||||
|
||||
const network = useNetworkStore()
|
||||
// Use: network.isOnline, network.isLoading, network.actions()
|
||||
</script>
|
||||
```
|
||||
|
||||
### From a Composable or Setup Function
|
||||
```javascript
|
||||
import { useNetworkStore } from '../stores/network'
|
||||
|
||||
const network = useNetworkStore()
|
||||
// Now you can access the store's state and actions
|
||||
```
|
||||
|
||||
### Global Access (Not Recommended)
|
||||
```javascript
|
||||
window.$networkStore = useNetworkStore() // Available if defined globally
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
The project has successfully migrated from Blade to Vue with:
|
||||
- ✅ SPA routing via `useNavigate` composable
|
||||
- ✅ Pinia for state management
|
||||
- ✅ Global modal system
|
||||
- ✅ Auto-imported page components
|
||||
- ✅ Browser history navigation (popstate support)
|
||||
55
ai-docs/ai-guide.md
Normal file
55
ai-docs/ai-guide.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# AI Assistant Navigation Guide
|
||||
|
||||
## Reading Strategy
|
||||
|
||||
### Start Here
|
||||
1. **repo-overview.md** - Understand the project purpose, technologies, and high-level structure
|
||||
2. **architecture.md** - Learn module interactions and data flows
|
||||
|
||||
### Locating Code
|
||||
3. Use **file-map.json** to find which module contains your target file
|
||||
4. Use **function-index.json** to locate specific functions/classes by name
|
||||
|
||||
### Reading Details
|
||||
5. Read the corresponding **module/*.md** for component-level understanding
|
||||
6. Read **file/*.md** for implementation details
|
||||
|
||||
### When to Open Source Files
|
||||
Only open original source files when:
|
||||
- Implementation details are not documented
|
||||
- Debugging requires exact line numbers
|
||||
- Understanding edge cases not covered in documentation
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Best Approach |
|
||||
|------|---------------|
|
||||
| Find login logic | Check `LoginController` in function-index.json, then read `app/Http/Controllers/LoginController.php` |
|
||||
| Add new user role | Modify `app/Enums/UserTypes.php`, update permissions in `UserPermissions` |
|
||||
| Create new product API | Add route in `routes/web.php`, create controller in `app/Http/Controllers/Market/` |
|
||||
| File upload handling | Use `FilesMainController@UploadFilefromRequest` |
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Adding a New Page
|
||||
1. Update `file-map.json` to add file mapping
|
||||
2. Create Vue component in `resources/js/Pages/`
|
||||
3. Add route in `routes/web.php`
|
||||
4. Create controller if needed
|
||||
5. Update viewmap config if server-side rendering required
|
||||
|
||||
### Modifying User Permissions
|
||||
1. Check `UserActions` enum in `app/Enums/UserActions.php`
|
||||
2. Review `UserPermissions::isActionPermitted()` logic
|
||||
3. Update role matrix as needed
|
||||
|
||||
### Debugging API Endpoints
|
||||
1. Find route in `routes/web.php`
|
||||
2. Locate controller from function-index.json
|
||||
3. Check middleware requirements in `Kernel.php`
|
||||
|
||||
## Architecture Tips
|
||||
|
||||
- **Vue Router**: SPA routes handled via Vue's client-side routing
|
||||
- **Server Rendering**: Use `/p/{page}/s/` for server-rendered pages
|
||||
- **Permissions**: Role checking happens via `canDo()` method on User model
|
||||
90
ai-docs/architecture.md
Normal file
90
ai-docs/architecture.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Architecture
|
||||
|
||||
## Major Modules
|
||||
|
||||
### User Management Module
|
||||
- **Purpose**: Handles user authentication, roles, and permissions
|
||||
- **Key Components**:
|
||||
- `User` model with hierarchical relationships (parent/children)
|
||||
- 13 user types via enum (Ultimate through Public)
|
||||
- Permission system via `UserActions` enum
|
||||
- **Location**: `app/Models/User.php`, `app/Enums/UserTypes.php`
|
||||
|
||||
### Market Module
|
||||
- **Purpose**: Product and store management
|
||||
- **Key Components**:
|
||||
- `Product` model - Products with categories, stores relationship
|
||||
- `Store` model - Stores with products via belongsToMany
|
||||
- `ProductTransaction` models - Transaction tracking
|
||||
- **Location**: `app/Models/Market/`
|
||||
|
||||
### File Management Module
|
||||
- **Purpose**: File upload and management system
|
||||
- **Key Components**:
|
||||
- `FileContent` model - Binary file storage
|
||||
- `FileList` model - File metadata
|
||||
- **Location**: `app/Models/FileContent.php`, `app/Models/FileList.php`
|
||||
|
||||
## Module Interactions
|
||||
|
||||
```
|
||||
User (Auth) → Middleware → Controllers → Models
|
||||
↓
|
||||
Market Models
|
||||
↓
|
||||
File Management
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
1. User attempts to access protected route
|
||||
2. `Authenticate` middleware checks session/JWT
|
||||
3. User object loaded with roles and permissions
|
||||
4. Route middleware (`auth`, `ultimate`, etc.) validates access
|
||||
|
||||
## Data Flows
|
||||
|
||||
### Login Flow
|
||||
```
|
||||
POST /post/loginnow → LoginController@authenticate
|
||||
↓
|
||||
Auth::attempt() (JWT-based)
|
||||
↓
|
||||
Session created via Hypervel\Session
|
||||
↓
|
||||
Redirect to home or return JSON response
|
||||
```
|
||||
|
||||
### User Creation Flow
|
||||
```
|
||||
POST /admin/user/create → CreateUserControllerUltimate@CreateUser
|
||||
↓
|
||||
Validate data (mobile number, username uniqueness)
|
||||
↓
|
||||
Create user record with encrypted password
|
||||
↓
|
||||
Return success/error response
|
||||
```
|
||||
|
||||
### Product View Flow
|
||||
```
|
||||
POST /View/Product/Details/data → ProductController@viewProductDetails
|
||||
↓
|
||||
Fetch product by ID
|
||||
↓
|
||||
Fetch associated stores
|
||||
↓
|
||||
Return product data with store info
|
||||
```
|
||||
|
||||
## External Services
|
||||
|
||||
- **Redis**: Caching layer (configurable via CACHE_DRIVER)
|
||||
- **Session Storage**: Database-backed sessions
|
||||
- **File Storage**: Local filesystem (via Flysystem)
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
- **MVC Pattern**: Models-Views-Controllers separation
|
||||
- **SPA Architecture**: Single Page Application with Vue Router
|
||||
- **Middleware Chain**: Request filtering via Hypervel middleware
|
||||
- **Enum-based Roles**: Type-safe user role system
|
||||
116
ai-docs/audit-2026-05-14-190110.md
Normal file
116
ai-docs/audit-2026-05-14-190110.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Store Owner Audit — 2026-05-14 19:01:10 UTC
|
||||
|
||||
Running notes from the Store Owner accessible-pages audit. Bugs we
|
||||
have already fixed are listed first for traceability; the open items
|
||||
below are not yet patched and need a follow-up decision before merge.
|
||||
|
||||
## Fixed (commit b4defbe + this batch)
|
||||
|
||||
### Erratic table names (Hypervel surfaces these as empty 400s)
|
||||
- `app/Http/Controllers/Market/ProductController.php:766`
|
||||
`pluck('stores.hashkey')` → `pluck('str.hashkey')`. Drove the
|
||||
Manage Listings modal in `ManageProductsAdmin.vue` to fail on every
|
||||
open.
|
||||
- `app/Http/Controllers/Market/BatchController.php:91`
|
||||
`where('products.id', …)` → `where('prd_items.id', …)`. Broke the
|
||||
"already attached to this store" check in batch-import (existing
|
||||
mode).
|
||||
|
||||
### Validation/permission walls
|
||||
- `ProductController::editProductAdmin` required `ModifyAllProducts`
|
||||
(Big-3 only) even when `data.store_hash` was supplied. STORE_OWNER
|
||||
edits via `UpdateProductModal.vue` returned 401. Now per-store
|
||||
edits only need `ModifyOwnProduct` / `AddProducttoOwnStore` plus
|
||||
the existing ownership branch.
|
||||
- `ProductController::toggleProductStatus_Admin` — same gate, same
|
||||
relaxation.
|
||||
- `ProductController::deleteProduct_Admin` required
|
||||
`DeleteAllProducts` (Big-3 only). Now accepts `DeleteOwnProduct`
|
||||
and the ancestor-of-creator path so owners can delete products
|
||||
they created.
|
||||
- `PosController::voidSession` had no authorization check at all.
|
||||
Any session_hash could void any session. Now gated by
|
||||
`UserPermissions::isUserAllowedAccessToStore`.
|
||||
- `PosAccessKeyController::destroy` and `toggleStatus` only checked
|
||||
the action permission. Now also verify the key belongs to a store
|
||||
the caller owns/manages.
|
||||
- `StoreController::autoCreate` had no RBAC. Now requires
|
||||
Big-3 or STORE_OWNER, and dedupes generated names against the
|
||||
globally unique `str.name` index by appending a short suffix.
|
||||
- `HomeStoreOwner.vue` referenced an undefined `creatingStore` ref
|
||||
(the actual ref is `creatingQuickStore`) — the "Creating your
|
||||
store..." spinner never rendered.
|
||||
|
||||
### BatchAddProducts opened to STORE_OWNER with guardrails
|
||||
- `BatchController::batchCreateProducts` now allows STORE_OWNER, but
|
||||
only when `target_store_hash` is provided AND the target store is
|
||||
owned (or managed) by the caller. Without it the call is rejected
|
||||
up-front instead of silently 401-ing partway through the loop.
|
||||
- New-mode rows are rejected when a global product with the same
|
||||
name (case-insensitive, trimmed) already exists. Owners are
|
||||
expected to pick the existing one via the fuzzy-search modal.
|
||||
- `BatchAddProducts.vue` now redirects STORE_OWNER to CreateStore via
|
||||
a yes/no modal when they have zero selectable stores, and refuses
|
||||
submit if no target store is picked.
|
||||
|
||||
## Open — not yet patched, decide before merging
|
||||
|
||||
### `editStoreDetails` data leak (medium)
|
||||
- File: `app/Http/Controllers/Market/StoreController.php:455`
|
||||
- Route: `POST /Edit/Store/Details/data`
|
||||
(`routes/web.php:468-474`, middleware `auth + module:stores`).
|
||||
- Behavior: looks up a `str` row by `hashkey`, and returns
|
||||
`name`, `category`, `subcategory`, `description`, `address`,
|
||||
`remarks`, `status`, `is_active`, `photourl`,
|
||||
`owner.hashkey`, `manager.hashkey`, all `store_managers` user
|
||||
hashkeys, all linked cooperative hashkeys, the parent-user
|
||||
selector list, and the dropzone payload — **without** any
|
||||
authorization check against the caller. Any authenticated user
|
||||
can issue this POST with any store's hashkey and get back the
|
||||
owner/manager/coop graph plus internal remarks. There is no
|
||||
modification path here, so this is information disclosure rather
|
||||
than escalation, but `remarks` is explicitly marked internal in
|
||||
the CreateStore flow and the manager/cooperative hashkeys leak
|
||||
the org graph.
|
||||
- Suggested fix: require Big-3 OR
|
||||
(`store->owner_id`/`manager_id` matches caller)
|
||||
OR caller appears in `store_managers` pivot
|
||||
OR caller is ancestor of any of the above
|
||||
(the same predicate used by `canUserAccessPos` /
|
||||
`isUserAllowedAccessToStore`). Strip `remarks` from the response
|
||||
for non-Big-3.
|
||||
- Status: **NOT FIXED IN THIS COMMIT.** Awaiting decision on
|
||||
whether to (a) tighten the endpoint and accept the chance of
|
||||
breaking any external/legacy caller, or (b) keep the
|
||||
read-anybody behavior and only strip `remarks`.
|
||||
|
||||
### Other observations (not bugs strictly)
|
||||
- `editProductAdminByStore` (`ProductController.php:494`) has a
|
||||
`TODO Check first if store_id is owned by current user` comment
|
||||
and modifies the global product directly. Currently nothing
|
||||
routes to it, but leaving it in the codebase is a footgun if
|
||||
someone wires it up later. Recommend deleting.
|
||||
- `listProducts_Admin` scopes non-Big-3 by `created_by`, so a
|
||||
store owner who attached a Big-3-created global product into
|
||||
their store cannot see/manage that product from "My Products".
|
||||
Per dictionary L265 this is intentional, but it does mean
|
||||
store-listings management for owners is split between
|
||||
"My Products" (creator-scoped) and the per-store flows
|
||||
(`AddProductsToStore`, `ManageStoresAdmin`).
|
||||
- The Store Owner home's "Import Products" tile (`pagename:
|
||||
BatchAddProducts`) now works for STORE_OWNER thanks to the
|
||||
BatchController relaxation above. Pre-fix this would have
|
||||
loaded the page and then 401-ed on submit.
|
||||
|
||||
## Stores accessed during this audit
|
||||
- `StoreController` (full file)
|
||||
- `ProductController` (full file)
|
||||
- `BatchController` (lines 33–172)
|
||||
- `PosController` (sample around startSession/voidSession/getTodayStats)
|
||||
- `PosAccessKeyController` (full file)
|
||||
- Pages: `HomeStoreOwner.vue`, `CreateProductStoreOwner.vue`,
|
||||
`CreateStore.vue`, `AddProductsToStore.vue`, `BatchAddProducts.vue`,
|
||||
`ManageProductAdmin.vue`, `ManageProductsAdmin.vue`,
|
||||
`ManageStoresAdmin.vue`, `PosAccessKeys.vue`,
|
||||
`UpdateProductModal.vue`
|
||||
- Routes: `routes/web.php` (home-data + Store/Product/POS blocks)
|
||||
86
ai-docs/call-graph.md
Normal file
86
ai-docs/call-graph.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Call Graph
|
||||
|
||||
## Authentication Flow
|
||||
```
|
||||
POST /post/loginnow → LoginController@authenticate
|
||||
↓
|
||||
Auth::attempt()
|
||||
↓
|
||||
Session::start()
|
||||
↓
|
||||
Redirect or JSON response
|
||||
```
|
||||
|
||||
## User Creation Flow
|
||||
```
|
||||
POST /admin/user/create → CreateUserControllerUltimate@CreateUser
|
||||
↓
|
||||
validateMobileNumberExists() / validateUsernameExists()
|
||||
↓
|
||||
HashPassword()
|
||||
↓
|
||||
User::create()
|
||||
↓
|
||||
Return success/error
|
||||
```
|
||||
|
||||
## User Management Flow
|
||||
```
|
||||
POST /admin/user/details → UserModifyAdminPageController@Response_UserDetails
|
||||
↓
|
||||
User::find($id)
|
||||
↓
|
||||
Return user details with relationships
|
||||
```
|
||||
|
||||
## Login Flow (SPA)
|
||||
```
|
||||
GET /login → viewHelperController@servePageFragmentWithTemplate
|
||||
↓
|
||||
view('Auth.Login', $data)
|
||||
↓
|
||||
Vue renders login form
|
||||
↓
|
||||
POST /post/loginnow
|
||||
```
|
||||
|
||||
## Product View Flow
|
||||
```
|
||||
POST /View/Product/Details/data → ProductController@viewProductDetails
|
||||
↓
|
||||
Product::find($id)
|
||||
↓
|
||||
Store::where('products.product_id')
|
||||
↓
|
||||
Return product with store data
|
||||
```
|
||||
|
||||
## Market Operations Flow
|
||||
```
|
||||
POST /Products/Admin/New/ → ProductController@createNew_Admin
|
||||
↓
|
||||
Product::create()
|
||||
↓
|
||||
Return product data
|
||||
```
|
||||
|
||||
## File Upload Flow
|
||||
```
|
||||
POST /File/Upload/{category} → FilesMainController@UploadFilefromRequest
|
||||
↓
|
||||
Storage::put('files', $file)
|
||||
↓
|
||||
FileContent::create($data)
|
||||
↓
|
||||
Return file hash key
|
||||
```
|
||||
|
||||
## Application Startup
|
||||
```
|
||||
resources/js/app.js → createApp()
|
||||
↓
|
||||
Pinia.createPinia()
|
||||
↓
|
||||
useUserStore().fetchCurrentUser()
|
||||
↓
|
||||
mount('#app')
|
||||
76
ai-docs/dependency-index.json
Normal file
76
ai-docs/dependency-index.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"app/Http/Controllers/viewHelperController.php": {
|
||||
"module": "pages",
|
||||
"imports": [
|
||||
"app/Http/Controllers/Pages/UserModifyAdminPageController.php"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/UserCreateController.php": {
|
||||
"module": "userManagement",
|
||||
"imports": [
|
||||
"app/Http/Controllers/Helpers/Permissions/UserPermissions.php"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/Market/ProductController.php": {
|
||||
"module": "market",
|
||||
"imports": [
|
||||
"App\\Enums\\UserActions",
|
||||
"App\\Models\\Market\\Product",
|
||||
"App\\Models\\Market\\Store",
|
||||
"App\\Http\\Controllers\\Helpers\\Permissions\\UserPermissions"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/Market/StoreController.php": {
|
||||
"module": "market",
|
||||
"imports": [
|
||||
"App\\Models\\Market\\Product",
|
||||
"App\\Models\\Market\\Store",
|
||||
"App\\Models\\User"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/Pages/UserModifyAdminPageController.php": {
|
||||
"module": "userManagement",
|
||||
"imports": [
|
||||
"App\\Enums\\UserActions",
|
||||
"App\\Http\\Controllers\\Helpers\\Permissions\\UserPermissions",
|
||||
"App\\Models\\User"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/Pages/UserListPageController.php": {
|
||||
"module": "userManagement",
|
||||
"imports": [
|
||||
"App\\Models\\User"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/Pages/AccountSettingsPageController.php": {
|
||||
"module": "accountSettings",
|
||||
"imports": [
|
||||
"App\\Models\\User",
|
||||
"Hypervel\\Support\\Facades\\Auth"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/FilesMainController.php": {
|
||||
"module": "files",
|
||||
"imports": [
|
||||
"App\\Models\\FileContent",
|
||||
"App\\Models\\FileList",
|
||||
"Hypervel\\Support\\Facades\\Storage"
|
||||
],
|
||||
"exports": []
|
||||
},
|
||||
"app/Http/Controllers/Pages/TransferMyCreditPageController.php": {
|
||||
"module": "transferCredits",
|
||||
"imports": [
|
||||
"App\\Models\\User",
|
||||
"Hypervel\\Support\\Facades\\Auth"
|
||||
],
|
||||
"exports": []
|
||||
}
|
||||
}
|
||||
748
ai-docs/dictionary.md
Normal file
748
ai-docs/dictionary.md
Normal file
@@ -0,0 +1,748 @@
|
||||
# Dictionary
|
||||
|
||||
## Database Standards
|
||||
|
||||
## Required Fields
|
||||
- **created_by**: Every table should have this field, linked to the user ID of the creator.
|
||||
- **updated_by**: Every table should have this field, linked to the user ID of the last updater.
|
||||
- **is_active**: A boolean field, typically set to `true` by default, used for soft-deactivation or status tracking.
|
||||
- **hashkey**: string (300), unique. A unique identifier for every record, typically generated by `App\Listeners\ModelSavingListener` using `Str::uuid() . Str::random(100)`.
|
||||
|
||||
## Audit Fields & Eloquent Events
|
||||
- **Audit Fields**: `created_by` and `updated_by` are critical for audit trails across all modules.
|
||||
- **ModelSavingListener**: `app/Listeners/ModelSavingListener.php` automatically populates `hashkey`, `created_by`, and `updated_by` for Eloquent models during the `Saving` event.
|
||||
- **Raw DB Bypass**: Using `DB::table(...)->insert()` or `update()` **bypasses** Eloquent events and listeners.
|
||||
- **Manual Requirement**: When using raw DB queries for performance (e.g., in `PosController.php`), you **MUST** manually include `created_by` or `updated_by` (using `Auth::id()`) and `updated_at`/`created_at` (using `now()`) in the data array to maintain data integrity and audit trails.
|
||||
|
||||
|
||||
|
||||
## UI Standards
|
||||
- **Buttons and Inputs**: Prefer `.rounded-pill` for a modern, premium look unless otherwise specified.
|
||||
- **Form Spacing**: Use `d-flex gap-2` or `mb-3` to ensure interactive elements are not too close, especially on mobile views.
|
||||
- **Form Controls**: Standardize on `form-control` and `form-select` with `rounded-pill`.
|
||||
- **Empty States**: When a list or dataset is empty, provide a centered, clear message (e.g., "No records found") and an icon (using `fad` or `fas` with `text-muted` and `opacity-2`) to guide the user.
|
||||
- **Avatars**: For user profile pictures, if a custom photo is not provided (`photourl`), use `https://ui-avatars.com/api/` with the user's name as a dynamic fallback. This ensures a premium, personalized look even without an uploaded image. Avoid relying on local static placeholders like `avatar.png`.
|
||||
- **Glassmorphism**: Use `backdrop-filter: blur(15px);` and `background-color: rgba(var(--bg-card-rgb), 0.7);` for floating headers and overlay components. A utility class `.glass-card` is available in `app.js` for consistent application.
|
||||
- **Dark Mode Surface Isolation**: For a modern "seamless" look, avoid forced backgrounds on navigation items and service buttons (`li`, `a`). Instead, use subtle drop-shadows and icon contrast filters (`brightness`, `contrast`) to maintain hierarchy.
|
||||
- **Scoped Dark Mode**: When fixing dark mode in Vue components with `<style scoped>`, use `:global(.dark-mode) .selector` to ensure styles are correctly applied when the `dark-mode` class is on the `body`.
|
||||
- **Theme Variables**: Use global CSS variables defined in `app.js` (e.g., `--bg-primary`, `--bg-card`, `--text-primary`, `--accent-color`) for consistent theme-aware styling.
|
||||
- **Administrative Icon Standards**: For high-density administrative dashboards (like `UltimateConsole.vue`), standardize on the following Font Awesome sizes for visual hierarchy:
|
||||
- **Navigation Icons**: `fa-lg` to provide better target recognition.
|
||||
- **Dashboard Stat Cards**: `fa-2x` to fill the visual container and provide focus.
|
||||
- **Primary Action Buttons**: `fa-lg` (e.g., `fas fa-paper-plane fa-lg`) within buttons for a premium feel.
|
||||
- **Main Dashboard Logo**: `fa-4x` for a bold, centered focus on primary consoles.
|
||||
- **Bulk Tool Icons**: `fa-2x` to accentuate the tool's category and provide clear differentiation.
|
||||
- **Table Actions**: `fa-lg` for critical row-level controls (Download, Delete, Edit).
|
||||
- **Dark Mode Scoped Fixes**: Use `:global(.dark-mode) .selector` or theme variables (e.g., `var(--bg-primary)`) to override hardcoded colors in Vue `<style scoped>` blocks. Remove `text-dark` and `bg-white` classes where theming is required.
|
||||
- **Ultimate Console Premium Header**: The administrative header uses a deep, multi-stop radial gradient (`#020617` base) with mesh highlights and intensified glassmorphism for stats pills (`badge-pill-premium`). This ensures a "Power User" feel that distinguishes administrative zones from standard user interfaces.
|
||||
- **Header Action Buttons**: Use `.btn-glass-premium` for auxiliary header actions (e.g., Refresh, Filter) within colored headers. This style uses backdrop blur and subtle transparency for a sophisticated, modern look without clashing with the header background.
|
||||
- **Responsive Table Actions**:
|
||||
- **Container**: Use `d-flex flex-wrap gap-1` instead of `btn-group` in table action columns to prevent horizontal scrolling.
|
||||
- **Text Labels**: Use `.d-none .d-lg-inline` for text labels within buttons (e.g., `<span class="d-none d-lg-inline">Manage</span>`) to save space on smaller screens while keeping the icon visible.
|
||||
- **Min Width**: Apply `style="min-width: 150px;"` (or appropriate value) to the container to ensure enough space for at least a few icons without excessive wrapping.
|
||||
- **Sticky Headers**: For configuration-heavy or high-density administrative pages (e.g., `SystemSettings.vue`, `UltimateConsole.vue`), use `sticky-top` on the primary header. This ensures critical actions like "Save" or "Back" remain accessible while scrolling through long forms. **CRITICAL**: Because the application uses a fixed `TopHeader`, you **MUST** provide an explicit `top` offset (e.g., `top: 66px;` or `top: var(--header-height, 66px);`) and a high `z-index` (e.g., `1020`) to ensure the sticky element sticks *below* the global navigation bar rather than being hidden behind it. Adjust sidebar `top` values accordingly (e.g., `13rem` to `18rem` depending on header height) to prevent overlap.
|
||||
|
||||
|
||||
## Layout & Responsiveness
|
||||
- **Max Width**: The application uses a dynamic maximum width for the main layout, controlled by the CSS variable `--layout-max-width`.
|
||||
- **Default Width**: `1440px` (Modern Desktop/Tablet Landscape).
|
||||
- **Mobile Behavior**: On screens smaller than the max width, the layout automatically scales to 100% width.
|
||||
- **Centering**: The `body` and fixed elements (`TopHeader`, `BottomNav`) are centered using `margin: 0 auto`.
|
||||
- **Full Width Mode**: The `.is-full-width` class on the `body` overrides the max-width to `none`, allowing components like the POS to utilize the entire screen.
|
||||
- **Implementation**: Managed globally in `resources/js/app.js` via an injected `<style>` tag, and supported by `:global` overrides in scoped components.
|
||||
|
||||
## Migration Standards (Hypervel/Hyperf)
|
||||
- **DO NOT use Illuminate classes** in migrations. This project runs on Hypervel (Hyperf + Swoole), not Laravel. Using `Illuminate\Database\Migrations\Migration`, `Illuminate\Database\Schema\Blueprint`, or `Illuminate\Support\Facades\Schema` will cause fatal class-not-found errors at runtime.
|
||||
- **Correct imports for all migrations**:
|
||||
```php
|
||||
use Hyperf\Database\Schema\Blueprint;
|
||||
use Hypervel\Database\Migrations\Migration;
|
||||
use Hypervel\Support\Facades\Schema;
|
||||
```
|
||||
- **Why**: Hypervel wraps Hyperf's database layer. The `Illuminate` namespace does not exist in this project. The Hypervel `Migration` class provides proper coroutine-context-aware connection resolution via `ApplicationContext`.
|
||||
- **Commands**: `php artisan migrate`, `php artisan migrate:rollback`, `php artisan migrate:fresh`, etc. work identically to Laravel.
|
||||
|
||||
# Development Requirements
|
||||
|
||||
## 1. API / Web Route Permissions
|
||||
- For every new function exposed through an API or web route:
|
||||
- Add a corresponding permission in `UserPermissions` and `UserActions`.
|
||||
- Ensure the permission is validated before granting access to the function.
|
||||
|
||||
### Route file scope (important)
|
||||
- **`routes/api.php`** is intentionally minimal. It only registers: `IndexController@index`, `RemoteLogoutController@logout`, and `Market\ActivityController` (`/activity/recent`, `/activity/search`).
|
||||
- **Products, users, stores, POS, cart, cooperatives, admin, settings, landing pages, etc. are ALL defined in `routes/web.php`** — not `api.php`. Despite paths like `/api/pos/...`, `/api/public/...`, or `/admin/users/list` appearing throughout the codebase, these are web routes (Inertia/session-auth based), not REST API routes.
|
||||
- When searching for product/user/store endpoints, grep `routes/web.php` first. Do not assume `/api/*` paths live in `routes/api.php`.
|
||||
|
||||
## 2. Vue Routing
|
||||
- For every new page:
|
||||
- Add a corresponding entry in `VueRouteMap`.
|
||||
- Ensure the route is properly registered and accessible via the application's routing system.
|
||||
- Pages are using vue not blade. Blade pages are legacy code and must not be modified but can be a source of investigation
|
||||
|
||||
- **Direct Page Access**: Pages such as **view** or **edit** must include one of the following in the URL:
|
||||
- `h=<HASH_KEY>`
|
||||
- `e=<ENCODED_PAYLOAD>`
|
||||
- URL Format: `/page-name--h:HASHKEY` or `/page-name--e:PAYLOAD`
|
||||
- **Product Context**: When a product is accessed from a store context (e.g., in `BuyViewProductMarket` or `ManageProductAdmin`), use an encoded payload (`--e:`) containing `product_hashkey` and `store_hashkey` to ensure context persistence and allow direct URL access.
|
||||
|
||||
- This ensures that:
|
||||
- Pages can be accessed directly via URL.
|
||||
- Functionality remains intact even without navigation state.
|
||||
|
||||
## POS Access Keys
|
||||
- **Terminal Name**: A descriptive name for a POS terminal (e.g., "Counter 1").
|
||||
- **Access Key**: A unique key used to authenticate a POS terminal without a traditional login.
|
||||
- **Target File**: `resources/js/Pages/PosAccessKeys.vue` - Main management interface for POS access keys.
|
||||
- **Store Hash**: The hashkey of the store associated with the access key.
|
||||
- **Store Name**: Displayed in `PosMain.vue`, retrieved from `posStore.activeSession.store.name` or `posStore.todayStats.store_name`.
|
||||
|
||||
## Sharing Protocols
|
||||
- **Web Share API**: Used for sharing content (text, URLs) via the device's native sharing dialog. Supported on most modern mobile browsers.
|
||||
|
||||
## Market Enums
|
||||
- **TransactionFlow**: Categorizes transactions into `INCOME` (1), `EXPENSE` (-1), or `NEUTRAL` (0).
|
||||
- **ProductTransactionType**: Defines specific transaction events (e.g., `ONLINE_SALE`, `PURCHASE`) which map to a `TransactionFlow`.
|
||||
- **Add Transaction Page**: `resources/js/Pages/AddTransaction.vue` - Interface for manually recording global or store-specific transactions.
|
||||
|
||||
## Customer Management
|
||||
- **Table**: `cst` (Customers)
|
||||
- **Key Fields**: `hashkey`, `name`, `store_id`, `user_id`.
|
||||
- **Suggestions**: Handled in `PosMain.vue` via `/api/pos/get-customers`. Employs a **Stale-While-Revalidate** pattern: immediate results from Pinia/LocalStorage cache while background fetching latest from the server.
|
||||
- **Session Lifecycle**: Transactions are completed in `confirmComplete`. Upon success, the session is reset, but the terminal identity (`pos_access_key` and captured `storeHash`) is preserved. A new session is then automatically started via `posStore.startNewSession` to ensure a seamless "customer-after-customer" checkout flow. To support this, `pos_sessions` allows multiple records per `pos_access_key` (removed unique constraint).
|
||||
- **Quick Select**: Displays the last 3 recently used customers as quick-pick buttons when the customer name input is empty/focused. This provides immediate selection without searching.
|
||||
- **Cache**: Persisted in `localStorage` under `pos_cached_customers`.
|
||||
|
||||
## Routing Architecture
|
||||
- **VueRouteMap**: `app/Http/Controllers/Support/VueRouteMap.php` - Centralized mapping for Vue components to routes.
|
||||
- **SPA Routing**: Catch-all route in `web.php` maps to `VueRouteMap::handleSpa()`.
|
||||
- **Navigation**: Uses `useNavigate` composable. Pagenames should match Vue component names (PascalCase).
|
||||
- **Initial State**: `app.js` initializes history state on load using `window.history.replaceState` to ensure back/forward navigation works for the entry page.
|
||||
- **Prop Extraction**: The backend automatically extracts hash/payload tokens and provides them as `target`, `hashkey`, and `id` props to the rendered Vue component.
|
||||
- **URL Flexibility**: The SPA router (`VueRouteMap::handleSpa`) is robust against hyphen and case discrepancies in the URL slug. It automatically matches unhyphenated slugs (e.g., `/viewstoremarket`) and misaligned casing to their correct components (e.g., `ViewStoreMarket`) defined in the map.
|
||||
- **Common Mappings**:
|
||||
- `/user-list` -> `UserList.vue`
|
||||
- `/manage-products` -> `ManageProductsAdmin.vue`
|
||||
- `/manage-product-admin` -> `ManageProductAdmin.vue`
|
||||
|
||||
## Missing/Future Modules
|
||||
- **Shipments**: `ShipmentList.vue` - For logistics and product tracking. Handled by `ShipmentController.php`.
|
||||
- **Farmer Management**: `FarmerProfileEdit.vue` - For registering farmer profiles. Handled by `FarmerController.php`.
|
||||
- **Verification Dashboard**: `VerificationDashboard.vue` - For admin review of farmer profiles. Handled by `FarmerController.php`.
|
||||
|
||||
## Groups & Associations
|
||||
- **Module**: `Groups`
|
||||
- **Tables**: `groups`, `group_members`
|
||||
- **Architecture**: A generic container for various member organizations.
|
||||
- **Group Types**: Dynamic (COOPERATIVE, ASSOCIATION, NGO, etc.) defined in `system_settings` under `group_types`.
|
||||
- **Key Fields**: `hashkey`, `name`, `type`, `description`.
|
||||
- **Membership**: Linked via `group_members` pivot table with `role` and `membership_type`.
|
||||
|
||||
- **Property Management**: `ListProperties.vue` - Interface for managing properties.
|
||||
- **Referrals**: `ListReferrals.vue` - Interface for managing property referrals and leads.
|
||||
- **Accounting**: Handles financial tracking and reporting.
|
||||
- **Tables**: `accounts`, `account_transactions`
|
||||
- **Controller**: `AccountingController.php`
|
||||
- **Pages**:
|
||||
- `AccountingDashboard.vue` (`/accounting-dashboard`) - Tree and Leaf views.
|
||||
- `ListReports.vue` (`/list-reports`) - Transaction list and summaries.
|
||||
- **Endpoints**:
|
||||
- `POST /admin/accounting/reports` - Fetches recent transactions.
|
||||
- `POST /admin/accounting/tree` - Hierarchical account tree.
|
||||
- `POST /admin/accounting/leaf` - Flat list of leaf accounts.
|
||||
|
||||
## Server Configuration Notes
|
||||
- **Nginx Headers**: Large JWT tokens and complex data trees (like accounting) require increased header and proxy buffers in Nginx.
|
||||
- `large_client_header_buffers 4 32k;`
|
||||
- `proxy_buffer_size 32k;`
|
||||
- `proxy_buffers 8 64k;`
|
||||
- **POST Requests**: Production environments (Nginx/WAF) may reject empty POST bodies with a 400 error. A global Axios interceptor in `app.js` ensures all POST requests carry at least an empty object `{}`.
|
||||
|
||||
## Shipment & Logistics
|
||||
- **Module**: `Shipment`
|
||||
- **Key Fields**: `hashkey`, `transaction_id`, `courier_id`, `status`, `tracking_number`.
|
||||
- **Statuses**: `PENDING`, `PICKED_UP`, `IN_TRANSIT`, `DELIVERED`, `FAILED`, `RETURNED`.
|
||||
- **Target Files**: `ai-docs/modules/shipment_module.md`
|
||||
|
||||
## Farmer Management
|
||||
- **Module**: `FarmerProfile`
|
||||
- **Key Fields**: `hashkey`, `user_id`, `organization_id`, `verification_status`.
|
||||
- **Verification Statuses**: `UNVERIFIED`, `PENDING`, `VERIFIED`, `REJECTED`.
|
||||
- **Target Files**: `ai-docs/modules/farmer_management_module.md`
|
||||
|
||||
## Redis Configuration
|
||||
- **Connection**: `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`.
|
||||
- **Authentication**: `REDIS_AUTH` or `REDIS_PASSWORD`.
|
||||
- **Support**: Empty passwords are supported by setting `REDIS_AUTH=` in `.env`. The configuration uses `(?: null)` logic to handle empty strings correctly.
|
||||
- **Config Files**: `config/database.php` and `config/autoload/redis.php`.
|
||||
|
||||
## Query Caching (Redis)
|
||||
- **Helper**: `App\Http\Controllers\Helpers\CacheHelper.php`
|
||||
- **Key Format**: `querycache:[queryhash]++++[parameterhash]`
|
||||
- **Methods**:
|
||||
- `get_cache($query, $params = [])`: Fetches from cache via `Cache` facade. Supports SQL strings or Builder objects.
|
||||
- `set_cache($query, $data, $params = [], $ttl = 86400)`: Sets in cache via `Cache` facade inside a **fire-and-forget** coroutine.
|
||||
- `erase_cache($query, $params = [])`: Deletes the specific cache key using `Cache::forget`.
|
||||
- **Implementation**: Uses `Hypervel\Support\Facades\Cache` for better integration with the application's caching layer.
|
||||
- **Async Logic**: `set_cache` uses `Hyperf\Coroutine\Coroutine::create` to return responses immediately without waiting for cache network I/O.
|
||||
- **TTL**: Default is 1 day (86400 seconds).
|
||||
|
||||
## User Settings & UI Persistence
|
||||
- **Composable**: `resources/js/composables/useUserSettings.js`
|
||||
- **Controller**: `app/Http/Controllers/Market/UserSettingsController.php`
|
||||
- **Frontend Sync**: Handled via `uiStore.syncSettings(settings)` which is called after `userStore.fetchCurrentUser()` in `app.js`.
|
||||
- **Purpose**: Dynamically store and retrieve user-specific UI preferences in the `users.settings` JSON column.
|
||||
- **Key Settings**:
|
||||
- `pos_layout`: `landscape` or `regular`. Controls the split-screen behavior in `PosMain.vue`.
|
||||
- `dark_mode`: Boolean. Globally toggles the application's dark theme by applying the `.dark-mode` class to the `body`.
|
||||
- `pos_sound_effects`: (Future) Boolean to toggle audible feedback during scanning.
|
||||
|
||||
## Global Theme & Dark Mode
|
||||
- **Class**: `.dark-mode` - Applied to the `<body>` element.
|
||||
- **Persistence**: Saved in the `users.settings` JSON column. Included in initial user data fetch via `AccountSettingsPageController@listDetails`.
|
||||
- **Implementation**: Handled via state in `uiStore` and a global reactive `watch` in `app.js` with `{ immediate: true }`.
|
||||
- **UI Toggle**: Located in `AccountSettings.vue`, calls `/UserSettings/Update`.
|
||||
- **Color Overrides**: Global text color overrides for dark mode are defined in `app.js` and apply to standard typography tags (`h1-h6`, `span`, `p`, `label`, `i`) and generic `div` elements, ensuring uniform color contrast across the application.
|
||||
|
||||
|
||||
## POS & Terminal Management (Advanced)
|
||||
- **"Start New Session" requires store context**: `startNewSessionSilently` (in `usePosSession.js`) guards against calling the API when both `storeHash` and `access_key` are absent. When neither is available (e.g. navigating to `/pos-main` without a URL hash or stored key), it sets `posStore.error` and returns false instead of hitting `/api/pos/start`. `PosController::startSession` now returns 422 (not 404) when no store can be resolved, with a descriptive message. `posStore.error` is surfaced in the cart area of `PosMain.vue`.
|
||||
- **Landscape Mode**: Optimized for tablet/desktop usage with a split product/cart view.
|
||||
- **QR/Barcode Scanner**: Integrated via `html5-qrcode` in `PosMain.vue`. Supports scanning product hashkeys, barcodes, or store-specific QR codes.
|
||||
- **Session Auto-Start & Access Keys**: POS sessions are automatically restarted after completion. To prevent authorization failures, the system distinguishes between permanent terminal access keys (`PK-` prefix, stored in `pos_access_keys`) and temporary session-specific random tokens. The frontend (`pos.js`) ensures only valid terminal keys are persisted to `localStorage` (via `PosMain.vue`), and the backend (`PosController.php`) now allows authenticated users to start a session by specifying a `store_hash` even if an invalid or unrecognized `access_key` is present in the request payload. This prevents 401 errors caused by session-specific tokens being used as terminal credentials.
|
||||
- **Clear All**: A "Clear All Items" feature is available in `PosMain.vue` and `pos.js` which performs a bulk deletion of all transactions in the current session via `/api/pos/clear`. This includes a database update and a session archive entry for auditing.
|
||||
|
||||
|
||||
### File Uploads
|
||||
- **Route**: `POST /File/Upload/{category}` → `FilesMainController::UploadFilefromRequest` ([app/Http/Controllers/FilesMainController.php](app/Http/Controllers/FilesMainController.php)).
|
||||
- **Permission scoping**: gated by `canUploadCategory($category)` — allows anyone with `UploadAllFiles` (ULTIMATE / SUPER_OPERATOR). For `ProductMarket`, also allows roles with `CreateProductForOwnStore` or `AddProducttoOwnStore` via `ProductPermissions::isActionAllowed` (covers STORE_OWNER, STORE_MANAGER, OPERATOR). All other categories require `UploadAllFiles`.
|
||||
- **Frontend**: `resources/js/composables/useFileUpload.js` posts to `/File/Upload/${category}`. The `CreateProductStoreOwner` wizard uses `category: 'ProductMarket'`.
|
||||
|
||||
### RBAC & Permissions Logic
|
||||
- **Core Controller**: `app/Http/Controllers/Helpers/Permissions/UserPermissions.php`
|
||||
- **Actions Enum**: `app/Enums/UserActions.php` (Currently defines 103 user actions).
|
||||
- **Default Permissions**: ULTIMATE users are automatically granted all actions defined in `UserActions::cases()`.
|
||||
- **Roles Method**: `UserPermissions::roles()` defines the default permission mapping per user type.
|
||||
- **Roles with No Target User**: `UserPermissions::$RoleswithNoTargetUser` is a collection for actions that don't target a specific user (e.g., `ViewAllUserTypes`, `ViewShipments`, `UltimateConsole`).
|
||||
- **User Types**: `ULTIMATE` (all access), `SUPER_OPERATOR` (most management), `OPERATOR` (limited operational access), `COORDINATOR`, `SUPPLIER_OVERSEER`, `WHOLESALE_BUYER`, `SUPPLIER`, `STORE_OWNER`, `STORE_MANAGER`, `USER`, `RIDER`, `AUDIT`, `POS_TERMINAL`.
|
||||
- **Allowed User Types (Creation Hierarchy)**: Defined in `UserPermissions::UserTypeService::getAllowedUserTypes()`.
|
||||
- **ULTIMATE**: Can manage ALL types.
|
||||
- **SUPER_OPERATOR**: management of all entities except ULTIMATE accounts. Includes global store and POS key management. Now includes `ViewAccountingReports` and `ManageAccounting` to allow access to the Accounting Dashboard.
|
||||
- **OPERATOR**: management of multiple stores, farmers, and logistics (Shipments/POS Reports). Now includes global store and product management, POS access keys, and accounting management (`ViewAccountingReports`, `ManageAccounting`).
|
||||
- **STORE_OWNER**: Can manage `STORE_MANAGER`, `RIDER`, `POS_TERMINAL`.
|
||||
- **RIDER**: Has no default management permissions.
|
||||
|
||||
## User Management
|
||||
- **Create User**:
|
||||
- **Validation**: All required fields (`mobile_number`, `username`, `name`, `fullname`, `type`, `password`) must show real-time validation status.
|
||||
- **Field Conflicts**: Conflicts (e.g., "Mobile number already taken" or "Username already taken") must be checked via API and displayed as inline error messages.
|
||||
- **Type Selection**: Filtered based on the current user's `acct_type` via `UserTypeService::getAllowedUserTypes()`.
|
||||
- **UX**: The "Create Account" button should remain visible but disabled until all requirements are met. A "Remaining Requirements" summary box should appear if the user has started filling the form but missed some fields.
|
||||
- **Table**: `users`
|
||||
- **Controller**: `app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php`
|
||||
- **Page**: `resources/js/Pages/CreateUser.vue`
|
||||
- **User List API**: `/admin/users/list` returns an object `{ success: true, users: [...] }` containing all visible descendants of the current user.
|
||||
|
||||
## Cooperative & User Profile
|
||||
- **UserInfo**: Detailed personal information for users (firstname, lastname, bank details, addresses). Linked to `users` via `user_id`.
|
||||
- **Table**: `user_infos`
|
||||
- **Edit Page**: `resources/js/Pages/UserInfoEdit.vue`
|
||||
- **Navigation Fix (April 2026)**: Accessing the profile without a `target` prop now defaults to the currently logged-in user's profile by fetching the hashkey from the Pinia store.
|
||||
- **Cooperative**: An organization of type `COOPERATIVE`. Users are linked via `cooperative_members`. Data is stored in the `organizations` table.
|
||||
- **Key Fields**: `registration_number` (CDA COR), `cin` (Cooperative Identification Number), `tin` (BIR TIN), `cooperative_type`, `cooperative_category`, `registration_date`, `contact_person`, `contact_number`, `contact_email`, `compliance_status`.
|
||||
- **Types**: Credit, Consumer, Producer, Marketing, Service, Multi-purpose, Advocacy, Agrarian Reform, Cooperative Bank, Dairy, Education, Electric, Financial Service, Fishermen, Health Service, Housing, Insurance, Transport, Water Service, Workers.
|
||||
- **Categories**: Micro (Assets <= P3M), Small (Assets > P3M to P15M), Medium (Assets > P15M to P100M), Large (Assets > P100M).
|
||||
- **Member Table**: `cooperative_members`
|
||||
- **List Page**: `resources/js/Pages/CooperativeList.vue`
|
||||
- **Create Page**: `resources/js/Pages/CreateCooperative.vue`
|
||||
- **Detail Page**: `resources/js/Pages/CooperativeDetail.vue`
|
||||
- **Model**: `app/Models/Market/Organization.php`
|
||||
- **Controller**: `app/Http/Controllers/Market/CooperativeController.php`
|
||||
|
||||
## Home Store Owner Dashboard
|
||||
- **Component**: `resources/js/Pages/Fragments/Home/HomeStoreOwner.vue` - Used by `Home.vue` for `STORE_OWNER` accounts.
|
||||
- **Balance Card Stats**: `transactions_today_no` (count) and `cash_flow_today_php` (signed `SUM(amount * flow)` for today, scoped to stores where the user is owner or in `store_managers`). Plus `my_stores_no` (count of stores owned by the current user).
|
||||
- **Backend**: Stats are added to `GET /home-data` (`routes/web.php`), scoped by store ownership for non-Big3.
|
||||
- **Buttons**: Create Store (opens a yes/no chooser modal — "Quick Create" calls `POST /Store/AutoCreate`, "Custom Create" navigates to `CreateStore.vue` with store-owner-only fields hidden), Import Products (`BatchAddProducts`), New Product (`CreateProductStoreOwner` — wizard-style flow, see below), My Products (`ManageProductsAdmin` — backend `listProducts_Admin` already scopes non-Big3 to products created by the user + descendants), POS Keys (`PosAccessKeys`).
|
||||
- **Store Owner Create Product Flow** (`CreateProductStoreOwner.vue`): 4-step wizard tailored for store owners (replaces `CreateProductUltimate` on the Store Owner home). On mount, fetches `/Admin/Stores/Selectable`; if empty, opens a `yesNoModal` directing the user to `CreateStore` and blocks. Step 1: live fuzzy search via `/Products/Admin/FuzzySearch`; user picks an existing global product, OR clicks "My product is not listed — Create new" to enter Step 2-new. Step 2 (existing): description override (per-store). Step 2 (new): full global product form (name/desc/category/subcategory/price/unit/available/barcode/photos). Step 3: multi-select stores from the selectable list. Step 4: per-store rows with editable `price` and `available` (defaults seeded from global). Submit: for "new" mode, first `POST /Products/Admin/New/` with `TargetStore` = first selected store (required because store owners lack `CreateProductGlobal`), then iterate every selected store and `POST /Products/AssignToStore/` passing `target`, `TargetStore`, `price`, `available`, `description`. **`AssignProductToOwnStore` now upserts the pivot** (`updateExistingPivot` if already attached, else `attach`) and accepts an optional `description` field written to the `prd_str` pivot.
|
||||
- **Auto-Create Store**: `StoreController@autoCreate` → `POST /Store/AutoCreate`. Quick path used by the Store Owner home's "Quick Create" choice. Creates a store with sensible defaults and self-assigns owner/manager + a `store_managers` row.
|
||||
- **CreateStore for STORE_OWNER**:
|
||||
- Hidden fields: Store Owner dropdown, Internal Remarks, Linked Cooperatives, Store Status. Owner is forced to the current user; status forced to `active`; remarks cleared.
|
||||
- Additional Store Managers list: only shown when descendants of the current user with `acct_type = STORE_MANAGER` exist. Backend (`StoreController@store`) re-validates that each submitted manager hashkey is both a descendant AND a STORE_MANAGER — anything else is silently dropped.
|
||||
- Self-assignment: STORE_OWNER is automatically inserted into `store_managers` for the new store, in addition to `owner_id`/`manager_id`.
|
||||
- Name uniqueness: `stores.name` is validated `unique` globally. Violations return 422 with message "A store with this name already exists. Please choose a different name."
|
||||
- RBAC: only Big3 + STORE_OWNER may hit `POST /Store/New`; STORE_MANAGER cannot create (only edit assigned stores).
|
||||
- **User list filter (`POST /admin/user/list/numbers/hash`)**: accepts an optional `type` (string or array of `acct_type` values, e.g. `"store manager"`) to restrict the descendants-only result set. Response now includes `acct_type` per row.
|
||||
- **Route Map Adjustments**: `/manage-stores`, `/batch-add-products`, and `/manage-products` now allow `store owner` (and `store manager` for `/manage-stores` and `/manage-products`) so the Store Owner home buttons land on accessible pages.
|
||||
- **`/create-product-store-owner` route**: mapped in `VueRouteMap.php` → `CreateProductStoreOwner`, allowed for `ult`, `super operator`, `operator`, `store owner`, `store manager`. Required so direct URL-bar access / page reload works (in-app navigation via `useNavigate` push-states this URL).
|
||||
- **Add Products to Store flow (`AddProductsToStore.vue`)**: two-step wizard at `/add-products-to-store--h:<store_hashkey>` (allowed for Big3 + `store owner` + `store manager`). Step 1: browse every active global product via `POST /Products/GlobalList` (`ProductController@listGlobalProductsForPicker` — returns all `is_active=true` products regardless of `created_by`, since picking a global product to list in your own store requires visibility into the full catalog). Multi-select card grid + free-text search over name/category/subcategory. Step 2: editable table of selected products with per-row `price` and `available`, seeded from the global defaults, plus a "Bulk apply" panel that sets every row's price or availability at once. Submit iterates `POST /Products/AssignToStore/` per row (existing upsert endpoint — see L266). Entry points: (1) `CreateStore.vue` after `POST /Store/New` success — navigates here with the new store's hashkey instead of `ListStores`; (2) `HomeStoreOwner.vue` Quick Create branch after `POST /Store/AutoCreate`; (3) `ManageStoresAdmin.vue` per-row green `+` "Add Products" action (visible when `canModifyStore(store)`).
|
||||
|
||||
## Home Ultimate Dashboard (Revised)
|
||||
- **Component**: `resources/js/Pages/Fragments/Home/HomeUltimate.vue`
|
||||
- **Style**: Maintains the "Classic" BukidBounty look with a single **ServiceButtonGrid** for primary services and a **SideTextButtonList** for secondary management actions.
|
||||
- **Icons**: Standardized to use micons (`/assets/micons/`) for better dark mode visibility and modern aesthetics for key services like Products (`box-seam.svg`), Verification (`patch-check.svg`), Cooperatives (`people.svg`), and Announce (`megaphone.svg`).
|
||||
- **Organization**:
|
||||
- **Primary Grid**: Contains the most visual and frequently used administrative tools (Users, Stores, products, Transactions, Reports, Market, Shipments, etc.).
|
||||
- **Management & Tools List**: Text-based list for specific onboarding tasks (New User/Store/Product) and personal/social features (Profile, Referrals, Properties, Credit Transfer).
|
||||
- **Data Integration**: Connected to live counts for Users, Active Stores, Pending Orders, and System Balance via the updated `/home-data` endpoint.
|
||||
|
||||
## Product Management
|
||||
- **Manage Product Admin**: `resources/js/Pages/ManageProductAdmin.vue` - Central console for managing a specific product's global and store-specific data.
|
||||
- **Update Product Modal**: `resources/js/Components/Market/UpdateProductModal.vue` - A unified modal interface for editing product details, including pricing overrides for specific stores.
|
||||
- **Create Product Flow** (`CreateProductUltimate.vue`): On submit, runs a fuzzy-name dedup check via `POST /Products/Admin/FuzzySearch` (`ProductController@fuzzySearchByName`, name-only LIKE + SOUNDEX + `similar_text` scoring, threshold ≥ 45). If matches exist, a modal lists them with "Import to Store" (calls `/Products/AssignToStore/` with the user's typed price/available). Otherwise — or after "None of these — Create new" — a store-picker modal always opens (per product spec) and the chosen store is sent as `TargetStore` to `/Products/Admin/New/`.
|
||||
- **`.bb-modal*` styles**: shared modal styles (`.bb-modal-backdrop`, `.bb-modal`, `.bb-modal-header/body/footer/close`) are defined as **scoped** styles inside `CreateProductUltimate.vue` and `BatchAddProducts.vue`. They are NOT global — any new page using `<div class="bb-modal-backdrop">` must duplicate the CSS in its own scoped block or the modal will render invisibly.
|
||||
- **Batch Add Products** (`BatchAddProducts.vue` → `POST /admin/batch/products` → `BatchController@batchCreateProducts`): Big-3 only. Top-of-page target-store picker (`/Admin/Stores/Selectable`). Each leaf is independent; default mode creates a new global product. "Pick existing" button per leaf opens a fuzzy-search modal (reuses `/Products/Admin/FuzzySearch`) to link a global product instead — if the search returns zero matches, the picker is closed and a `Warning` modal is shown. Payload per leaf: `source: 'new' | 'existing'`. For `existing`, only `product_hash`, `price`, `available`, `description` are sent; backend attaches the global product to the target store via the `prd_str` pivot, **falling back to the global's `price`/`description` when the leaf's value is blank or 0**. For `new`, the global product is created and — when a target store is picked — also attached to that store with the same price/stock/description. Pivot table `prd_str` supports `available`, `price`, `description`, `is_active` etc. (see `Store::products()` `withPivot`).
|
||||
- **Store-scoped product creation permission**: `UserActions::CreateProductForOwnStore` (granted to STORE_OWNER, STORE_MANAGER, OPERATOR, SUPER_OPERATOR). `ProductController@createNew_Admin` allows the request when the caller has either `CreateProductGlobal` (Big3) or `CreateProductForOwnStore` + a `TargetStore` they own/manage; the existing per-store ownership check at the top of `createNew_Admin` enforces the latter.
|
||||
- **Selectable Stores Endpoint**: `POST /Admin/Stores/Selectable` → `StoreController@listSelectableStoresForAddingProduct`. Non-Big3 query was widened from descendants-only to include self + descendants + `store_managers` pivot, so a fresh store owner sees their own store(s).
|
||||
- **Manage Listings Modal (`ManageProductsAdmin.vue` store icon)**: opens a per-product modal listing the user's selectable stores. For each store it shows a "Listed" / "Not listed" badge based on `POST /Products/AssignedStores/` (`ProductController@getAssignedStoresForProduct`, returns store hashkeys the product is attached to). Each row has a List or Unlist action; List runs the existing price/availability prompt → `POST /Products/AssignToStore/`; Unlist confirms then calls `POST /Products/UnassignFromStore/` → `ProductController@RemoveProductFromStore`. `RemoveProductFromStore` was relaxed from the Big3-only `RemoveProductfromAnyStore` permission to ownership-aware checks mirroring `AssignProductToOwnStore` (Big3, owner, legacy manager, store_managers pivot member, or descendant of any of those).
|
||||
- **Payload Keys**:
|
||||
- `product_hashkey`: Used in URL payloads to identify a specific product.
|
||||
- `store_hashkey`: Used in URL payloads to identify the store context for a product.
|
||||
|
||||
## Ultimate Console
|
||||
- **Component**: `resources/js/Pages/UltimateConsole.vue` - Powerful administrative interface for ULTIMATE accounts.
|
||||
- **Roles**: Access restricted via `ultimate` middleware and `UserActions::UltimateConsole`.
|
||||
- **Key Features**:
|
||||
- Raw SQL Query execution (`UltimateQueryModal.vue`).
|
||||
- System-wide maintenance mode toggle (Redis-backed).
|
||||
- Global broadcast messaging (Redis-backed).
|
||||
- Batch operations for users, transactions, and store management.
|
||||
- Database clearing/flushing for specific tables.
|
||||
- System statistics and artisan command execution.
|
||||
- **Backend**: `app/Http/Controllers/Market/UltimateController.php`
|
||||
- **Migrate Endpoint**: `POST /admin/ultimate/migrate` → `UltimateController@runMigrate`. Runs `php artisan migrate --force` (non-interactive, prod-safe). Auth: `auth` + `ultimate` middleware. Returns `{ success, output }`.
|
||||
- **Flush Endpoint**: `POST /admin/ultimate/flush` → `UltimateController@flushData`. Body `{ target }`. Valid targets: `transactions`, `pos_sessions`, `stores` (truncates `str` + `prd_str` + `store_managers`), `products` (truncates `prd_items` + `prd_str`), `cooperatives` (truncates `cooperative_*` child tables + deletes `organizations` where `type='COOPERATIVE'`), `carts` (truncates `cart_items` + `carts`), `farmer_profiles`, `cache` (Redis flushDB). UI lives in the "Dangerous Zone" tab via `flushOptions` in `UltimateConsole.vue`.
|
||||
- **Composable**: `resources/js/composables/useUltimate.js` - Contains methods like `getStats` (to retrieve system metrics), `toggleMaintenance`, `sendGlobalMessage`, and `runCommand` (for Artisan calls). Ensure methods are called exactly as defined (e.g., `getStats` instead of `fetchStats`).
|
||||
|
||||
## POS (Point of Sale)
|
||||
- **Main Interface**: `resources/js/Pages/PosMain.vue`
|
||||
- **Buttons**: In dark mode, quantity adjustment buttons (`btn-light`) and the delete button (`btn-soft-danger`) should use theme-aware glassmorphism or deep charcoal backgrounds to avoid a "stark white" or "quirky" appearance.
|
||||
- **Security**: Raw queries are restricted to `SELECT`, `SHOW`, and `DESCRIBE` for data retrieval; others use standard DB statements. Artisan commands are whitelisted.
|
||||
- **Dark Mode Overlays**: Global overlays such as the `.loading-overlay` in `application-layout.blade.php` must be dark-mode aware. Ensure `background: rgba(var(--bg-card-rgb), 0.7)` or themed variables are used instead of hardcoded white backgrounds (`rgba(255, 255, 255, 0.7)`).
|
||||
- **Hardcoded Backgrounds**: Avoid using `bg-white` or `bg-light` classes directly in Vue templates for components that should persist in dark mode (like modals in `PosMain.vue`). Use themed CSS variables or `:global(.dark-mode)` overrides.
|
||||
- **listProductsData store resolution**: Must use sequential `if (!$targetStore)` guards, NOT `if/elseif`. A stale `access_key` in localStorage (e.g., a revoked key or session-specific token) must not block the `store_hash`/`session_hash` fallbacks. Priority order: access_key → session_hash → store_hash. Bug pattern: using `elseif` causes all global products to be shown when access_key is present but unrecognized.
|
||||
- **Session Composable**: `resources/js/composables/Market/usePosSession.js` - Isolates POS initialization, session loading, and transaction completion logic. Use this in `PosMain.vue` and related components to ensure stable terminal identity and prevent logic fragmentation.
|
||||
- **Session Hashkey Integrity**: The backend `PosController.php` must NOT truncate hashkeys at 36 characters. The `hashkey` field is 300 characters and can contain long uniquely generated identifiers. Incorrect truncation leads to 404 "Session not found" errors.
|
||||
- **Performance Optimization**: To reduce perceived latency in local and production environments, the POS API uses **Request Consolidation**. The `addItem` and `removeItem` endpoints return the full current session object (with transactions and products) instead of just the single modified record. This eliminates the need for the frontend to make a second `loadSession` call, halving the total network delay for cart actions. Additionally, the backend implements **High-Efficiency SQL Paths**:
|
||||
- **Joined Lookups**: Product pricing and store overrides are fetched in a single combined query to reduce database round-trips.
|
||||
- **Aggregate Operations**: Session totals are calculated using database-side `SUM()` aggregates rather than memory-heavy PHP collection mapping.
|
||||
- **Lean Archiving**: The `PosSessionArchive` logic only captures raw session attributes (`getAttributes()`) rather than nested relationship trees, which significantly reduces JSON serialization overhead and database write bloat.
|
||||
- **Column Projection**: Eager loads are restricted to only necessary columns (`select(['id', 'hashkey', ...])`) to minimize the JSON payload size returned in the response.
|
||||
|
||||
|
||||
## User Notes
|
||||
- **Composable**: `resources/js/composables/useUserNotes.js`
|
||||
- **Controller**: `app/Http/Controllers/Pages/AccountSettingsPageController.php`
|
||||
- **Mechanics**: Fetches from `/user/note/content`. Also updated in real-time via SSE in `useSessionGuard.js`.
|
||||
- **Security**: Requires `web` and `auth` middleware to ensure session persistence and prevent 302 redirects to login on AJAX calls.
|
||||
- **Handling**: Frontend should strictly ignore HTML responses (which indicate redirects) to avoid displaying login page source code in the notes modal.
|
||||
|
||||
## Database Backups
|
||||
- **Table**: `db_backups`
|
||||
- **Model**: `App\Models\DbBackup`
|
||||
- **Key Fields**: `hashkey`, `file_content_hashkey`, `filename`, `size_in_bytes`.
|
||||
- **Logic**: Backups are generated as `.sql` files, compressed to `.7z` Ultra, and then stored as base64 in the `file_content` table. The `db_backups` table tracks these records. Temporary `.sql` and `.7z` files are deleted from the filesystem immediately after being saved to the database to prevent persistence on disk.
|
||||
- **Exclusion**: The `db_backups` table is excluded from the SQL dump to prevent recursive growth.
|
||||
- **UI**: Managed in `resources/js/Pages/UltimateConsole.vue` under the "Backups" tab.
|
||||
- **Controller**: `app/Http/Controllers/Market/UltimateController.php`
|
||||
|
||||
## POS History
|
||||
- **Component**: `resources/js/Pages/PosHistory.vue` - Standalone page for viewing a store's POS transaction history.
|
||||
- **Route**: `/pos-history--h:STORE_HASH` (Mapped in `VueRouteMap.php`).
|
||||
- **Logic**: Uses `PosHistoryList` component and `posStore` for paginated data retrieval.
|
||||
- **Navigation**: In `PosMain.vue`, clicking the "Today: X txns" count navigates to this page.
|
||||
- **Permissions**: Restricted to `ult`, `super operator`, `operator`, `store owner`, and `store manager`.
|
||||
|
||||
## Cart Module
|
||||
- **Table**: `carts`, `cart_items`
|
||||
- **Models**: `App\Models\Market\Cart`, `App\Models\Market\CartItem`
|
||||
- **Controller**: `App\Http\Controllers\Market\CartController`
|
||||
- **Page**: `resources/js/Pages/CartProductMarket.vue`
|
||||
- **Logic**: Users have a single active cart. Cart items are linked to products from `prd_items`.
|
||||
- **API Routes**: `/cart/get`, `/cart/add`, `/cart/update`, `/cart/remove`, `/cart/clear`.
|
||||
|
||||
## Global System Settings
|
||||
- **Model**: `App\Models\SystemSetting.php` - Stores key-value configurations with caching.
|
||||
- **Page**: `resources/js/Pages/SystemSettings.vue` - Admin interface for managing branding and system flags.
|
||||
- **Helper**: `App\Support\SystemSettingsHelper.php` - Facade-like access for Blade and Controllers.
|
||||
- **Branding**: Includes `app_name`, `app_logo`, `app_description`, `footer_text`, and `primary_color`.
|
||||
- **Dynamic Branding (Frontend)**:
|
||||
- **Pattern**: Use `uiStore.appName` in Vue templates instead of hardcoded strings like "BukidBounty".
|
||||
- **Components**: `Home.vue`, `Login.vue`, `AccountSettings.vue`, and `HomePublic.vue` use this pattern.
|
||||
- **Tab Title**: Automatically managed via `uiStore.pageTitle`, which defaults to `appName`.
|
||||
- **Inertia Shared Data**:
|
||||
- **Controller**: `App\Http\Controllers\Support\Inertia.php` - Automatically includes `systemSettings` in every `Inertia::render` call.
|
||||
- **Synchronization**: `app.js` populates the `uiStore` from `initialPage.props.systemSettings` immediately upon mounting. This eliminates the "flash" of default branding before the API background refresh completes.
|
||||
|
||||
## Common Modals & UI Components
|
||||
- **BaseModal**: `resources/js/Components/Core/BaseModal.vue` - Standardized modal structure with header, body, and footer slots. Supports `v-model` for visibility.
|
||||
- **ConfirmModal**: `resources/js/Components/Core/ConfirmModal.vue` - Specialized modal for action confirmations (Maintenance toggle, Backups, Flushes). Supports `danger`, `warning`, and `info` variants.
|
||||
- **UltimateQueryModal**: `resources/js/Components/Ultimate/UltimateQueryModal.vue` - Advanced SQL console for ULTIMATE accounts, refactored to use `BaseModal` and theme-aware styling.
|
||||
- **FileImage**: `resources/js/Components/Core/FileImage.vue` — emits a single `<img>` with `@error` fallback. The `:src` prop is forwarded as-is, so callers must pass a string URL. `photourl` from product endpoints may be `string | string[] | null`; passing `photourl[0]` when `photourl` is a string yields a 1-char URL that 404s. Use a safe accessor like `const firstPhoto = (v) => Array.isArray(v) ? (v[0] || '') : (v || '');` before binding.
|
||||
- **FileList::resolvedUrl()**: `app/Models/FileList.php` (table `file_list`) returns `cdn_url` if non-empty, else `/RequestData/File/{hashkey}`. `str.photourl` is a JSON array of `file_list` hashkeys — raw hashkeys are NOT renderable URLs. `StoreController@viewStoreDetails` resolves them into `$store->resolved_photos` (array of usable URLs, cdn_url preferred); `ViewStoreMarket.vue` reads `store.resolved_photos` for its avatar/banner/photo-gallery modal.
|
||||
- **CardSimple**: `resources/js/Components/Core/CardSimple.vue` — wrapper with `is-premium` defaulting to `true`, which adds a translucent background and a `translateY(-2px)` lift on hover. For non-interactive panels (page headers, search bars, info boxes), pass `:is-premium="false"` to disable the hover lift, otherwise the surrounding card jitters when the cursor moves over it.
|
||||
|
||||
## Cooperative Chapter Officer/Member Features
|
||||
- **User types**: `COOP_OFFICER` ('coop officer'), `COOP_MEMBER` ('coop member') in `app/Enums/UserTypes.php`. Home fragments: `resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue`, `HomeCoopMember.vue` (wired in `Home.vue`).
|
||||
- **Controller**: `app/Http/Controllers/Support/ChapterController.php` — methods `getOrgChart`, `getOfficerScope`, `memberSearch`, `assignOfficer`, `createChapter`, `publicGetChapter`, `publicRegisterToChapter`.
|
||||
- **Routes** (`routes/web.php`): authed `POST /Chapters/OrgChart`, `/Chapters/Officer/Scope`, `/Chapters/Members/Search`, `/Chapters/Officer/Assign`, `/Chapters/Create`; public `GET /api/public/chapter/{hkey}`, `POST /api/public/chapter/register`.
|
||||
- **Composable**: `resources/js/composables/useChapters.js` — `fetchOrgChart`, `fetchOfficerScope`, `searchMembers`, `assignOfficer`, `createChapter`.
|
||||
- **SPA pages** (VueRouteMap, module 'cooperatives'): `/chapter-org-chart`, `/coop-member-search`, `/create-coop-user`, `/assign-chapter-officer`, `/create-chapter`, `/register-chapter` (public; decodes base64 `payload` prop `{coop_hash,chapter_hash}`).
|
||||
- **CHILD_LEVELS**: national→region→province→[city|municipal]→barangay. `chapters.level` enum has NO 'municipal' (peers in code only). Level checks use `in_array($level,['city','municipal'])`.
|
||||
- **assignOfficer** MOVES a member (deactivate parent ChapterMember, create child officer row, upgrade COOP_MEMBER→COOP_OFFICER). **memberSearch** returns ONLY {name, role, chapter_name}. HomeCoopMember shows officer names + member count only (never fellow member names).
|
||||
- **home-data**: COOP_OFFICER/COOP_MEMBER branches set `$props['props']['chapter_info']` + stats `chapter_member_count`/`child_chapter_count`/`new_members_7d`.
|
||||
- **Duplicate-check endpoints**: authed `POST /admin/user/number/exists` (`mobile_number`), `POST /admin/user/username/exists` (`username`) → `{exists}`; public `POST /api/public/user/check-mobile`.
|
||||
|
||||
## Account Type Definitions (RBAC)
|
||||
- **ULTIMATE**: full system access, bypasses all hierarchy checks.
|
||||
- **SUPER_OPERATOR**: management of all entities except ULTIMATE accounts. Includes global store and POS key management.
|
||||
- **OPERATOR**: management of multiple stores, farmers, and logistics (Shipments/POS Reports). Now includes global store and product management, and POS access keys.
|
||||
- **COORDINATOR**: focused on logistics, organizations, and farm management.
|
||||
- **RIDER**: delivery and shipment status updates only; blocked from user management.
|
||||
- **POS_TERMINAL**: point-of-sale operations, viewing POS reports, and customer lists.
|
||||
- **AUDIT**: read-only view of all system data (Global Reports, Transactions, Accounting, User Lists).
|
||||
- **ANY_USER**: Standard system user with no administrative or logistics privileges.
|
||||
|
||||
## Page Access Controls
|
||||
- **Setting Key**: `disabled_pages` (stored as JSON in `system_settings`).
|
||||
- **Logic**: Enforced at both the backend (`VueRouteMap.php`) and the frontend navigation layer (`useNavigate.js`).
|
||||
- **Management**: Controlled via the "Page Controls" tab in `UltimateConsole.vue`.
|
||||
- **Backend API**: `SystemSettingsController@getPublicSettings` exposes the list to the frontend, and `SystemSettingsController@update` handles saving toggled states.
|
||||
- **Sync Mechanism**:
|
||||
- **Initial Load**: `app.js` calls `uiStore.refreshSettings()` on startup.
|
||||
- **Background Refresh**: `app.js` runs a background interval calling `uiStore.refreshSettings()` every 1 minute to stay in sync.
|
||||
- **Pre-navigation Refresh**: `useNavigate.js` automatically triggers a refresh if settings are "stale" (older than 5 minutes) before evaluating the access guard. **Known Issue**: This threshold may cause a delay when a page is enabled/disabled until a reload or the 5-minute window expires.
|
||||
- **Sync Strategy**: Admin tools (e.g., `UltimateConsole.vue`) should manually trigger `uiStore.refreshSettings()` or `syncSettings()` after updating `disabled_pages` to ensure immediate session-wide consistency.
|
||||
- **Case-Insensitivity**: All disabled page checks are case-insensitive to ensure reliable blocking regardless of component naming conventions.
|
||||
- **Affected Files**:
|
||||
- `resources/js/composables/Core/useNavigate.js` - Global navigation guard.
|
||||
- `resources/js/stores/ui.js` - Reactive store for `disabledPages` and `lastSynced` timestamp.
|
||||
- `app/Http/Controllers/Support/VueRouteMap.php` - Backend enforcement for direct URL access; implements `isUserAllowed` logic using `allowedUserTypes` key.
|
||||
- `app/Http/Controllers/Helpers/Permissions/UserPermissions.php` - Core RBAC engine mapping roles to `UserActions` enums.
|
||||
- `app/Http/Controllers/Admin/SystemSettingsController.php` - API bridge for public setting retrieval.
|
||||
## System vs Account Settings
|
||||
- **Account Settings**: `AccountSettings.vue` - User-specific profile and security settings (password, photo, basic info).
|
||||
- **Management**: Controlled via the "Page Controls" tab in `UltimateConsole.vue`.
|
||||
- **Global System Settings**: `SystemSettings.vue` - Administrative configuration for the entire platform (branding, maintenance, page controls).
|
||||
- **Navigation**: In `HomeUltimate.vue` and other navigation menus, "Global System Settings" must point to the `SystemSettings` pagename, while "My Personal Profile" or "Account" points to `AccountSettings`.
|
||||
|
||||
## User Additional Details
|
||||
- **Settings Field**: JSON column in `users` table.
|
||||
- **Cooperatives**: Stored in `settings.cooperatives` as an array of organization hashkeys.
|
||||
- **Composable**: `resources/js/composables/useUserAdditionalDetails.js`
|
||||
- **Controller**: `app/Http/Controllers/UserManagement/UserAdditionalDetailsController.php`
|
||||
- **Functionality**: Allows users to join/leave cooperatives via settings, and provides quick search by cooperative membership.
|
||||
- **Sync**: Automatically synced in `CooperativeController::joinCooperative` to maintain consistency between `cooperative_members` table and user settings.
|
||||
|
||||
## SSE & Real-time Integration
|
||||
- **Controller**: `app/Http/Controllers/Support/SSEController.php`
|
||||
- **Mechanism**: Uses `Hypervel\Coroutine\Parallel` for concurrent fetching of multiple data streams (Stats, Customers, Inventory, Settings) to minimize latency.
|
||||
- **Pulse Interval**: 5 seconds.
|
||||
- **Syncing Logic**:
|
||||
- **Customers**: Employs a **First-Fetch vs Delta** strategy. Upon initial connection (or first fetch), the top 20 most recent customers are pushed to the client to "preload" search suggestions. Subsequent pulses only push records with `updated_at > lastSync` (deltas).
|
||||
- **Products (Marketplace)**: Precaches the full active product list for the global marketplace on the **first fetch** (`isFirstFetch`). Subsequent ticks send detailed product updates (price, stock, descriptions) as `inventory_deltas`.
|
||||
- **Inventory**: Product inventory updates (stock/price changes) are pushed as deltas based on the `lastSync` timestamp across all stores owned/managed by the user.
|
||||
- **Frontend Integration**:
|
||||
- **Composable**: `resources/js/composables/Core/useSessionGuard.js` acts as the primary consumer and dispatches data to Pinia stores.
|
||||
- **Stores**: `pos.js` (`syncFromSSE`), `product.js` (`syncFromSSE`), and `ui.js` (`syncDisabledPages`) handle the reactive merge of incoming data into the UI state.
|
||||
- **Caching**: The `product.js` store maintains a `detailsCache` (Object/Map) of full product details to enable instant page transitions in the Marketplace and Product Detail views.
|
||||
|
||||
## Session Management & PWA
|
||||
- **Multi-tab Logout**: Synchronized via both SSE (`isloggedin: false` payload) and a `localStorage` `storage` event (`logout_event` key). When any tab logs out, it clears `localStorage` and sets `logout_event`, which triggers an immediate redirect to `/login` in all other open tabs via the `useSessionGuard` composable.
|
||||
- **SSE Connection**: Handled in `resources/js/composables/Core/useSessionGuard.js`. It maintains a persistent `EventSource` connection to `/sse/stream` for real-time updates and session monitoring. The backend `SSEController` explicitly sends an `isloggedin: false` message before closing the stream when a session is lost.
|
||||
- **Service Worker Registration**: Managed in `resources/js/app.js`. To ensure persistent SSE and background features, the service worker should NOT be unregistered upon landing on the login page; automatic unregistration logic in `Login.vue` was removed to prevent "service worker deleted" issues.
|
||||
- **Polling Fallback**: If SSE is unavailable, the system automatically falls back to a Web Worker (`session-guard-worker.js`) or main-thread interval polling to ensure session integrity.
|
||||
- **Observability**: Console logs with the prefix `[SessionGuard]` provide observability into connection status, version changes, and logout events.
|
||||
- **Session Locking (SSE)**: Long-running SSE stream loops in PHP can lock the session associated with the request by default. This prevents concurrent requests from the same session (like logout, login, or any other middleware-protected route) from processing until the SSE thread yields. To avoid this "blocking" behavior, call `session()->save()` at the start of the loop in `SSEController` to release the lock immediately.
|
||||
|
||||
## User Management
|
||||
- **Create User**: `resources/js/Pages/CreateUser.vue` - Main interface for creating new users.
|
||||
- **Controller**: `app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php`
|
||||
- **Required Fields**:
|
||||
- `mobile_number`: Unique, format `09XXXXXXXXX`.
|
||||
- `username`: Unique, required for all users.
|
||||
- `name`: Required (Display Name).
|
||||
- `password`: Min 6 characters.
|
||||
- `type`: User Type (via `UserTypes` enum).
|
||||
- **Validation**: When the user clicks "Create User Account", a modal appears if any required fields are missing or invalid, listing them clearly with badges and a description of the requirements. This replaces the previous inline badge system.
|
||||
- **Hierarchy**: Users can be assigned a `parentuid` (parent) to create a multi-level marketing or organizational hierarchy. The `listAllUsersforParentSelectHTML` method in the controller filters the parent list to only show the current user and their descendants, ensuring data isolation.
|
||||
|
||||
## Build & Deployment Standards
|
||||
- **Requirement**: After any changes to the frontend (Vue components, assets) or backend logic, run the build and restart process to ensure the latest changes are active.
|
||||
- **Build Process**:
|
||||
- Primary: `npm run build` (on host, if node/npm are installed).
|
||||
- Containerized Fallback: If `npm` is missing on the host, use a specialized Node container to build directly into the host filesystem:
|
||||
`docker run --rm -v "$(pwd)":/var/app -w /var/app node:latest /bin/sh -c "npm install && npm run build"`
|
||||
- **Vite Version**: The project uses **Vite 7**, which requires **Node.js >= 22.12.0** or **>= 20.19.0**.
|
||||
- **Container Restart**: After building assets, restart the main application container to refresh the Swoole service and pick up the new build:
|
||||
`docker restart bukidbountyapp`
|
||||
- **Volume Mapping**: The application container mounts the current directory to `/var/app`, so build assets generated on the host are immediately visible to the container upon restart.
|
||||
- **Branch Synchronization**: The `main` branch is periodically updated to match the `experimental` branch to promote stable experimental features to production. This is done via `git checkout main && git reset --hard experimental`.
|
||||
|
||||
## RBAC & Session Hardening Best Practices
|
||||
- **User Type Listing (Dropdown Fix)**: The `CreateUserControllerUltimate@listAllUserTypesforSelectHTML` method must handle `UserTypes` enums directly. Avoid using `UserTypes::from($currentUser->acct_type)` if the `acct_type` attribute is already cast to the enum in the `User` model, as this will trigger a `TypeError`.
|
||||
- **SPA Route Protection**: All sensitive management pages (e.g., `/create-user`) **MUST** be explicitly defined in `App\Http\Controllers\Support\VueRouteMap::$routes` with an `allowedUserTypes` array. If a route is not mapped, it defaults to allowing any authenticated user via the `handleSpa` catch-all, which creates a security vulnerability.
|
||||
- **Session State Isolation**: To prevent "Role Leaking" (where a new user sees the previous user's dashboard fragments or cached roles), the `Login.vue` component must execute `sessionStorage.clear()` within its `onMounted` hook. This ensures a clean slate for every new authentication attempt.
|
||||
- **Dashboard Fragmentation**: Dashboard fragments in `resources/js/Pages/Fragments/Home/` (e.g., `HomeShared.vue`, `HomeOperator.vue`) should implement `computed` filtering for action buttons based on `UserTypes` to ensure that unauthorized actions are not visible in the UI, even if the user has navigated to the dashboard correctly.
|
||||
|
||||
## Expanded Member Profile (Cooperative)
|
||||
- **Table**: `user_infos`
|
||||
- **Personal Details**: `firstname`, `middlename`, `lastname`, `suffix`, `gender`, `dob`.
|
||||
- **Dynamic Context**: `priority_sector` (links to `priority_sectors` in system settings).
|
||||
- **Social Accounts**: `messenger_id`, `viber_number`, `tiktok_username`.
|
||||
- **Address Components**: `region`, `province`, `city`, `barangay`. Detailed address (House No, Street, Zip, Country) is stored in the `addresses` JSON column.
|
||||
- **Family & Education**: `civil_status`, `children_count`, `dependent_count`, `education_level`, `course`, `school`, `year_last_attended`.
|
||||
- **Employment & Livelihood**: `livelihood_source`, `last_company`, `employer_name`, `last_position`, `occupation`, `last_employment_year`.
|
||||
- **Financial Details**: `monthly_income` (decimal), `bank_account_no` (string).
|
||||
- **Government Info**: `tin`, `philhealth_id`, `gov_id`, `id_type`, `id_number`, `beneficiary_type`.
|
||||
- **Emergency Contact**: `emergency_contact_name`, `emergency_contact_address`, `emergency_contact_phone`, `emergency_contact_relation`, `emergency_contact_user_id`.
|
||||
- **Linking Logic**: The system automatically links emergency contacts by matching their phone number with an existing user's `mobile_number` in `UserInfoController@updateUserInfo`.
|
||||
- **Virtual Attributes**: `age` (calculated on the fly from `dob`).
|
||||
|
||||
## Public Cooperative Registration
|
||||
- **Route (Frontend)**: `/register-coop--h:HASHKEY` → `RegisterCoop.vue` (`loginRequired: false`).
|
||||
- **Public API**: `GET /api/public/cooperative/{hkey}` returns coop info (no auth). `POST /api/public/cooperative/register` creates a new USER account and registers them as a MEMBER.
|
||||
- **Controller Methods**: `CooperativeController@publicGetCooperative` and `CooperativeController@publicRegisterMember`.
|
||||
- **Parent Assignment**: Uses the cooperative's `created_by` user as parent, falling back to first COORDINATOR, then first user.
|
||||
- **Share Button**: In `CooperativeDetail.vue` → "Share Register Link" button uses the Web Share API on mobile (native dialog) and falls back to clipboard copy on desktop.
|
||||
|
||||
## Cooperative Membership Details
|
||||
- **Table**: `cooperative_members`
|
||||
- **Key Fields**: `membership_type`, `membership_level`, `officer_position`, `officer_level`, `concurrent_position`, `concurrent_level`, `cooperative_name_alt`, `cooperative_position`, `year_beginning`.
|
||||
- **Management**: Membership details can be updated via `/Cooperatives/Member/Update`.
|
||||
- **Self-Service Registration**: Users can register themselves as members via the `CooperativeMemberRegister.vue` page (route: `/cooperative-member-register--h:COOP_HASH`).
|
||||
- **Registration Logic**: Handled by `CooperativeController@registerMember`. It automatically links the user to the organization and updates the `settings.cooperatives` array in the `users` table for session/preference synchronization.
|
||||
- **Access Control**: Controlled by `UserActions::JoinCooperative`. Standard users, operators, and coordinators have this permission by default.
|
||||
- **UI Integration**: A "Register as Member" button appears on the `CooperativeDetail.vue` page if the user is not yet a member. After registration, the button is hidden and the user appears in the members list.
|
||||
|
||||
## RBAC & Hierarchy Concepts
|
||||
- **Big 3**: Refers to `ULTIMATE`, `SUPER_OPERATOR`, and `OPERATOR` account types. They have global management privileges, bypass standard organizational hierarchy checks, and are the only roles permitted to edit global product details (categories, base prices, etc.) in `ManageProductAdmin.vue`.
|
||||
- **Hierarchy Management**: Management actions (Listing, Viewing, Editing) for stores and users are typically restricted to direct or indirect parents (ancestors). "Big 3" roles are immune to this restriction when accessing stores.
|
||||
- **Store Managers**: Users assigned via the `store_managers` table who are granted delegated management access to a specific store.
|
||||
- **Multi-Manager System**: Replaces the legacy single-manager field (`manager_id`) with a searchable, multi-select interface in `CreateStore.vue` and `EditStoreUltimate.vue`, allowing multiple users to be assigned as managers for a single store.
|
||||
- **Context-Aware Product Locking**: A UI pattern in `ManageProductAdmin.vue` that hides or disables global product fields for non-"Big 3" users, while still allowing them to manage store-specific overrides if they are an owner or manager.
|
||||
|
||||
## POS Access Control (Hardened)
|
||||
- **Helper**: `UserPermissions::isUserAllowedAccessToStore($user, $store)` - Centralized authorization check for POS/Reports access.
|
||||
- **Logic**:
|
||||
- `ULTIMATE` users have global access.
|
||||
- Owners and Managers of the store have access.
|
||||
- Any ancestor of the Owner or Manager has access.
|
||||
- Children (e.g., `POS_TERMINAL`, `RIDER`) of the Owner or Manager have access.
|
||||
- **Enforcement**: Applied to `PosController` methods: `startSession`, `getSession`, `getPosSessions`, `getTodayStats`, and `getCustomers`.
|
||||
- **Ancillary Helper**: `UserPermissions::isAncestorOf($ancestor, $descendant)` - Recursively checks user hierarchy via `parentuid`.
|
||||
|
||||
## UI Audit Results (April 2026)
|
||||
- **Navigation**: "My Personal Profile" now explicitly passes the current user's hashkey in HomeUltimate.vue.
|
||||
- **Connectivity**: SSE connection refused (ERR_CONNECTION_REFUSED) is a known issue being investigated at the container/proxy level.
|
||||
- **Functional Checks**:
|
||||
- **UserInfoEdit**: Fixed hanging by ensuring loader always closes and defaults to current user.
|
||||
- **Create/Manage Store**: Confirmed functional with correct route mapping.
|
||||
- **Ultimate Console**: Operational with full monitoring and setting controls.
|
||||
## Remote Development & Tunneling
|
||||
- **Script**: `scripts/tunnel.sh` - Standardized script for creating a Cloudflare Tunnel to `localhost:9522`.
|
||||
- **Port**: `9522` (Host) maps to `9501` (Container) for the `bukidapp` service.
|
||||
- **Usage**:
|
||||
- Temporary URL: `./scripts/tunnel.sh` (generates a `*.trycloudflare.com` link).
|
||||
- Persistent Tunnel: `./scripts/tunnel.sh <YOUR_TUNNEL_TOKEN>`.
|
||||
- **Implementation**: Uses Docker `cloudflare/cloudflared` with `--network host` to expose the local development port online for testing and external access.
|
||||
|
||||
## Dokploy Deployment
|
||||
- **Guide**: `dokploy-deployment-guide.md` (repo root) — Full step-by-step deployment guide.
|
||||
- **Network**: All services (app, MySQL, DragonflyDB) must join `dokploy-network` (external: true) for DNS resolution.
|
||||
- **Hostname Resolution**:
|
||||
- **Swarm services** (Dokploy-managed databases like MySQL): Use the **service name** as hostname (e.g., `prodservers-prodsqlmain-kv3yph`). Find via `docker service ls`.
|
||||
- **Compose services** (like DragonflyDB): Use the **full container name** as hostname (e.g., `prodservers-dragonflydb-rnfaje-dragonflydb-1`). Find via `docker ps --format '{{.Names}}'`.
|
||||
- **Common Error — Redis DNS Lookup Failed**: Caused by `REDIS_HOST` pointing to a container name that is not on the same Docker network. Fix by adding `dokploy-network` to the DragonflyDB compose and using the correct container name.
|
||||
- **Port Exposure**: Do NOT use host port mapping (e.g., `9522:9501`) in `docker-compose.yml` for Dokploy. Expose only the internal port (`9501`); Traefik handles external routing via auto-injected labels.
|
||||
- **Variants**: Multiple app instances (prod, beta, test) can share the same MySQL server and DragonflyDB by using different `DB_DATABASE` names and `REDIS_DB` numbers (0–15).
|
||||
|
||||
## Landing Page Management
|
||||
- **Table**: `landing_pages`
|
||||
- **Model**: `App\Models\LandingPage`
|
||||
- **Key Fields**: `title`, `html_content`, `description`, `hashkey`, `is_active`.
|
||||
- **Active Constraint**: Only one landing page can be `is_active=true` at a time, enforced at the application level via `LandingPage::setAsActive()`.
|
||||
- **Controller**: `App\Http\Controllers\Admin\LandingPageController`
|
||||
- **Editor Page**: `resources/js/Pages/LandingPageEditor.vue` — Full CRUD interface with HTML editor, live preview, template snippets (hero/features/CTA), and card-based gallery view.
|
||||
- **Guest Rendering**: `HomePublic.vue` fetches the active landing page via `/api/public/landing-page` and renders the HTML for unauthenticated visitors. Falls back to the default homepage if no active landing page exists.
|
||||
- **Access Control**: Landing page management is restricted to `ULTIMATE`, `SUPER_OPERATOR`, and `COORDINATOR` roles.
|
||||
- **RBAC**: `UserActions::ManageLandingPages` permission added to `$RoleswithNoTargetUser`.
|
||||
- **Route Map**: Registered as `LandingPageEditor` in `VueRouteMap.php`.
|
||||
- **Public API**: `GET /api/public/landing-page` (no auth) returns the active landing page HTML content.
|
||||
- **Admin APIs**:
|
||||
- `POST /admin/landing-pages/list` — List all landing pages.
|
||||
- `POST /admin/landing-pages/show` — Get a single landing page by hashkey.
|
||||
- `POST /admin/landing-pages/store` — Create or update (if hashkey provided).
|
||||
- `POST /admin/landing-pages/set-active` — Set a page as active (deactivates all others).
|
||||
- `POST /admin/landing-pages/deactivate-all` — Deactivate all pages (revert to default homepage).
|
||||
- `POST /admin/landing-pages/delete` — Delete a landing page.
|
||||
|
||||
## CDN Asset Pipeline (jsDelivr / obj-vault-3a)
|
||||
- **CDN Repo**: `telemagnadon/obj-vault-3a` on GitHub, served via jsDelivr.
|
||||
- **Pinned Tag**: Configured in `config/cdn.php` (`base`) and `public/sw.js` (`CDN_SHA`). Current: `v2026.05.14-vendor` (SHA `927de981da85`).
|
||||
- **Asset Path Convention**: `a/<sha256-12char>.<ext>`. Extension is `.bin` by default to obscure content type. Whitelist that **preserves original extension**: `.css`, `.js`, `.mjs`, `.json`, `.svg`.
|
||||
- **SVG Critical**: jsDelivr serves all files with `Content-Type: application/octet-stream` + `X-Content-Type-Options: nosniff`. Browsers will render PNG/JPG inside `<img>` from octet-stream (image decoder sniffs bytes anyway), but **SVG requires `image/svg+xml`** — `<img src="*.bin">` containing SVG markup silently fails to render. Therefore SVGs MUST be stored as `a/<hash>.svg` so jsDelivr serves them with the correct MIME.
|
||||
- **Pipeline Skill**: `cdn-asset-pipeline` (in `~/.claude/skills/`) handles sync. After updating extension whitelist, all SVG `.bin` files must be re-synced to `.svg` and orphaned `.bin` files removed.
|
||||
- **CSS Relative URLs Break on CDN**: CSS files served from CDN use the flat `a/<hash>.<ext>` path, so any relative `url(...)` inside them (e.g. `@font-face src: url('icomoon.ttf')`) resolves to `a/icomoon.ttf` and 404s. **Fix**: rewrite any internal asset references in CSS to absolute jsDelivr URLs pointing at their published hashed `.bin`/`.svg`. Known offender: `public/assets/vendor/dist/alt-theme/icons-alipay.css` (icomoon @font-face). When breaking this, top-bar back/filter icons and bottom-nav home icon (any `.icon-*` class from icomoon) silently render blank.
|
||||
- **Icomoon Font Path**: `public/assets/vendor/dist/alt-theme/icomoon.ttf` → CDN `a/c99ad0580805.bin`. Reference absolutely in `icons-alipay.css` so font loads regardless of where the CSS is hosted.
|
||||
- **Icomoon Font Broken on jsDelivr (May 2026)**: The `.ttf` is published as `a/c99ad0580805.bin` and served as `application/octet-stream` with `X-Content-Type-Options: nosniff`. Chrome/Firefox refuse to use it as a font, silently breaking every `.icon-*` class. Same MIME-sniff problem the SVG migration solved. **Resolution**: don't use `icomoon` `.icon-*` classes in Vue components — use Font Awesome (`fas fa-*`, already loaded). Mappings used: `icon-home → fa-home`, `icon-left → fa-chevron-left`, `icon-right → fa-chevron-right`, `icon-copy2 → fa-copy`, `icon-filter → fa-sliders-h`. If icomoon is ever needed again, either add `.ttf/.woff/.woff2/.eot/.otf` to the CDN pipeline's preserve-extension whitelist and re-publish, or self-host the font under `/public` so Nginx sets the correct MIME.
|
||||
|
||||
## Service Worker & Caching
|
||||
- **Hashed Assets**: Files under `/build/assets/` (generated by Vite) include hashes in their names.
|
||||
- **Caching Strategy**: These files use a **Cache-First** strategy since their content is immutable for a given filename.
|
||||
- **Expiration**: Cached for 6 months (`180 * 24 * 60 * 60` seconds) with a maximum of 1000 entries.
|
||||
- **Implementation**: Configured in `public/sw.js` via Workbox `registerRoute`.
|
||||
|
||||
|
||||
## Infrastructure & Docker
|
||||
- **Main Service**: `bukidapp` - PHP Swoole/Hypervel application service.
|
||||
- **Container Name**: Usually `bukidbountyapp-bukidapp-1` (or `bukidbountyapp-nginx-1` for the entry point).
|
||||
- **Port**: Internal `9501`, exposed to the host/nginx.
|
||||
- **Reverse Proxy**: Nginx acts as the entry point and reverse proxy, configured in `docker/nginx/default.conf`.
|
||||
- **Static Asset Delivery**: Nginx serves files from `/public` directly (CSS, JS, images) to minimize latency and bypass the PHP/Swoole stack for static requests.
|
||||
- **Docker Compose**: Managed via `docker-compose-local.yml` (development) and `docker-compose.yml` (production).
|
||||
- **Service Dependency**: The `nginx` service depends on `bukidapp`.
|
||||
- **Networking**:
|
||||
- Uses `dokploy-network` (external) for Traefik routing and cross-service communication.
|
||||
- Uses `app-internal` (bridge) as an **isolated network** for nginx→bukidapp communication to avoid DNS collisions.
|
||||
- **DNS Collision Warning**: On `dokploy-network`, multiple compose projects may share the same service name `bukidapp`. Nginx must NOT resolve `bukidapp` directly on this shared network. Instead, use a unique network alias (`swoole-upstream`) on the `app-internal` network.
|
||||
- **Upstream Alias**: The `bukidapp` service declares `aliases: [swoole-upstream]` on `app-internal`, and `docker/nginx/default.conf` targets `server swoole-upstream:9501`.
|
||||
- **Priority Sectors**: Stored as a JSON array in `system_settings` under the key `priority_sectors`. Defines groups eligible for specific assistance and priority (e.g., FISHERMEN, SENIOR, PWD). Managed via the Global System Settings editor.
|
||||
- **Group Types**: Stored as a JSON array in `system_settings` under the key `group_types`. Defines the categories of organizations (e.g., COOPERATIVE, ASSOCIATION).
|
||||
- **Address Fields**: Stored as a JSON array in `system_settings` under the key `default_address_fields`. Defines the expected keys in the `user_infos.addresses` JSON (e.g., `house_no`, `street`, `zipcode`).
|
||||
- **JSON Setting Editor**: A dynamic editor in `SystemSettings.vue` that supports array-based configurations with tag/badge management.
|
||||
|
||||
- **Dokploy Traefik Routing**:
|
||||
- Dokploy auto-injects Traefik labels on the first service by default.
|
||||
- **Domain Target**: In Dokploy UI → Domains tab, the service name MUST be set to `nginx` (not `bukidapp`), so Traefik routes `domain → nginx:80 → bukidapp:9501`.
|
||||
- Traefik uses Docker labels (not DNS) to find the target container, so the nginx service MUST be on `dokploy-network` for Traefik to reach it.
|
||||
- **Local vs Production Volumes**:
|
||||
- **Local (`docker-compose-local.yml`)**: Uses bind mount `.:/var/app` for live code reloading during development. Nginx mounts `./public:/var/app/public` directly.
|
||||
- **Production (`docker-compose.yml`)**: Does **NOT** use bind mounts on `bukidapp`. The image is self-contained — `composer install` and `npm install` run during `docker build`, and the resulting `vendor/` and `node_modules/` are baked into the image.
|
||||
- **Shared Build Volume (Production)**: A named volume `public_build` shares Vite-built assets from bukidapp to nginx. bukidapp copies `/var/app/public/` to `/var/app/public-shared/` (the shared volume) on startup via the command `sh -c "cp -r /var/app/public/. /var/app/public-shared/ && exec php artisan serve"`. Nginx mounts this volume at `/var/app/public`.
|
||||
- **Why not bind mount in prod?**: The host's `./public` directory (from the repo clone) does NOT contain Vite build output — those are generated inside the bukidapp container during `docker build`. A bind mount would hide the built assets.
|
||||
- **Dockerignore**: `.dockerignore` excludes `vendor/`, `node_modules/`, `.git`, and `.env*` to prevent host directories from overwriting dependencies installed during image build via `COPY . .`.
|
||||
|
||||
|
||||
## Module Enable/Disable System
|
||||
- **Purpose**: Allows enabling or disabling entire application modules (POS, Products, Accounting, etc.) via environment variables without code changes.
|
||||
- **Config**: `config/modules.php` - Central registry of modules and their associated `MODULE_<NAME>_ENABLED` environment variables.
|
||||
- **Master Toggle**: `MODULES_SYSTEM_ENABLED` - If set to `false`, all module-specific checks are bypassed (everything is enabled). Defaults to `true`.
|
||||
- **Helper**: `App\Support\ModuleHelper::isEnabled($moduleKey)` - Used to check module status in code.
|
||||
- **Middleware**: `module:<key>` (e.g., `module:pos`) - Applied to routes in `web.php` to block API/web access to disabled modules with a 403 response.
|
||||
- **SPA Routing**: `VueRouteMap.php` - Each SPA route can be mapped to a module using the `'module'` key. The router (`handleSpa` and `registerRoutes`) automatically blocks access and redirects to `/` if the module is disabled.
|
||||
- **Frontend Sync**: Module states are exposed via `SystemSettingsController@getPublicSettings` under the `module_states` key. This allows the frontend to reactively hide navigation links or UI elements for disabled modules.
|
||||
|
||||
## QRPH / EMV Payment QR
|
||||
- **Standard**: QRPH is the Philippine QR payment standard based on EMVCo Merchant-Presented QR Code Specification (TLV encoding: 2-char tag + 2-char decimal length + value).
|
||||
- **Decoder**: `app/Http/Controllers/Helpers/QrphDecoder.php` — parses raw QRPH strings into merchant name, network, account, validity (CRC-16/CCITT-FALSE), initiation method, currency.
|
||||
- **Storage**: QRPH payment code is stored in `system_settings` under key `qrph_payment_code`.
|
||||
- **Action**: `UserActions::ManageQrphPaymentCode` (`manageqrphpaymentcode`) — ULTIMATE-only via route middleware; listed in `$RoleswithNoTargetUser`.
|
||||
- **Routes**:
|
||||
- `POST /Financial/Qrph/Get` — any authenticated user; returns raw string + decoded info for top-up display.
|
||||
- `POST /Financial/Qrph/Set` — `ultimate` middleware; saves QRPH code to system_settings.
|
||||
- `POST /Financial/Qrph/Decode` — `ultimate` middleware; decode-only preview (no save).
|
||||
- **JS Composable**: `resources/js/composables/useQrph.js` — `parseTlv`, `injectAmount` (inserts EMV field 54 + recalculates CRC-16/CCITT-FALSE), `generateQrDataUrl` (renders QR via `qrcode` npm library — no external API).
|
||||
- **Amount injection**: In `MyWallet.vue` QRPH tab, selecting an amount rebuilds the QRPH string client-side with the amount embedded, then renders a new QR — member's banking app (GoTyme, GCash, Maya, etc.) auto-fills the amount.
|
||||
- **GoTyme / InstaPay GUIDs**: `QrphDecoder.php` recognises `com.gotyme`, `ph.ppmi`, `ph.bsp.qrph`, `com.p2pqrph` (and all major PH banks). Raw GUID is shown as fallback for unrecognised networks.
|
||||
- **Top-Up UI**: `MyWallet.vue` top-up modal "Scan to Pay" tab: generates QR client-side with optional amount embedded; falls back to stored static image if no amount set.
|
||||
- **Admin UI**: `AccountSettings.vue` shows a "Payment QR Code (QRPH)" card only for ULTIMATE accounts; drop-zone decodes QR image locally via `Html5Qrcode.scanFile`, uploads the image file, and shows Static/Dynamic badge.
|
||||
|
||||
## Known Bug Patterns & Fixes
|
||||
|
||||
### RBAC — Action Permission vs Page Access Mismatch
|
||||
- VueRouteMap `allowedUserTypes` controls SPA page access; `UserPermissions::roles()` controls individual API action access. These must stay in sync.
|
||||
- Example: `OPERATOR` listed in `/list-properties` `allowedUserTypes`, but `ViewProperties`/`ViewReferrals` were missing from `UserTypes::OPERATOR->value` in `roles()` → caused 403 on the `POST /admin/properties/list` API.
|
||||
- Fix: always add the required `UserActions::*` to `roles()` whenever a role is added to `allowedUserTypes` for a page.
|
||||
|
||||
### SPA Catch-all — Undefined $moduleKey
|
||||
- `VueRouteMap::handleSpa()` uses `$moduleKey` on line ~530 but only assigns it inside the `if/else` match blocks. If the path matches no known route, `$moduleKey` is undefined → potential 500.
|
||||
- Fix: initialize `$moduleKey = null;` before the `if ($routeSettings)` block.
|
||||
- `/bukidbountyapp` path is now registered in VueRouteMap as an alias of `Home` (no login required) to prevent it hitting the catch-all fallback.
|
||||
|
||||
### useUserNotes — Unauthenticated fetch
|
||||
- `TopHeader.vue` calls `fetchNotes()` in `onMounted` unconditionally, firing `GET /user/note/content` (an auth-protected endpoint) before the user logs in.
|
||||
- Fix: guard the call with `if (userStore.isLoggedIn)` in TopHeader, and add the same guard inside `useUserNotes::fetchNotes()` itself as a safety net.
|
||||
|
||||
### POS Access Key — STORE_OWNER / STORE_MANAGER missing create/delete/toggle permissions
|
||||
- `/pos-access-keys` VueRouteMap allows `store owner` and `store manager`, but `roles()` only gave them `ViewPosAccessKeys`. Calls to create/delete/toggle keys returned 403.
|
||||
- Fix: add `CreatePosAccessKey`, `DeletePosAccessKey`, `TogglePosAccessKey` to both `STORE_OWNER` and `STORE_MANAGER` in `UserPermissions::roles()`.
|
||||
- **Default State**: All modules default to `true` (enabled) if the environment variable is missing.
|
||||
|
||||
## Database Table Names (canonical)
|
||||
|
||||
**Always reference these exact table names** in raw SQL, `unique:`/`exists:` validation rules, `DB::table(...)`, joins, and migrations. Several tables use abbreviated names — using the human/plural form (e.g. `stores`, `products`) causes `Table not found` SQL errors that Hypervel surfaces as a generic 400 with empty body.
|
||||
|
||||
### Market / Commerce
|
||||
- `str` — **Stores** (model: `App\Models\Market\Store`)
|
||||
- `prd_items` — **Products** (model: `App\Models\Market\Product`)
|
||||
- `prd_str` — **Product↔Store pivot** (per-store price, stock, description, is_active)
|
||||
- `prd_trx` — **Product Transactions** (model: `ProductTransaction`). Fields used by sales aggregation: `product_id`, `store_id`, `quantity`, `created_at`. `ProductController::viewProductDetails` aggregates `sold_today` (global) and `store_sold_today` (per-store, when a store hash is provided) by summing `quantity` filtered with `whereDate('created_at', today())`. Surfaced in `BuyViewProductMarket.vue` POS scan card alongside a `printPosCode()` button (opens a small window with QR + barcode and auto-`window.print()`/close).
|
||||
- `prd_trx_ses` — **Product Transaction Sessions** (model: `ProductTransactionSession`)
|
||||
- `prd_trx_ses_arc` — **Product Transaction Sessions Archive** (model: `ProductTransactionSessionArchive`)
|
||||
- `cst` — **Customers** (model: `App\Models\Market\Customer`)
|
||||
- `carts` — Carts
|
||||
- `cart_items` — Cart Items
|
||||
- `store_managers` — Store ↔ Manager User pivot (multi-manager system)
|
||||
- `couriers` — Couriers
|
||||
- `shipments` — Shipments
|
||||
- `pos_access_keys` — POS Access Keys
|
||||
- `pos_sessions` — POS Sessions
|
||||
- `pos_sessions_archive` — Archived POS Sessions
|
||||
- `pos_transactions` — POS Transactions
|
||||
|
||||
### Organizations / Cooperatives
|
||||
- `organizations` — Organizations (cooperatives, associations, etc.)
|
||||
- `main_organizations` — Top-level / parent organization records
|
||||
- `org_str` — **Organization↔Store pivot** (cooperative-store linkage)
|
||||
- `cooperative_members` — Cooperative Members
|
||||
- `cooperative_documents` — Cooperative Documents
|
||||
- `cooperative_resolutions` — Cooperative Resolutions
|
||||
- `cooperative_votes` — Cooperative Votes
|
||||
- `chapters` — Chapters
|
||||
- `chapter_members` — Chapter Members
|
||||
- `groups` — Groups
|
||||
- `group_members` — Group Members
|
||||
- `farmer_profiles` — Farmer Profiles
|
||||
|
||||
### Users / Auth / Accounting
|
||||
- `users` — Users
|
||||
- `user_infos` — User Info (addresses JSON, profile extras)
|
||||
- `personal_access_tokens` — API personal access tokens
|
||||
- `accounts` — Accounting accounts
|
||||
- `account_transactions` — Account Transactions
|
||||
- `member_ledgers` — Member Ledgers
|
||||
- `global_transactions` — Global Transactions (cross-domain ledger)
|
||||
|
||||
### Properties / Referrals
|
||||
- `properties` — Properties
|
||||
- `referrals` — Referrals
|
||||
- `referral_keys` — Referral Keys
|
||||
|
||||
### Files / System
|
||||
- `file_list` — File List (uploaded file index)
|
||||
- `file_content` — File Content (binary/blob storage)
|
||||
- `landing_pages` — Landing Pages
|
||||
- `announcements` — Announcements
|
||||
- `system_settings` — System Settings (key/value, JSON values supported)
|
||||
- `db_backups` — Database Backup records
|
||||
- `logs` — Generic Logs
|
||||
- `table_logs` — Per-table audit logs (model: `App\Models\Generic\TableLog`)
|
||||
|
||||
## Performance / Load-Testing API
|
||||
- **Purpose**: Token-gated REST endpoints for hammering the box from a client machine over curl — exercise creation paths (users/stores/products) and the POS hot path while capturing per-op timing in milliseconds.
|
||||
- **Controller**: `app/Http/Controllers/Market/PerformanceController.php` (acts as an `ULTIMATE` system user; pick a specific actor via env `PERF_ACTOR_HASH`).
|
||||
- **Routes** (declared in `routes/api.php`, true API routes — NOT in `web.php`):
|
||||
- `GET /api/perf/ping` — token check / health.
|
||||
- `POST /api/perf/seed/users` — body `{count, type?, parent_hash?, prefix?}`.
|
||||
- `POST /api/perf/seed/stores` — body `{count, owner_hash?, category?, prefix?}`.
|
||||
- `POST /api/perf/seed/products` — body `{count, target_store_hash?, attach_to_store?, prefix?}`.
|
||||
- `POST /api/perf/seed/batch` — body `{users?, stores?, products?, type?, category?, prefix?}` runs all three, returns per-phase ms.
|
||||
- `POST /api/perf/pos/simulate` — body `{store_hash, items?, cycles?, complete?}` runs N end-to-end POS cycles (open session → bulk insert N line items via raw `pos_transactions` insert → optional complete). Reports `avg/min/max cycle_ms` plus per-cycle `open_ms`/`add_items_ms`/`complete_ms`.
|
||||
- **Auth**: header `X-Perf-Token: <env PERF_API_TOKEN>` (also accepted as `?token=` query/body). If env unset, every endpoint 403s — feature is off by default.
|
||||
- **Limits**: each seeder is clamped to 1..1000 per call; POS sim is clamped to ≤200 items per cycle and ≤100 cycles per call.
|
||||
|
||||
### Rule of thumb
|
||||
- Plural English names (`users`, `accounts`, `shipments`, `organizations`, `properties`, `couriers`, `carts`, `groups`, `chapters`, `announcements`, `landing_pages`, `system_settings`, `personal_access_tokens`) → safe to assume the table name matches.
|
||||
- **Abbreviated tables** (`str`, `cst`, `prd_items`, `prd_str`, `prd_trx`, `prd_trx_ses`, `prd_trx_ses_arc`, `org_str`) → **never guess** — use the canonical name from this list.
|
||||
- When writing `unique:<table>,<col>` or `exists:<table>,<col>` rules, look up the model's `protected ?string $table` value rather than pluralizing the model class name.
|
||||
517
ai-docs/dokploy-deployment-guide.md
Normal file
517
ai-docs/dokploy-deployment-guide.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# Dokploy Deployment Guide — BukidBountyApp (Hypervel/Swoole)
|
||||
|
||||
> This guide documents the exact process for deploying BukidBountyApp (and derivative projects) on a Dokploy-managed server. It is based on real troubleshooting and verified configuration.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Prerequisites on Dokploy Server](#prerequisites-on-dokploy-server)
|
||||
3. [Step 1: Create Infrastructure Services](#step-1-create-infrastructure-services)
|
||||
4. [Step 2: Deploy the Application](#step-2-deploy-the-application)
|
||||
5. [Step 3: Configure Environment Variables](#step-3-configure-environment-variables)
|
||||
6. [Step 4: Configure Domain & HTTPS](#step-4-configure-domain--https)
|
||||
7. [Step 5: Create the Database](#step-5-create-the-database)
|
||||
8. [Step 6: Run Migrations & Seed](#step-6-run-migrations--seed)
|
||||
9. [Deploying Test/Beta Variants](#deploying-testbeta-variants)
|
||||
10. [Troubleshooting](#troubleshooting)
|
||||
11. [Reference: Working Configuration Files](#reference-working-configuration-files)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Internet → Traefik (ports 80/443) → dokploy-network → bukidapp container (port 9501)
|
||||
→ DragonflyDB (port 6379)
|
||||
→ MySQL (port 3306)
|
||||
```
|
||||
|
||||
| Component | Type | Network |
|
||||
|-------------------|--------------------------|------------------|
|
||||
| BukidBountyApp | Docker Compose (Dokploy) | `dokploy-network`|
|
||||
| MySQL 8 | Dokploy Database (Swarm) | `dokploy-network`|
|
||||
| DragonflyDB | Docker Compose (Dokploy) | `dokploy-network`|
|
||||
| Traefik | Managed by Dokploy | `dokploy-network`|
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites on Dokploy Server
|
||||
|
||||
- Dokploy installed and running
|
||||
- Traefik configured with Let's Encrypt cert resolver
|
||||
- DNS pointing your domain to the server IP
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Infrastructure Services
|
||||
|
||||
### 1a. Create MySQL Database
|
||||
|
||||
1. In Dokploy UI → **Create** → **Database** → **MySQL**
|
||||
2. Set root password, default user, and password
|
||||
3. Dokploy will create a Swarm service (e.g., `prodservers-prodsqlmain-kv3yph`)
|
||||
|
||||
> **⚠️ IMPORTANT:**
|
||||
> The `DB_HOST` for your app is the **Swarm service name**, not the container name.
|
||||
> For Swarm services, use the service name directly (e.g., `prodservers-prodsqlmain-kv3yph`).
|
||||
|
||||
**Find the MySQL service name (run on Hostinger server terminal):**
|
||||
```bash
|
||||
docker service ls | grep -i sql
|
||||
```
|
||||
|
||||
### 1b. Create DragonflyDB (Redis Alternative)
|
||||
|
||||
1. In Dokploy UI → **Create** → **Compose** → new project for DragonflyDB
|
||||
2. Use this `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
dragonflydb:
|
||||
image: 'docker.dragonflydb.io/dragonflydb/dragonfly'
|
||||
ulimits:
|
||||
memlock: -1
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- dragonflydata:/data
|
||||
environment:
|
||||
- DFLY_requirepass
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
volumes:
|
||||
dragonflydata:
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
3. Deploy the service
|
||||
|
||||
> **⚠️ IMPORTANT:**
|
||||
> The `REDIS_HOST` for your app is the **full container name**, not the Dokploy project name.
|
||||
> For Docker Compose services, the container name follows the pattern: `{project-name}-{service-name}-{replica}`.
|
||||
|
||||
**Find the DragonflyDB container name (run on Hostinger server terminal):**
|
||||
```bash
|
||||
docker ps --format '{{.Names}}' | grep dragonfly
|
||||
# Example output: prodservers-dragonflydb-rnfaje-dragonflydb-1
|
||||
```
|
||||
|
||||
> **🔴 CRITICAL DISTINCTION:**
|
||||
> - **Swarm services** (Dokploy Databases) → use the **service name** as hostname
|
||||
> - **Compose services** (like DragonflyDB) → use the **container name** as hostname
|
||||
>
|
||||
> These are different! Always verify with `docker service ls` or `docker ps --format '{{.Names}}'`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Deploy the Application
|
||||
|
||||
1. In Dokploy UI → **Create** → **Compose**
|
||||
2. Connect to your Git repository (e.g., `git@git.cr8.space:josh/BukidBountyApp.git`)
|
||||
3. Set branch to `main`
|
||||
4. The `docker-compose.yml` in the repo will be used automatically
|
||||
|
||||
### Working `docker-compose.yml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bukidapp:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.php
|
||||
ports:
|
||||
- 9501
|
||||
networks:
|
||||
- dokploy-network
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "curl -f http://localhost:9501/health || exit 1" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
> **🔴 CAUTION:**
|
||||
> Do **NOT** use host port mapping like `9522:9501`. Just expose the internal port `9501`.
|
||||
> Traefik handles external routing via labels that Dokploy injects automatically.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Configure Environment Variables
|
||||
|
||||
In Dokploy UI → your service → **Environment** tab, set:
|
||||
|
||||
```env
|
||||
# === Database (MySQL) ===
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=<mysql-swarm-service-name>
|
||||
DB_DATABASE=bukid
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=mysql
|
||||
DB_PASSWORD=<your-mysql-password>
|
||||
|
||||
# === Cache/Redis (DragonflyDB) ===
|
||||
CACHE_DRIVER=redis
|
||||
REDIS_HOST=<dragonflydb-container-name>
|
||||
REDIS_AUTH=<your-dragonfly-password>
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
```
|
||||
|
||||
### How to Find the Correct Hostnames
|
||||
|
||||
| Service | Type | How to Find Hostname | Example |
|
||||
|-------------|--------|------------------------------------------------------------|-------------------------------------------------------|
|
||||
| MySQL | Swarm | `docker service ls \| grep sql` | `prodservers-prodsqlmain-kv3yph` |
|
||||
| DragonflyDB | Compose| `docker ps --format '{{.Names}}' \| grep dragonfly` | `prodservers-dragonflydb-rnfaje-dragonflydb-1` |
|
||||
| PostgreSQL | Swarm | `docker service ls \| grep pgsql` | `prodservers-prodpgsqlmain-x8frsw` |
|
||||
|
||||
> **💡 TIP:**
|
||||
> You can verify connectivity from the app container using Dokploy's container terminal (UI):
|
||||
> ```bash
|
||||
> ping -c 2 <hostname>
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Configure Domain & HTTPS
|
||||
|
||||
1. In Dokploy UI → your service → **Domains** tab
|
||||
2. Add domain:
|
||||
- **Host**: `bukid.hesed.sbs` (or your domain)
|
||||
- **Container Port**: `9501`
|
||||
- **HTTPS**: Enabled
|
||||
- **Certificate**: Let's Encrypt
|
||||
3. **Deploy/Redeploy** the service
|
||||
|
||||
### What Dokploy Adds Automatically
|
||||
|
||||
Dokploy injects these Traefik labels into your compose at deploy time:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- traefik.docker.network=dokploy-network
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.<id>-web.rule=Host(`bukid.hesed.sbs`)
|
||||
- traefik.http.routers.<id>-web.entrypoints=web
|
||||
- traefik.http.services.<id>-web.loadbalancer.server.port=9501
|
||||
- traefik.http.routers.<id>-web.middlewares=redirect-to-https@file
|
||||
- traefik.http.routers.<id>-websecure.rule=Host(`bukid.hesed.sbs`)
|
||||
- traefik.http.routers.<id>-websecure.entrypoints=websecure
|
||||
- traefik.http.services.<id>-websecure.loadbalancer.server.port=9501
|
||||
- traefik.http.routers.<id>-websecure.tls.certresolver=letsencrypt
|
||||
```
|
||||
|
||||
> **⚠️ IMPORTANT:**
|
||||
> You do **NOT** need to add these labels manually in your `docker-compose.yml`.
|
||||
> Dokploy handles them when you configure the domain in the UI.
|
||||
> Adding them manually can cause **label conflicts**.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Create the Database
|
||||
|
||||
The MySQL database must be created manually after the MySQL service is running.
|
||||
|
||||
**Via Hostinger server terminal:**
|
||||
|
||||
```bash
|
||||
# Connect to MySQL container
|
||||
docker exec -it $(docker ps --format '{{.Names}}' | grep prodsqlmain) mysql -u root -p
|
||||
```
|
||||
|
||||
**Inside MySQL shell:**
|
||||
|
||||
```sql
|
||||
CREATE DATABASE bukid;
|
||||
GRANT ALL PRIVILEGES ON bukid.* TO 'mysql'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Run Migrations & Seed
|
||||
|
||||
**Via Dokploy UI → your service → Terminal:**
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
php artisan db:seed
|
||||
```
|
||||
|
||||
**Or from the Hostinger server terminal:**
|
||||
|
||||
```bash
|
||||
docker exec <bukidapp-container-name> php artisan migrate
|
||||
docker exec <bukidapp-container-name> php artisan db:seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deploying Test/Beta Variants
|
||||
|
||||
To deploy a test or beta version of the same app with separate databases:
|
||||
|
||||
### 1. Plan Variant Resources
|
||||
|
||||
| Variant | MySQL DB Name | Redis DB | Domain |
|
||||
|---------|---------------|----------|---------------------------|
|
||||
| Prod | `bukid` | `0` | `bukid.hesed.sbs` |
|
||||
| Beta | `bukid_beta` | `1` | `beta.bukid.hesed.sbs` |
|
||||
| Test | `bukid_test` | `2` | `test.bukid.hesed.sbs` |
|
||||
|
||||
> **💡 TIP:**
|
||||
> You can reuse the **same MySQL server** and **same DragonflyDB instance** for all variants.
|
||||
> Just create different databases in MySQL and use different `REDIS_DB` numbers (0-15).
|
||||
|
||||
### 2. Create Additional MySQL Databases
|
||||
|
||||
**Via Hostinger server terminal:**
|
||||
|
||||
```bash
|
||||
docker exec -it $(docker ps --format '{{.Names}}' | grep prodsqlmain) mysql -u root -p
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE DATABASE bukid_beta;
|
||||
CREATE DATABASE bukid_test;
|
||||
GRANT ALL PRIVILEGES ON bukid_beta.* TO 'mysql'@'%';
|
||||
GRANT ALL PRIVILEGES ON bukid_test.* TO 'mysql'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
### 3. Deploy New Compose Project in Dokploy
|
||||
|
||||
1. In Dokploy UI → **Create** → **Compose**
|
||||
2. Connect to the same Git repo, optionally use a different branch (e.g., `beta`, `develop`)
|
||||
3. Set environment variables with variant-specific values:
|
||||
|
||||
```env
|
||||
DB_DATABASE=bukid_beta
|
||||
REDIS_DB=1
|
||||
# DB_HOST and REDIS_HOST remain the same (same infrastructure)
|
||||
```
|
||||
|
||||
4. Configure domain in Domains tab: `beta.bukid.hesed.sbs` → port `9501`
|
||||
5. Deploy
|
||||
6. Run migrations via Dokploy container terminal:
|
||||
```bash
|
||||
php artisan migrate
|
||||
php artisan db:seed
|
||||
```
|
||||
|
||||
### 4. Verify Each Variant
|
||||
|
||||
**Via Hostinger server terminal:**
|
||||
|
||||
```bash
|
||||
# Find all bukid containers
|
||||
docker ps --format '{{.Names}}' | grep bukid
|
||||
|
||||
# Check health of each — must show (healthy)
|
||||
docker ps | grep bukid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 404 Not Found (Domain Access)
|
||||
|
||||
**Check this in order:**
|
||||
|
||||
1. **Container health** — Must show `(healthy)`, NOT `(unhealthy)`
|
||||
```bash
|
||||
docker ps | grep bukidapp
|
||||
```
|
||||
2. **If unhealthy**, check app logs:
|
||||
```bash
|
||||
docker logs <container-name> --tail 100
|
||||
```
|
||||
3. **Most common cause**: Redis or MySQL DNS resolution failure → wrong hostname in env vars
|
||||
4. **Network**: Verify container is on `dokploy-network`:
|
||||
```bash
|
||||
docker network inspect dokploy-network --format '{{range .Containers}}{{.Name}} {{end}}' | tr ' ' '\n' | grep bukid
|
||||
```
|
||||
5. **Redeploy**: Domain/label changes require a full **Redeploy** in Dokploy UI, not just restart
|
||||
|
||||
### Redis DNS Lookup Failed
|
||||
|
||||
```
|
||||
RedisException: DNS Lookup resolve failed
|
||||
```
|
||||
|
||||
- **Cause**: Wrong `REDIS_HOST` value
|
||||
- **Fix**: Use full container name → `docker ps --format '{{.Names}}' | grep dragonfly`
|
||||
|
||||
### MySQL Access Denied
|
||||
|
||||
```
|
||||
SQLSTATE[HY000] [1044] Access denied for user 'mysql'@'%' to database 'bukid'
|
||||
```
|
||||
|
||||
- **Cause**: Database doesn't exist or user lacks permissions
|
||||
- **Fix**: Create the database and grant permissions (see [Step 5](#step-5-create-the-database))
|
||||
|
||||
### Traefik Logs Empty
|
||||
|
||||
```bash
|
||||
docker logs dokploy-traefik --tail 50
|
||||
```
|
||||
|
||||
If empty, Traefik hasn't encountered errors — the issue is likely the container being unhealthy.
|
||||
|
||||
### Container Keeps Restarting
|
||||
|
||||
Check logs for startup errors:
|
||||
```bash
|
||||
docker logs <container-name> --tail 200
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- Missing `.env` variables
|
||||
- Database not reachable
|
||||
- Redis not reachable
|
||||
- PHP extension missing
|
||||
|
||||
---
|
||||
|
||||
## Reference: Working Configuration Files
|
||||
|
||||
### `docker-compose.yml` (committed in repo root)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bukidapp:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.php
|
||||
ports:
|
||||
- 9501
|
||||
networks:
|
||||
- dokploy-network
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "curl -f http://localhost:9501/health || exit 1" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
### `Dockerfile.php`
|
||||
|
||||
```dockerfile
|
||||
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole-v6
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk add --no-cache \
|
||||
postgresql-dev \
|
||||
php83-pdo_pgsql \
|
||||
php83-pgsql \
|
||||
7zip
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
&& curl -fsSL https://unofficial-builds.nodejs.org/download/release/v20.19.0/node-v20.19.0-linux-x64-musl.tar.gz \
|
||||
| tar -xz -C /usr/local --strip-components=1 \
|
||||
&& node -v \
|
||||
&& npm -v
|
||||
|
||||
RUN echo "extension=pdo_pgsql.so" > /etc/php83/conf.d/01_pdo_pgsql.ini && \
|
||||
echo "extension=pgsql.so" > /etc/php83/conf.d/00_pgsql.ini
|
||||
|
||||
WORKDIR /var/app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 9501
|
||||
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
CMD ["php", "artisan", "serve"]
|
||||
```
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```env
|
||||
# Database
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=<mysql-swarm-service-name>
|
||||
DB_DATABASE=bukid
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=mysql
|
||||
DB_PASSWORD=<password>
|
||||
|
||||
# Redis / DragonflyDB
|
||||
CACHE_DRIVER=redis
|
||||
REDIS_HOST=<dragonflydb-container-name>
|
||||
REDIS_AUTH=<password>
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
```
|
||||
|
||||
### `/health` Route (in `routes/web.php`)
|
||||
|
||||
```php
|
||||
Route::get('/health', function () {
|
||||
return 'OK';
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Useful Commands
|
||||
|
||||
All commands are run on the **Hostinger server terminal** unless otherwise noted.
|
||||
|
||||
```bash
|
||||
# List all running containers
|
||||
docker ps
|
||||
|
||||
# Find a specific container name
|
||||
docker ps --format '{{.Names}}' | grep <keyword>
|
||||
|
||||
# List Swarm services (for Dokploy Databases)
|
||||
docker service ls
|
||||
|
||||
# Check container health
|
||||
docker ps | grep <container>
|
||||
|
||||
# View container logs
|
||||
docker logs <container-name> --tail 100
|
||||
|
||||
# Inspect dokploy-network's connected containers
|
||||
docker network inspect dokploy-network --format '{{range .Containers}}{{.Name}} {{end}}'
|
||||
|
||||
# Test connectivity (run in Dokploy container terminal via UI)
|
||||
ping -c 2 <target-hostname>
|
||||
|
||||
# Run artisan commands (via Dokploy container terminal via UI)
|
||||
php artisan migrate
|
||||
php artisan db:seed
|
||||
|
||||
# Connect to MySQL (via Hostinger server terminal)
|
||||
docker exec -it $(docker ps --format '{{.Names}}' | grep prodsqlmain) mysql -u root -p
|
||||
```
|
||||
86
ai-docs/features-recommendations.md
Normal file
86
ai-docs/features-recommendations.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# System Scan & Feature Recommendations
|
||||
|
||||
This document outlines recommended changes, improvements, and new features for the BukidBountyApp based on recent system-wide scans and development patterns.
|
||||
|
||||
## 🏗️ Architecture & Technical Improvements
|
||||
|
||||
### 1. Unified API Response & Error Handling [HIGH PRIORITY]
|
||||
- **Current State**: `executeRequest.js` is a basic fetch wrapper. Axios is used directly in stores without global interceptors.
|
||||
- **Recommendation**: Transition all API calls to a centralized Axios instance with global interceptors.
|
||||
- **Action**: Implement a standard response structure `{ success: boolean, data: any, message: string }`. Automatically handle 401 (Logout), 403 (Forbidden), and 500 (Server Error) via global toast notifications.
|
||||
- **Benefit**: Reduces boilerplate in components and ensures consistent error feedback.
|
||||
|
||||
### 2. Model Boot Traits / HasStandardFields
|
||||
- **Current State**: `ModelSavingListener` handles `hashkey`, `created_by`, and `updated_by` globally.
|
||||
- **Recommendation**: Move this logic to a `HasStandardFields` trait.
|
||||
- **Benefit**: Cleaner model definitions and avoids global listener overhead for models that don't need these fields.
|
||||
|
||||
### 3. Real-time Synchronization (WebSockets)
|
||||
- **Recommendation**: Integrate WebSockets for live updates.
|
||||
- **Targets**:
|
||||
- Real-time sale notifications for Store Owners.
|
||||
- Live shipment status updates for Buyers.
|
||||
- POS session updates across multiple terminals.
|
||||
- **Benefit**: Makes the application feel alive and reduces manual refreshing.
|
||||
|
||||
### 4. PWA & Offline Support (Advanced)
|
||||
- **Current State**: `PosMain.vue` has basic local storage for access keys, but no offline transaction capability.
|
||||
- **Recommendation**: Implement IndexedDB for `PosMain.vue` to allow full offline operation.
|
||||
- **Action**: Store product catalog and pending transactions locally. Auto-sync when back online using Background Sync API.
|
||||
- **Benefit**: Critical for farm locations with poor connectivity.
|
||||
|
||||
### 5. Defensive Middleware & Null Safety
|
||||
- **Recommendation**: Audit all middleware (especially `EnsureUserGroupIsActive`) for null checks.
|
||||
- **Benefit**: Prevents "Call to a member function on null" errors during user role/group transitions.
|
||||
|
||||
---
|
||||
|
||||
## 💎 UI/UX Enhancements
|
||||
|
||||
### 1. Skeleton Screens [PENDING]
|
||||
- **Current State**: `HomeUltimate.vue` and others use a generic `LoadingSpinner`.
|
||||
- **Recommendation**: Replace spinners with content-matching Skeleton Loaders.
|
||||
- **Benefit**: Improves perceived performance and reduces layout shift (CLS).
|
||||
|
||||
### 2. Premium Micro-animations
|
||||
- **Recommendation**: Add subtle transitions for page changes and modal entries.
|
||||
- **Action**: Implement "Success" Lottie animations for completed transactions in POS.
|
||||
- **Benefit**: Increases user satisfaction and perceived app quality.
|
||||
|
||||
### 3. Dynamic Dashboard Activity
|
||||
- **Current State**: `HomeUltimate.vue` recent activity is hardcoded placeholders.
|
||||
- **Recommendation**: Connect the "Recent System Activity" list to real audit logs or transaction history.
|
||||
- **Benefit**: Provides immediate value and transparency to administrators.
|
||||
|
||||
### 4. Compact Mode for Data Tables
|
||||
- **Recommendation**: Add a "Compact Mode" toggle in User Settings for dense lists (Users, Transactions).
|
||||
- **Benefit**: Improves productivity for power users managing large datasets.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 New Feature Recommendations
|
||||
|
||||
### 1. POS "Quick-Clear" & Reset [COMPLETED]
|
||||
- **Description**: A dedicated button to completely reset the current POS cart with a confirmation modal.
|
||||
- **Status**: Implemented via `/api/pos/clear` and `PosMain.vue` UI.
|
||||
|
||||
### 2. QR-Based Logistics Tracking
|
||||
- **Description**: Scan QR codes at each stage of the shipment lifecycle (Picked up -> In Transit -> Delivered).
|
||||
- **Benefit**: Enhances trust and transparency in the supply chain.
|
||||
|
||||
### 3. Advanced Ultimate Tools
|
||||
- **Description**: Add Redis Memory Monitor and live SQL profiling to the Ultimate Console.
|
||||
- **Benefit**: Empower system admins to diagnose performance issues in real-time.
|
||||
|
||||
### 4. Farmer-to-Store Bidding Board
|
||||
- **Description**: A digital board where stores post needs and farmers bid to fulfill them.
|
||||
- **Benefit**: Bridges the gap between production and retail demand.
|
||||
|
||||
---
|
||||
|
||||
## 📈 Roadmap Suggestions
|
||||
|
||||
1. **Phase 1 (Stability)**: Unified API Handling, `HasStandardFields` Trait, Middleware Null-Safety.
|
||||
2. **Phase 2 (Experience)**: Skeleton Screens, Dynamic Activity Logs, POS Reset Button.
|
||||
3. **Phase 3 (Logistics)**: QR Shipment Tracking, Offline POS (IndexedDB).
|
||||
4. **Phase 4 (Marketplace)**: Bidding Board, Advanced Ultimate Monitoring.
|
||||
49
ai-docs/file-map.json
Normal file
49
ai-docs/file-map.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"auth": [
|
||||
"resources/js/Pages/Auth/Login.vue",
|
||||
"app/Http/Controllers/LoginController.php"
|
||||
],
|
||||
"userManagement": [
|
||||
"resources/js/Pages/CreateUser.vue",
|
||||
"resources/js/Pages/EditUser.vue",
|
||||
"resources/js/Pages/UserList.vue",
|
||||
"app/Http/Controllers/UserCreateController.php",
|
||||
"app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php",
|
||||
"app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"app/Http/Controllers/Pages/UserListPageController.php"
|
||||
],
|
||||
"market": [
|
||||
"resources/js/Pages/BuyViewProductMarket.vue",
|
||||
"resources/js/Pages/EditProductUltimate.vue",
|
||||
"resources/js/Pages/ListProductsMarket.vue",
|
||||
"resources/js/Pages/ListStores.vue",
|
||||
"resources/js/Pages/RemoveProductFromStoreAdmin.vue",
|
||||
"resources/js/Pages/ViewStoreMarket.vue",
|
||||
"app/Http/Controllers/Market/ProductController.php",
|
||||
"app/Http/Controllers/Market/StoreController.php"
|
||||
],
|
||||
"home": [
|
||||
"resources/js/Pages/Home.vue",
|
||||
"resources/js/Pages/Fragments/Home/HomePublic.vue",
|
||||
"resources/js/Pages/Fragments/Home/HomeUltimate.vue",
|
||||
"app/Http/Controllers/Pages/Core/HomeController.php"
|
||||
],
|
||||
"accountSettings": [
|
||||
"resources/js/Pages/AccountSettings.vue",
|
||||
"app/Http/Controllers/Pages/AccountSettingsPageController.php"
|
||||
],
|
||||
"transferCredits": [
|
||||
"resources/js/Pages/TransferMyCredit.vue",
|
||||
"app/Http/Controllers/Pages/TransferMyCreditPageController.php"
|
||||
],
|
||||
"fileManagement": [
|
||||
"app/Http/Controllers/FilesMainController.php",
|
||||
"app/Models/FileContent.php",
|
||||
"app/Models/FileList.php"
|
||||
],
|
||||
"pages": [
|
||||
"resources/js/Pages/Fragments/Home/*.vue",
|
||||
"app/Http/Controllers/viewHelperController.php",
|
||||
"app/Http/Controllers/PageMemoryController.php"
|
||||
]
|
||||
}
|
||||
35
ai-docs/files/LoginController.md
Normal file
35
ai-docs/files/LoginController.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# LoginController.php
|
||||
|
||||
## Purpose
|
||||
Handles user authentication including login, session extension, and logout operations.
|
||||
|
||||
## Key Components
|
||||
- `LoginController` class
|
||||
- `authenticate()` method
|
||||
- `extendcurrentSession()` method
|
||||
|
||||
## Exported Interfaces
|
||||
- `authenticate()` - Authenticate user via JWT
|
||||
- `extendcurrentSession()` - Extend current session lifetime
|
||||
|
||||
## Inputs
|
||||
- `$request` - HTTP request with username/password
|
||||
- Session data for token validation
|
||||
|
||||
## Outputs
|
||||
- Redirect to home page on success
|
||||
- JSON response on failure
|
||||
- Session creation on successful authentication
|
||||
|
||||
## Side Effects
|
||||
- Creates JWT-based session
|
||||
- Sets session cookie
|
||||
|
||||
## Dependencies
|
||||
- `Hypervel\Support\Facades\Auth`
|
||||
- `App\Models\User`
|
||||
|
||||
## Notes
|
||||
- Uses JWT authentication via Hypervel framework
|
||||
- Validates credentials against database
|
||||
- Supports both web and API authentication
|
||||
42
ai-docs/files/UserModifyAdminPageController.md
Normal file
42
ai-docs/files/UserModifyAdminPageController.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# UserModifyAdminPageController.php
|
||||
|
||||
## Purpose
|
||||
Provides admin-level user modification and management capabilities including enabling/disabling users, password resets, role changes, and session management.
|
||||
|
||||
## Key Components
|
||||
- `UserModifyAdminPageController` class
|
||||
- `PageResponses_UserModify` trait
|
||||
|
||||
## Exported Interfaces
|
||||
- `Response_UserDetails()` - Get user details by hash key
|
||||
- `Response_DisableUser()` / `EnableUser()` - Toggle user active status
|
||||
- `Response_DeleteUser()` - Delete user record
|
||||
- `Response_ResetUserPassword()` - Reset user password
|
||||
- `Response_LogoutUser()` - Force logout user session
|
||||
|
||||
## Inputs
|
||||
- `$hashkey` - User hash key identifier
|
||||
- `$details` - Array of user details to update
|
||||
- `$newPassword` - New password string
|
||||
- `$active` - Boolean for active status
|
||||
- `$request` - Hypervel HTTP request object
|
||||
|
||||
## Outputs
|
||||
- JSON responses with success/error data
|
||||
- User detail arrays or error messages
|
||||
|
||||
## Side Effects
|
||||
- Updates user records in database
|
||||
- Invalidates Redis sessions on force logout
|
||||
- Modifies user password hashes
|
||||
|
||||
## Dependencies
|
||||
- `App\Enums\UserActions`
|
||||
- `App\Http\Controllers\Helpers\Permissions\UserPermissions`
|
||||
- `App\Models\User`
|
||||
- `Hypervel\Support\Facades\Auth`, `Hash`, `Redis`, `Cache`
|
||||
|
||||
## Notes
|
||||
- Uses trait pattern for response handlers
|
||||
- Permission checks via `UserPermissions::isActionPermitted()`
|
||||
- Supports hierarchical user relationships (parent/children)
|
||||
40
ai-docs/files/app.md
Normal file
40
ai-docs/files/app.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# app.js
|
||||
|
||||
## Purpose
|
||||
Main Vue application entry point that sets up the SPA (Single Page Application) architecture with global state management and component loading.
|
||||
|
||||
## Key Components
|
||||
- `createApp()` - Vue app instance creation
|
||||
- `Pinia` - State management (replaces Vuex)
|
||||
- `useUserStore()` - User state management
|
||||
- `useAuth()` - Authentication composable
|
||||
|
||||
## Exported Interfaces
|
||||
- `$navigate` - Global navigation helper
|
||||
- `$user` - User store access
|
||||
- `$auth` - Auth composable access
|
||||
- `$modal` - Modal dialog access
|
||||
|
||||
## Inputs
|
||||
- Server-side page data via `dataset.page`
|
||||
- Component imports from `resources/js/Pages/`
|
||||
|
||||
## Outputs
|
||||
- Vue app mounted to `#app` element
|
||||
- Global navigation via `$navigate` helper
|
||||
- Async component loading based on current route
|
||||
|
||||
## Side Effects
|
||||
- Initializes Pinia state management
|
||||
- Fetches current user data on startup
|
||||
- Registers global components (TopHeader, BottomNav)
|
||||
|
||||
## Dependencies
|
||||
- `createApp`, `h`, `defineAsyncComponent` from Vue 3
|
||||
- `Pinia` for state management
|
||||
- Axios for API calls
|
||||
|
||||
## Notes
|
||||
- Uses Pinia instead of Vuex for state management
|
||||
- Async component loading improves performance
|
||||
- Global `$navigate` helper available for programmatic navigation
|
||||
40
ai-docs/files/viewHelperController.md
Normal file
40
ai-docs/files/viewHelperController.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# viewHelperController.php
|
||||
|
||||
## Purpose
|
||||
Central controller for page rendering and Vue component delivery. Handles both server-side rendering with templates and client-side SPA navigation.
|
||||
|
||||
## Key Components
|
||||
- `viewHelperController` class
|
||||
- `servePageFragmentUnified()` method
|
||||
- `getDefaultDataVariables()` method
|
||||
- `getAllViews()` method
|
||||
|
||||
## Exported Interfaces
|
||||
- `servePageFragment()` - Render page without template
|
||||
- `servePageFragmentWithTemplate()` - Render page with layout template
|
||||
- `getDefaultDataVariables()` - Get default view variables
|
||||
- `getAllViews()` - Get all available blade views
|
||||
|
||||
## Inputs
|
||||
- `$page` - Page identifier (route name)
|
||||
- `$data` - Data payload for the page
|
||||
- `$withTemplate` - Boolean to include layout template
|
||||
|
||||
## Outputs
|
||||
- HTML response with embedded navigation script
|
||||
- Base64-encoded HTML for SPA
|
||||
- View variables including currentUser and userModifyAdmin
|
||||
|
||||
## Side Effects
|
||||
- Renders blade templates
|
||||
- Returns base64-encoded HTML for Vue navigation
|
||||
|
||||
## Dependencies
|
||||
- `App\Enums\UserTypes`
|
||||
- `Hypervel\Support\Facades\Auth`, `Response`, `File`
|
||||
- `App\Http\Controllers\Pages\Core\ApplicationController`
|
||||
|
||||
## Notes
|
||||
- Supports public and authenticated routes
|
||||
- Uses viewMap config for page-to-view mapping
|
||||
- Returns base64-encoded HTML for SPA navigation
|
||||
287
ai-docs/function-index.json
Normal file
287
ai-docs/function-index.json
Normal file
@@ -0,0 +1,287 @@
|
||||
{
|
||||
"authenticate": {
|
||||
"file": "app/Http/Controllers/LoginController.php",
|
||||
"module": "auth",
|
||||
"type": "function"
|
||||
},
|
||||
"extendcurrentSession": {
|
||||
"file": "app/Http/Controllers/LoginController.php",
|
||||
"module": "auth",
|
||||
"type": "function"
|
||||
},
|
||||
"setSessiontoKeepAlive": {
|
||||
"file": "app/Http/Controllers/LoginController.php",
|
||||
"module": "auth",
|
||||
"type": "method"
|
||||
},
|
||||
"servePageFragment": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "function"
|
||||
},
|
||||
"servePageFragmentWithTemplate": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "function"
|
||||
},
|
||||
"servePageFragmentUnified": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "function"
|
||||
},
|
||||
"getDefaultDataVariables": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "method"
|
||||
},
|
||||
"getAllViews": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "function"
|
||||
},
|
||||
"decodeData": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "method"
|
||||
},
|
||||
"tryjsondecode": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "method"
|
||||
},
|
||||
"tryjsonencode": {
|
||||
"file": "app/Http/Controllers/viewHelperController.php",
|
||||
"module": "pages",
|
||||
"type": "method"
|
||||
},
|
||||
"uploadFilefromRequest": {
|
||||
"file": "app/Http/Controllers/FilesMainController.php",
|
||||
"module": "files",
|
||||
"type": "function"
|
||||
},
|
||||
"viewFilebyFileListHash": {
|
||||
"file": "app/Http/Controllers/FilesMainController.php",
|
||||
"module": "files",
|
||||
"type": "function"
|
||||
},
|
||||
"createUser": {
|
||||
"file": "app/Http/Controllers/UserCreateController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_ListChildrenofCurrentUser": {
|
||||
"file": "app/Http/Controllers/Pages/UserListPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_UserDetails": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_childrenofTargetUser": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_DisableUser": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_EnableUser": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_DeleteUser": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_ViewNotes": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_DeleteNotes": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_ReplaceNotes": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_UpdateUserDetails": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_ResetUserPassword": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_LogoutUser": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_ChangeUserRoles": {
|
||||
"file": "app/Http/Controllers/Pages/UserModifyAdminPageController.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"CreateUser": {
|
||||
"file": "app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"listAllUserTypesforSelectHTML": {
|
||||
"file": "app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"listAllUsersforParentSelectHTML": {
|
||||
"file": "app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"checkIfUserMobileNumberExists": {
|
||||
"file": "app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"checkIfUsernameExists": {
|
||||
"file": "app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php",
|
||||
"module": "userManagement",
|
||||
"type": "function"
|
||||
},
|
||||
"listDetails": {
|
||||
"file": "app/Http/Controllers/Pages/AccountSettingsPageController.php",
|
||||
"module": "accountSettings",
|
||||
"type": "function"
|
||||
},
|
||||
"listSettings": {
|
||||
"file": "app/Http/Controllers/Pages/AccountSettingsPageController.php",
|
||||
"module": "accountSettings",
|
||||
"type": "function"
|
||||
},
|
||||
"changepassword": {
|
||||
"file": "app/Http/Controllers/Pages/AccountSettingsPageController.php",
|
||||
"module": "accountSettings",
|
||||
"type": "function"
|
||||
},
|
||||
"listRunScripts": {
|
||||
"file": "app/Http/Controllers/Pages/AccountSettingsPageController.php",
|
||||
"module": "accountSettings",
|
||||
"type": "function"
|
||||
},
|
||||
"getUserNotes": {
|
||||
"file": "app/Http/Controllers/Pages/AccountSettingsPageController.php",
|
||||
"module": "accountSettings",
|
||||
"type": "function"
|
||||
},
|
||||
"clearUserNotes": {
|
||||
"file": "app/Http/Controllers/Pages/AccountSettingsPageController.php",
|
||||
"module": "accountSettings",
|
||||
"type": "function"
|
||||
},
|
||||
"logoutnow": {
|
||||
"file": "app/Http/Controllers/Pages/AccountSettingsPageController.php",
|
||||
"module": "accountSettings",
|
||||
"type": "function"
|
||||
},
|
||||
"Response_TransferMyCredit": {
|
||||
"file": "app/Http/Controllers/Pages/TransferMyCreditPageController.php",
|
||||
"module": "transferCredits",
|
||||
"type": "function"
|
||||
},
|
||||
"createNew_Admin": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"editProductAdmin": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"AddProducttoStore": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"RemoveProductFromStore": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"viewProductDetails": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"listProductsData": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"getCategories": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"getSubcategories": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"viewProductDetailsByStoreEdit": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"viewProductwithAddStoreData": {
|
||||
"file": "app/Http/Controllers/Market/ProductController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"store": {
|
||||
"file": "app/Http/Controllers/Market/StoreController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"update": {
|
||||
"file": "app/Http/Controllers/Market/StoreController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"viewStoreDetails": {
|
||||
"file": "app/Http/Controllers/Market/StoreController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"editStoreDetails": {
|
||||
"file": "app/Http/Controllers/Market/StoreController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"listStoresActiveDataAll": {
|
||||
"file": "app/Http/Controllers/Market/StoreController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"removeProductfromStore": {
|
||||
"file": "app/Http/Controllers/Market/StoreController.php",
|
||||
"module": "market",
|
||||
"type": "function"
|
||||
},
|
||||
"handle": {
|
||||
"file": "app/Http/Controllers/Photos/PhotoGallery.php",
|
||||
"module": "photos",
|
||||
"type": "function"
|
||||
}
|
||||
}
|
||||
23
ai-docs/modules/accountSettings.md
Normal file
23
ai-docs/modules/accountSettings.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Account Settings Module
|
||||
|
||||
## Purpose
|
||||
Manages user account settings including password changes, notes, and script execution.
|
||||
|
||||
## Key Files
|
||||
- `app/Http/Controllers/Pages/AccountSettingsPageController.php` - Controller
|
||||
- `resources/js/Pages/AccountSettings.vue` - Vue component
|
||||
|
||||
## Public APIs
|
||||
- `listDetails()` - List account details
|
||||
- `changepassword()` - Change user password
|
||||
- `logoutnow()` - Logout current user
|
||||
- `getUserNotes()` / `clearUserNotes()` - Note management
|
||||
|
||||
## Dependencies
|
||||
- `App\Models\User`
|
||||
- `Hypervel\Support\Facades\Auth`
|
||||
|
||||
## Important Behavior
|
||||
- Passwords are encrypted using bcrypt
|
||||
- Notes stored in user's notes field
|
||||
- Scripts can be executed via exec_command field
|
||||
24
ai-docs/modules/auth.md
Normal file
24
ai-docs/modules/auth.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Auth Module
|
||||
|
||||
## Purpose
|
||||
Handles user authentication, login, session management, and logout functionality. This module provides the foundation for all authenticated routes in the application.
|
||||
|
||||
## Key Files
|
||||
- `resources/js/Pages/Auth/Login.vue` - Vue login component
|
||||
- `app/Http/Controllers/LoginController.php` - Authentication controller
|
||||
- `app/Models/User.php` - User model with authentication
|
||||
|
||||
## Public APIs
|
||||
- `authenticate()` - Authenticate user via JWT
|
||||
- `extendcurrentSession()` - Extend current session lifetime
|
||||
- `setSessiontoKeepAlive()` - Keep session alive
|
||||
|
||||
## Dependencies
|
||||
- `Hypervel\Support\Facades\Auth`
|
||||
- `App\Http\Controllers\Pages\Core\ApplicationController`
|
||||
|
||||
## Important Behavior
|
||||
- Uses JWT-based authentication
|
||||
- Validates user credentials against database
|
||||
- Creates session upon successful login
|
||||
- Redirects to home page or returns JSON response based on request type
|
||||
71
ai-docs/modules/cooperative_module.md
Normal file
71
ai-docs/modules/cooperative_module.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Module Planning: Cooperative Module
|
||||
|
||||
## Overview
|
||||
The Cooperative Module manages associations of users (members) who work together, typically in agriculture. It integrates deeply with `user_infos` to provide a complete picture of each member.
|
||||
|
||||
## Data Models
|
||||
|
||||
### `Organization` (existing)
|
||||
Used to store the cooperative's main details.
|
||||
- `id` (Primary Key)
|
||||
- `hashkey` (String, Unique)
|
||||
- `name` (String)
|
||||
- `type` (Enum: `COOPERATIVE`, `ASSOCIATION`, `COMPANY`)
|
||||
- `address` (Text)
|
||||
- `is_active` (Boolean)
|
||||
|
||||
### `CooperativeMember` [NEW]
|
||||
Links users to cooperatives.
|
||||
- `id` (Primary Key)
|
||||
- `hashkey` (String, Unique)
|
||||
- `organization_id` (Foreign Key to `organizations`)
|
||||
- `user_id` (Foreign Key to `users`)
|
||||
- `role` (String: `MEMBER`, `OFFICER`, `ADMIN`)
|
||||
- `joined_at` (DateTime)
|
||||
- `is_active` (Boolean)
|
||||
- `created_by` (Foreign Key to `users`)
|
||||
- `updated_by` (Foreign Key to `users`)
|
||||
|
||||
### `UserInfo` [NEW]
|
||||
Detailed personal information for users, expanding the core `users` table.
|
||||
- `id` (Primary Key)
|
||||
- `hashkey` (String, Unique)
|
||||
- `user_id` (Foreign Key to `users`)
|
||||
- `firstname` (String)
|
||||
- `middlename` (String)
|
||||
- `lastname` (String)
|
||||
- `fullname` (String) - Generated or stored
|
||||
- `landline` (String)
|
||||
- `mobile` (String)
|
||||
- `email` (String)
|
||||
- `alt_email` (String)
|
||||
- `alt_landline` (String)
|
||||
- `alt_mobile` (String)
|
||||
- `facebook_url` (String)
|
||||
- `bank_details` (JSON)
|
||||
- `addresses` (JSON) - Array of address objects
|
||||
- `other_details` (JSON)
|
||||
- `is_active` (Boolean)
|
||||
- `created_by` (Foreign Key to `users`)
|
||||
- `updated_by` (Foreign Key to `users`)
|
||||
|
||||
## Core Workflows
|
||||
|
||||
1. **Cooperative Management**: Creating and updating cooperative profiles (using `organizations` table).
|
||||
2. **Member Enrollment**: Adding users to a cooperative and assigning roles.
|
||||
3. **User Profile Completion**: Linking users to their detailed `userinfo` record.
|
||||
|
||||
## API Endpoints (Proposed)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/cooperatives` | GET | List all cooperatives |
|
||||
| `/api/cooperatives/{hashkey}/members` | GET | List members of a cooperative |
|
||||
| `/api/cooperatives/{hashkey}/join` | POST | Request to join or add a member |
|
||||
| `/api/user-info/{user_hashkey}` | GET | Get detailed user info |
|
||||
| `/api/user-info/{user_hashkey}` | POST/PUT | Update detailed user info |
|
||||
|
||||
## UI Components (Vue)
|
||||
- **CooperativeList.vue**: Standard list of cooperatives.
|
||||
- **CooperativeDetail.vue**: detailed view with member list.
|
||||
- **UserInfoEdit.vue**: Comprehensive form for editing personal details.
|
||||
48
ai-docs/modules/farmer_management_module.md
Normal file
48
ai-docs/modules/farmer_management_module.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Module Planning: Supplier/Farmer Management
|
||||
|
||||
## Overview
|
||||
This module expands the basic member information into a full profile for producers (farmers/suppliers). It allows the system to track where products come from, verify farmer credentials, and manage relationships with cooperatives.
|
||||
|
||||
## Data Models
|
||||
|
||||
### `FarmerProfile`
|
||||
Extended details for a producer.
|
||||
- `id` (Primary Key)
|
||||
- `hashkey` (String, Unique)
|
||||
- `user_id` (Foreign Key to `users`)
|
||||
- `organization_id` (Foreign Key to `organizations/cooperatives`, nullable)
|
||||
- `farm_name` (String)
|
||||
- `farm_location` (Point/Coordinates or Text)
|
||||
- `main_crops` (JSON/Array of strings)
|
||||
- `verification_status` (Enum: `UNVERIFIED`, `PENDING`, `VERIFIED`, `REJECTED`)
|
||||
- `certification_details` (JSON)
|
||||
- `created_by` (Foreign Key to `users`)
|
||||
- `updated_by` (Foreign Key to `users`)
|
||||
- `is_active` (Boolean)
|
||||
|
||||
### `FarmerProductMapping`
|
||||
Links farmers to the specific products they supply to stores.
|
||||
- `farmer_id` (Foreign Key to `farmer_profiles`)
|
||||
- `product_id` (Foreign Key to `products`)
|
||||
- `supply_capacity` (Decimal/Unit)
|
||||
- `harvest_season` (String)
|
||||
|
||||
## Core Workflows
|
||||
|
||||
1. **Farmer Registration**: Capturing the detailed "Member Information" as seen in the README.
|
||||
2. **Profile Verification**: Admin review of submitted documents and farm location.
|
||||
3. **Product Sourcing**: Linking products in the store to their original farmer sources for traceability.
|
||||
|
||||
## API Endpoints (Proposed)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/farmers` | GET | List all farmers (filtered by org/admin) |
|
||||
| `/api/farmers` | POST | Register a new farmer profile |
|
||||
| `/api/farmers/{hashkey}` | GET | Get farmer details |
|
||||
| `/api/farmers/{hashkey}/verify` | PATCH | Update verification status |
|
||||
|
||||
## UI Components (Vue)
|
||||
- **FarmerProfileEdit.vue**: Comprehensive form for farmer details.
|
||||
- **FarmerDirectory.vue**: Searchable list of registered farmers.
|
||||
- **VerificationDashboard.vue**: Admin interface for approving farmer applications.
|
||||
23
ai-docs/modules/files.md
Normal file
23
ai-docs/modules/files.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Files Module
|
||||
|
||||
## Purpose
|
||||
Handles file uploads and management operations including photos, documents, and binary files.
|
||||
|
||||
## Key Files
|
||||
- `app/Http/Controllers/FilesMainController.php` - File upload controller
|
||||
- `app/Models/FileContent.php` - File content model
|
||||
- `app/Models/FileList.php` - File list metadata model
|
||||
|
||||
## Public APIs
|
||||
- `uploadFilefromRequest()` - Upload file from request
|
||||
- `viewFilebyFileListHash()` - View file by hash key
|
||||
|
||||
## Dependencies
|
||||
- `App\Models\FileContent`
|
||||
- `Hypervel\Support\Facades\Storage`
|
||||
|
||||
## Important Behavior
|
||||
- Stores files in binary format via FileContent model
|
||||
- Tracks metadata in FileList model
|
||||
- Supports various file categories
|
||||
- Returns file content with proper headers for download/viewing
|
||||
23
ai-docs/modules/home.md
Normal file
23
ai-docs/modules/home.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Home Module
|
||||
|
||||
## Purpose
|
||||
Provides the main application shell and home page functionality. Includes layout fragments, top header, and bottom navigation.
|
||||
|
||||
## Key Files
|
||||
- `resources/js/Pages/Home.vue` - Main home component
|
||||
- `resources/js/Pages/Fragments/Home/HomePublic.vue` - Public home fragment
|
||||
- `resources/js/Pages/Fragments/Home/HomeUltimate.vue` - Ultimate user home fragment
|
||||
- `app/Http/Controllers/Pages/Core/HomeController.php`
|
||||
|
||||
## Public APIs
|
||||
- `render()` - Renders the main application shell
|
||||
|
||||
## Dependencies
|
||||
- `TopHeader` - Header component
|
||||
- `BottomNav` - Bottom navigation component
|
||||
- `BaseModal` - Modal component
|
||||
|
||||
## Important Behavior
|
||||
- Uses Pinia stores for state management (user, UI, network)
|
||||
- Async component loading based on current page
|
||||
- Global navigate helper available via `$navigate`
|
||||
28
ai-docs/modules/market.md
Normal file
28
ai-docs/modules/market.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Market Module
|
||||
|
||||
## Purpose
|
||||
Manages product and store operations for the multi-vendor marketplace. Handles product CRUD, store management, and transaction tracking.
|
||||
|
||||
## Key Files
|
||||
- `app/Http/Controllers/Market/ProductController.php` - Product controller
|
||||
- `app/Http/Controllers/Market/StoreController.php` - Store controller
|
||||
- `app/Models/Market/Product.php` - Product model
|
||||
- `app/Models/Market/Store.php` - Store model
|
||||
|
||||
## Public APIs
|
||||
- `createNew_Admin()` - Create product for admin
|
||||
- `editProductAdmin()` - Edit product
|
||||
- `AddProducttoStore()` / `RemoveProductFromStore()`
|
||||
- `store()` - Create new store
|
||||
- `update()` - Update store
|
||||
|
||||
## Dependencies
|
||||
- `App\Models\Market\Product`
|
||||
- `App\Models\Market\Store`
|
||||
- `App\Models\User`
|
||||
|
||||
## Important Behavior
|
||||
- Products can belong to multiple stores (belongsToMany)
|
||||
- Supports categories and subcategories via enums
|
||||
- Tracks transaction sessions per store
|
||||
- Store-product relationship with pivot data (available, price, is_active)
|
||||
25
ai-docs/modules/pages.md
Normal file
25
ai-docs/modules/pages.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Pages Module
|
||||
|
||||
## Purpose
|
||||
Handles page rendering and server-side Vue component delivery. Provides unified approach for both SPA (client-side) and traditional server-rendered pages.
|
||||
|
||||
## Key Files
|
||||
- `app/Http/Controllers/viewHelperController.php` - Main page controller
|
||||
- `resources/js/Pages/Fragments/Home/*.vue` - Home page fragments
|
||||
- `app/Http/Controllers/PageMemoryController.php`
|
||||
|
||||
## Public APIs
|
||||
- `servePageFragment()` - Render page without template
|
||||
- `servePageFragmentWithTemplate()` - Render page with layout template
|
||||
- `getDefaultDataVariables()` - Get default view variables
|
||||
- `getAllViews()` - Get all available views
|
||||
|
||||
## Dependencies
|
||||
- `App\Enums\UserTypes` - Role enum
|
||||
- `App\Http\Controllers\Pages\Core\ApplicationController`
|
||||
|
||||
## Important Behavior
|
||||
- Routes pages via `/p/{page}/s/` pattern
|
||||
- Uses viewMap config for page-to-view mapping
|
||||
- Supports public and authenticated routes
|
||||
- Returns base64-encoded HTML for SPA navigation
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user