initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,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`)