7.2 KiB
Plan: Batch Add Products — Photo Upload + Category Dropdown Fix
Goal
Two fixes for resources/js/Pages/BatchAddProducts.vue:
- 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. - 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 thecategoriesref (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 inCreateProductStoreOwner.vue. - Backend:
app/Http/Controllers/Market/BatchController.php→batchCreateProducts()— needs to acceptphotourland pass it toProduct::create(). - Product model:
app/Models/Market/Product.php—photourlis 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)
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)
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:
photourl: p.photoHash ? [p.photoHash] : [],
In BatchController::batchCreateProducts(), add to the validator for source === 'new':
'photourl' => 'nullable|array',
'photourl.*' => 'nullable|string',
And in Product::create([...]) add:
'photourl' => $productData['photourl'] ?? [],
Step-by-step implementation
Step 1 — Add photoHash + photoUploading to leaf state
In makeLeaf():
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:
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:
<!-- 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:
<input
v-model="product.category"
type="text"
list="leaf-categories"
class="form-control form-control-sm"
placeholder="e.g. Vegetables"
>
With:
<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:
{
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:
'photourl' => 'nullable|array',
'photourl.*' => 'nullable|string',
In Product::create([...]), add:
'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
photoHashis included in the save payload asphotourl: [hash]- Backend accepts and stores
photourlon the new product - Category field is a
<select>dropdown populated with categories from the API - Selecting a category from the dropdown sets
product.categorycorrectly <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)