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 [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
type="checkbox"
checked={selectedCatNames.includes(c.name)}
onChange={(e) => { onChange={(e) => {
const selected = categories.find((c) => catSlug(c.name) === e.target.value) setSelectedCatNames((prev) =>
setCatOverride(e.target.value) e.target.checked ? [...prev, c.name] : prev.filter((n) => n !== c.name)
setCatLabel(selected?.name ?? e.target.value) )
}} }}
> style={{ marginRight: 6 }}
<option value={item._rawCategory}>{item._rawCategoryLabel} (Square default)</option> />
{categories {c.name}
.filter((c) => catSlug(c.name) !== item._rawCategory) {(item.categoryLabels ?? [item.categoryLabel]).includes(c.name) && (
.map((c) => ( <span className="has-text-grey-light" style={{ fontSize: '0.72rem', marginLeft: 6 }}>Square</span>
<option key={c.id} value={catSlug(c.name)}>{c.name}</option> )}
</label>
))} ))}
</select> {categories.length === 0 && (
</div> <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,6 +13,19 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
return { return {
...item, ...item,
featured: ov.featured ?? item.featured, 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, category: ov.categoryOverride ?? item.category,
categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel, categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
categories: ov.categoryOverride categories: ov.categoryOverride
@ -21,6 +34,8 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
categoryLabels: ov.categoryLabelOverride categoryLabels: ov.categoryLabelOverride
? [ov.categoryLabelOverride, ...(item.categoryLabels ?? [item.categoryLabel]).slice(1)] ? [ov.categoryLabelOverride, ...(item.categoryLabels ?? [item.categoryLabel]).slice(1)]
: (item.categoryLabels ?? [item.categoryLabel]), : (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). */