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:
parent
0ea1b98a1f
commit
27093bcd54
@ -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 }}>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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). */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user