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

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

View File

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

View File

@ -24,6 +24,8 @@ export interface ItemOverride {
disabledColors?: string[] disabledColors?: string[]
/** Unit label for the quantity field, e.g. "ft". When set, the quantity control shows "X ft". */ /** Unit label for the quantity field, e.g. "ft". When set, the quantity control shows "X ft". */
quantityUnit?: string 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. */ /** When true, pickup is not offered — item must be delivered. */
requiresDelivery?: boolean requiresDelivery?: boolean
/** Override delivery base charge in cents for this item (replaces the tier default). */ /** Override delivery base charge in cents for this item (replaces the tier default). */