fix: multi-category checkboxes in admin + requires-delivery toggle

- Category selector replaced with checkboxes — items can now be
  assigned to multiple categories directly in admin (not just Square).
  Each category shows a "Square" label if it came from the Square
  assignment. Saves as categoriesOverride[] (array of category names).
- categoriesOverride takes precedence over old categoryOverride in the
  catalog route; old overrides still work as fallback.
- Requires-delivery toggle and custom rate fields were already in the
  code but needed container rebuild to appear — no logic change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-04-18 09:44:00 -04:00
parent 0ea1b98a1f
commit 27093bcd54
3 changed files with 61 additions and 41 deletions

View File

@ -730,8 +730,10 @@ function ItemEditor({
const [hidden, setHidden] = useState(ov.hidden ?? false)
const [featured, setFeatured] = useState(ov.featured ?? item.featured ?? false)
const [catOverride, setCatOverride] = useState(ov.categoryOverride ?? '')
const [catLabel, setCatLabel] = useState(ov.categoryLabelOverride ?? '')
// Multi-category selection: stores category names (labels). Initialise from new override or fall back to Square assignment.
const [selectedCatNames, setSelectedCatNames] = useState<string[]>(
ov.categoriesOverride ?? item.categoryLabels ?? [item.categoryLabel]
)
const [sortOrder, setSortOrder] = useState(String(ov.sortOrder ?? ''))
const [showColors, setShowColors] = useState<boolean | null>(
ov.showColors != null ? ov.showColors : null
@ -796,8 +798,8 @@ function ItemEditor({
hiddenVariationIds: hiddenVars,
hiddenModifierIds: hiddenMods,
}
if (catOverride) patch.categoryOverride = catOverride
if (catLabel) patch.categoryLabelOverride = catLabel
// Always save categoriesOverride (replaces old single-field overrides)
patch.categoriesOverride = selectedCatNames
if (sortOrder !== '') patch.sortOrder = Number(sortOrder)
if (showColors !== null) patch.showColors = showColors
if (descOverride) patch.descriptionOverride = descOverride
@ -833,8 +835,7 @@ function ItemEditor({
if (res.ok) {
setHidden(false)
setFeatured(item.featured ?? false)
setCatOverride('')
setCatLabel('')
setSelectedCatNames(item.categoryLabels ?? [item.categoryLabel])
setSortOrder('')
setShowColors(null)
setHiddenMods([])
@ -868,15 +869,13 @@ function ItemEditor({
if (!newCatName.trim()) return
setCreatingCat(true)
const cat = await onCreateCategory(newCatName.trim())
setCatOverride(cat.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''))
setCatLabel(cat.name)
// Auto-select the newly created category
if (cat.id) setSelectedCatNames((prev) => [...prev, cat.name])
setNewCatName('')
setShowNewCat(false)
setCreatingCat(false)
}
const catSlug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
return (
<div style={{ padding: '1rem', borderTop: '1px solid #eee', backgroundColor: '#fafafa' }}>
<div className="columns is-multiline">
@ -951,27 +950,31 @@ function ItemEditor({
</div>
)}
{/* Category */}
{/* Category — multi-select checkboxes */}
<div className="field">
<label className="label is-small">Category</label>
<div className="control">
<div className="select is-small is-fullwidth">
<select
value={catOverride || item._rawCategory}
<label className="label is-small">Categories <span className="has-text-grey-light" style={{ fontWeight: 'normal' }}>(item appears in all checked tabs)</span></label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 160, overflowY: 'auto', border: '1px solid #e8e8e8', borderRadius: 6, padding: '6px 8px' }}>
{categories.map((c) => (
<label key={c.id} className="checkbox" style={{ fontSize: '0.85rem' }}>
<input
type="checkbox"
checked={selectedCatNames.includes(c.name)}
onChange={(e) => {
const selected = categories.find((c) => catSlug(c.name) === e.target.value)
setCatOverride(e.target.value)
setCatLabel(selected?.name ?? e.target.value)
setSelectedCatNames((prev) =>
e.target.checked ? [...prev, c.name] : prev.filter((n) => n !== c.name)
)
}}
>
<option value={item._rawCategory}>{item._rawCategoryLabel} (Square default)</option>
{categories
.filter((c) => catSlug(c.name) !== item._rawCategory)
.map((c) => (
<option key={c.id} value={catSlug(c.name)}>{c.name}</option>
style={{ marginRight: 6 }}
/>
{c.name}
{(item.categoryLabels ?? [item.categoryLabel]).includes(c.name) && (
<span className="has-text-grey-light" style={{ fontSize: '0.72rem', marginLeft: 6 }}>Square</span>
)}
</label>
))}
</select>
</div>
{categories.length === 0 && (
<p className="is-size-7 has-text-grey">No categories found refresh from Square.</p>
)}
</div>
<button
className="button is-ghost is-small"
@ -979,7 +982,7 @@ function ItemEditor({
onClick={() => setShowNewCat(!showNewCat)}
type="button"
>
+ Create new category
+ Create new category in Square
</button>
{showNewCat && (
<div className="field has-addons" style={{ marginTop: 6 }}>

View File

@ -13,6 +13,19 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
return {
...item,
featured: ov.featured ?? item.featured,
// categoriesOverride (array of names) takes precedence over the old single-field overrides
...(ov.categoriesOverride?.length
? (() => {
const toSlug = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
const cats = ov.categoriesOverride!.map(toSlug)
return {
categories: cats,
categoryLabels: ov.categoriesOverride!,
category: cats[0],
categoryLabel: ov.categoriesOverride![0],
}
})()
: {
category: ov.categoryOverride ?? item.category,
categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
categories: ov.categoryOverride
@ -21,6 +34,8 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
categoryLabels: ov.categoryLabelOverride
? [ov.categoryLabelOverride, ...(item.categoryLabels ?? [item.categoryLabel]).slice(1)]
: (item.categoryLabels ?? [item.categoryLabel]),
}
),
showColors: ov.showColors != null ? ov.showColors : item.showColors,
colorMin: ov.colorMin ?? item.colorMin,
colorMax: ov.colorMax !== undefined ? ov.colorMax : item.colorMax,

View File

@ -24,6 +24,8 @@ export interface ItemOverride {
disabledColors?: string[]
/** Unit label for the quantity field, e.g. "ft". When set, the quantity control shows "X ft". */
quantityUnit?: string
/** Override the full list of display categories (stores category NAMES/labels). Replaces categoryOverride + categoryLabelOverride. */
categoriesOverride?: string[] | null
/** When true, pickup is not offered — item must be delivered. */
requiresDelivery?: boolean
/** Override delivery base charge in cents for this item (replaces the tier default). */