Files
BarangaySystem/.claude/plans/4165d258481eb317bef1ddcfd08f2295-complete.md
2026-06-06 18:43:00 +08:00

7.2 KiB
Raw Permalink Blame History

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.jsuploadFile(file) → POST /File/Upload/ProductMarket → returns { hashkey }. Already used in CreateProductStoreOwner.vue.
  • Backend: app/Http/Controllers/Market/BatchController.phpbatchCreateProducts() — needs to accept photourl and pass it to Product::create().
  • Product model: app/Models/Market/Product.phpphotourl 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)

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
  • 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)