initial: bootstrap from BukidBountyApp base
This commit is contained in:
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`)
|
||||
Reference in New Issue
Block a user