Compare commits

..

No commits in common. "27093bcd54ae72e75eb9409736843e3ed548858d" and "107ef43a0eb7f34c38861b1a1d2bceb32cac2982" have entirely different histories.

7 changed files with 88 additions and 227 deletions

View File

@ -730,10 +730,8 @@ 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)
// Multi-category selection: stores category names (labels). Initialise from new override or fall back to Square assignment. const [catOverride, setCatOverride] = useState(ov.categoryOverride ?? '')
const [selectedCatNames, setSelectedCatNames] = useState<string[]>( const [catLabel, setCatLabel] = useState(ov.categoryLabelOverride ?? '')
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
@ -764,13 +762,6 @@ function ItemEditor({
const [disabledColors, setDisabledColors] = useState<string[]>(ov.disabledColors ?? []) const [disabledColors, setDisabledColors] = useState<string[]>(ov.disabledColors ?? [])
const [showColorFilter, setShowColorFilter] = useState(false) const [showColorFilter, setShowColorFilter] = useState(false)
const [quantityUnit, setQuantityUnit] = useState<string>(ov.quantityUnit ?? '') const [quantityUnit, setQuantityUnit] = useState<string>(ov.quantityUnit ?? '')
const [requiresDelivery, setRequiresDelivery] = useState(ov.requiresDelivery ?? false)
const [deliveryBase, setDeliveryBase] = useState<string>(
ov.deliveryBaseOverride != null ? String(ov.deliveryBaseOverride / 100) : ''
)
const [deliveryPerMile, setDeliveryPerMile] = useState<string>(
ov.deliveryPerMileOverride != null ? String(ov.deliveryPerMileOverride / 100) : ''
)
// Create category // Create category
const [newCatName, setNewCatName] = useState('') const [newCatName, setNewCatName] = useState('')
@ -798,8 +789,8 @@ function ItemEditor({
hiddenVariationIds: hiddenVars, hiddenVariationIds: hiddenVars,
hiddenModifierIds: hiddenMods, hiddenModifierIds: hiddenMods,
} }
// Always save categoriesOverride (replaces old single-field overrides) if (catOverride) patch.categoryOverride = catOverride
patch.categoriesOverride = selectedCatNames if (catLabel) patch.categoryLabelOverride = catLabel
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
@ -810,9 +801,6 @@ function ItemEditor({
patch.disabledColors = disabledColors.length ? disabledColors : undefined patch.disabledColors = disabledColors.length ? disabledColors : undefined
if (quantityUnit.trim()) patch.quantityUnit = quantityUnit.trim() if (quantityUnit.trim()) patch.quantityUnit = quantityUnit.trim()
else patch.quantityUnit = undefined else patch.quantityUnit = undefined
patch.requiresDelivery = requiresDelivery || undefined
patch.deliveryBaseOverride = deliveryBase !== '' ? Math.round(Number(deliveryBase) * 100) : null
patch.deliveryPerMileOverride = deliveryPerMile !== '' ? Math.round(Number(deliveryPerMile) * 100) : null
const res = await fetch(`${BASE}/api/admin/items/${item.id}`, { const res = await fetch(`${BASE}/api/admin/items/${item.id}`, {
method: 'PATCH', method: 'PATCH',
@ -835,7 +823,8 @@ function ItemEditor({
if (res.ok) { if (res.ok) {
setHidden(false) setHidden(false)
setFeatured(item.featured ?? false) setFeatured(item.featured ?? false)
setSelectedCatNames(item.categoryLabels ?? [item.categoryLabel]) setCatOverride('')
setCatLabel('')
setSortOrder('') setSortOrder('')
setShowColors(null) setShowColors(null)
setHiddenMods([]) setHiddenMods([])
@ -844,9 +833,6 @@ function ItemEditor({
setColorMin('') setColorMin('')
setColorMax('') setColorMax('')
setChromeSurcharge('') setChromeSurcharge('')
setRequiresDelivery(false)
setDeliveryBase('')
setDeliveryPerMile('')
onSaved(item.id, {}) onSaved(item.id, {})
} }
} }
@ -869,13 +855,15 @@ 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())
// Auto-select the newly created category setCatOverride(cat.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''))
if (cat.id) setSelectedCatNames((prev) => [...prev, cat.name]) setCatLabel(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">
@ -903,78 +891,29 @@ function ItemEditor({
/> />
Featured Featured
</label> </label>
<label className="checkbox" style={{ fontWeight: 600, color: '#c0392b' }}>
<input
type="checkbox"
checked={requiresDelivery}
onChange={(e) => setRequiresDelivery(e.target.checked)}
style={{ marginRight: 6, accentColor: '#c0392b' }}
/>
🚗 Requires delivery
</label>
</div> </div>
{requiresDelivery && ( {/* Category */}
<div className="field" style={{ background: '#fff8f8', border: '1px solid #f5c6c6', borderRadius: 6, padding: '0.6rem 0.8rem', marginBottom: '0.75rem' }}>
<p className="is-size-7 has-text-grey" style={{ marginBottom: '0.4rem' }}>
Custom delivery rates for this item (leave blank to use global tier defaults)
</p>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<div>
<label className="label is-small" style={{ marginBottom: 2 }}>Base charge ($)</label>
<input
className="input is-small"
type="number"
min="0"
step="0.01"
placeholder="e.g. 75.00"
value={deliveryBase}
onChange={(e) => setDeliveryBase(e.target.value)}
style={{ width: 110 }}
/>
</div>
<div>
<label className="label is-small" style={{ marginBottom: 2 }}>Per mile ($)</label>
<input
className="input is-small"
type="number"
min="0"
step="0.01"
placeholder="e.g. 4.00"
value={deliveryPerMile}
onChange={(e) => setDeliveryPerMile(e.target.value)}
style={{ width: 110 }}
/>
</div>
</div>
</div>
)}
{/* Category — multi-select checkboxes */}
<div className="field"> <div className="field">
<label className="label is-small">Categories <span className="has-text-grey-light" style={{ fontWeight: 'normal' }}>(item appears in all checked tabs)</span></label> <label className="label is-small">Category</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 160, overflowY: 'auto', border: '1px solid #e8e8e8', borderRadius: 6, padding: '6px 8px' }}> <div className="control">
{categories.map((c) => ( <div className="select is-small is-fullwidth">
<label key={c.id} className="checkbox" style={{ fontSize: '0.85rem' }}> <select
<input value={catOverride || item._rawCategory}
type="checkbox"
checked={selectedCatNames.includes(c.name)}
onChange={(e) => { onChange={(e) => {
setSelectedCatNames((prev) => const selected = categories.find((c) => catSlug(c.name) === e.target.value)
e.target.checked ? [...prev, c.name] : prev.filter((n) => n !== c.name) setCatOverride(e.target.value)
) setCatLabel(selected?.name ?? e.target.value)
}} }}
style={{ marginRight: 6 }} >
/> <option value={item._rawCategory}>{item._rawCategoryLabel} (Square default)</option>
{c.name} {categories
{(item.categoryLabels ?? [item.categoryLabel]).includes(c.name) && ( .filter((c) => catSlug(c.name) !== item._rawCategory)
<span className="has-text-grey-light" style={{ fontSize: '0.72rem', marginLeft: 6 }}>Square</span> .map((c) => (
)} <option key={c.id} value={catSlug(c.name)}>{c.name}</option>
</label>
))} ))}
{categories.length === 0 && ( </select>
<p className="is-size-7 has-text-grey">No categories found refresh from Square.</p> </div>
)}
</div> </div>
<button <button
className="button is-ghost is-small" className="button is-ghost is-small"
@ -982,7 +921,7 @@ function ItemEditor({
onClick={() => setShowNewCat(!showNewCat)} onClick={() => setShowNewCat(!showNewCat)}
type="button" type="button"
> >
+ Create new category in Square + Create new category
</button> </button>
{showNewCat && ( {showNewCat && (
<div className="field has-addons" style={{ marginTop: 6 }}> <div className="field has-addons" style={{ marginTop: 6 }}>

View File

@ -13,19 +13,6 @@ 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
@ -34,17 +21,12 @@ 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,
chromeSurchargePerColor: ov.chromeSurchargePerColor ?? item.chromeSurchargePerColor, chromeSurchargePerColor: ov.chromeSurchargePerColor ?? item.chromeSurchargePerColor,
disabledColors: ov.disabledColors?.length ? ov.disabledColors : item.disabledColors, disabledColors: ov.disabledColors?.length ? ov.disabledColors : item.disabledColors,
quantityUnit: ov.quantityUnit ?? item.quantityUnit, quantityUnit: ov.quantityUnit ?? item.quantityUnit,
requiresDelivery: ov.requiresDelivery != null ? ov.requiresDelivery : item.requiresDelivery,
deliveryBaseOverride: ov.deliveryBaseOverride !== undefined ? ov.deliveryBaseOverride : item.deliveryBaseOverride,
deliveryPerMileOverride: ov.deliveryPerMileOverride !== undefined ? ov.deliveryPerMileOverride : item.deliveryPerMileOverride,
description: ov.descriptionOverride ?? item.description, description: ov.descriptionOverride ?? item.description,
variations: item.variations variations: item.variations
.filter((v) => !(ov.hiddenVariationIds ?? []).includes(v.id)), .filter((v) => !(ov.hiddenVariationIds ?? []).includes(v.id)),

View File

@ -3,10 +3,9 @@ import { geocode, calcDelivery, inferTier } from '@/lib/delivery'
import { readDeliveryRates } from '@/lib/delivery-rates' import { readDeliveryRates } from '@/lib/delivery-rates'
export async function POST(request: Request) { export async function POST(request: Request) {
const { address, itemNames, rateOverride } = await request.json() as { const { address, itemNames } = await request.json() as {
address: string address: string
itemNames: string[] itemNames: string[]
rateOverride?: { base: number; perMile: number }
} }
if (!address?.trim()) { if (!address?.trim()) {
@ -20,16 +19,6 @@ export async function POST(request: Request) {
const tier = inferTier(itemNames ?? []) const tier = inferTier(itemNames ?? [])
const rates = readDeliveryRates() const rates = readDeliveryRates()
// Apply per-item rate override if provided (overrides just base and perMile for the inferred tier)
if (rateOverride) {
rates[tier] = {
...rates[tier],
base: rateOverride.base,
perMile: rateOverride.perMile,
}
}
const quote = await calcDelivery(coords.lat, coords.lng, tier, rates) const quote = await calcDelivery(coords.lat, coords.lng, tier, rates)
if (quote.miles > 40) { if (quote.miles > 40) {

View File

@ -67,26 +67,6 @@ export default function CartDrawer() {
const [shortRef, setShortRef] = useState<string | null>(null) const [shortRef, setShortRef] = useState<string | null>(null)
const [fulfillmentType, setFulfillmentType] = useState<'delivery' | 'pickup'>('pickup') const [fulfillmentType, setFulfillmentType] = useState<'delivery' | 'pickup'>('pickup')
// If any item requires delivery, force delivery mode and suppress pickup option
const cartRequiresDelivery = useMemo(
() => entries.some((e) => e.product.requiresDelivery),
[entries]
)
// Effective fulfillment type — pickup blocked when any item requires delivery
const effectiveFulfillment = cartRequiresDelivery ? 'delivery' : fulfillmentType
// Merged delivery rate override: highest base + highest perMile across requires-delivery items
const deliveryRateOverride = useMemo(() => {
const overrideItems = entries.filter(
(e) => e.product.requiresDelivery &&
(e.product.deliveryBaseOverride != null || e.product.deliveryPerMileOverride != null)
)
if (!overrideItems.length) return undefined
const base = Math.max(...overrideItems.map((e) => e.product.deliveryBaseOverride ?? 0))
const perMile = Math.max(...overrideItems.map((e) => e.product.deliveryPerMileOverride ?? 0))
return { base, perMile }
}, [entries])
// Delivery step — persisted // Delivery step — persisted
const [street, setStreet] = useStoredString('bpb_street', '') const [street, setStreet] = useStoredString('bpb_street', '')
const [city, setCity] = useStoredString('bpb_city', '') const [city, setCity] = useStoredString('bpb_city', '')
@ -175,7 +155,7 @@ export default function CartDrawer() {
const CT_TAX_RATE = 0.0635 const CT_TAX_RATE = 0.0635
const subtotal = entries.reduce((sum, e) => sum + entryUnitPrice(e) * e.quantity, 0) const subtotal = entries.reduce((sum, e) => sum + entryUnitPrice(e) * e.quantity, 0)
const deliveryTotal = effectiveFulfillment === 'delivery' ? (quote?.totalCents ?? 0) : 0 const deliveryTotal = fulfillmentType === 'delivery' ? (quote?.totalCents ?? 0) : 0
const taxCents = Math.round(subtotal * CT_TAX_RATE) const taxCents = Math.round(subtotal * CT_TAX_RATE)
const grandTotal = subtotal + deliveryTotal + taxCents const grandTotal = subtotal + deliveryTotal + taxCents
@ -198,13 +178,13 @@ export default function CartDrawer() {
}), }),
})), })),
selectedColors: entries.flatMap((e) => e.selectedColors), selectedColors: entries.flatMap((e) => e.selectedColors),
deliverySlotISO: effectiveFulfillment === 'delivery' ? deliverySlot?.slotISO : undefined, deliverySlotISO: fulfillmentType === 'delivery' ? deliverySlot?.slotISO : undefined,
driveMinutes: effectiveFulfillment === 'delivery' ? deliverySlot?.driveMinutes : undefined, driveMinutes: fulfillmentType === 'delivery' ? deliverySlot?.driveMinutes : undefined,
deliveryAddress: effectiveFulfillment === 'delivery' ? (fullAddress || undefined) : undefined, deliveryAddress: fulfillmentType === 'delivery' ? (fullAddress || undefined) : undefined,
deliveryTier: effectiveFulfillment === 'delivery' ? quote?.tier : undefined, deliveryTier: fulfillmentType === 'delivery' ? quote?.tier : undefined,
deliveryNotes: effectiveFulfillment === 'delivery' && deliveryInstructions ? deliveryInstructions : undefined, deliveryNotes: fulfillmentType === 'delivery' && deliveryInstructions ? deliveryInstructions : undefined,
deliveryCents: effectiveFulfillment === 'delivery' ? (quote?.totalCents ?? 0) : undefined, deliveryCents: fulfillmentType === 'delivery' ? (quote?.totalCents ?? 0) : undefined,
pickupSlotISO: effectiveFulfillment === 'pickup' ? pickupSlot?.slotISO : undefined, pickupSlotISO: fulfillmentType === 'pickup' ? pickupSlot?.slotISO : undefined,
customerFirstName: custFirst, customerFirstName: custFirst,
customerLastName: custLast, customerLastName: custLast,
customerEmail: custEmail, customerEmail: custEmail,
@ -212,7 +192,7 @@ export default function CartDrawer() {
grandTotal, grandTotal,
idempotencyKey: checkoutKey || undefined, idempotencyKey: checkoutKey || undefined,
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}), [entries, effectiveFulfillment, deliverySlot, pickupSlot, fullAddress, quote, deliveryInstructions, custFirst, custLast, custEmail, custPhone, grandTotal, entryUnitPrice]) }), [entries, fulfillmentType, deliverySlot, pickupSlot, fullAddress, quote, deliveryInstructions, custFirst, custLast, custEmail, custPhone, grandTotal, entryUnitPrice])
const handleSuccess = (id: string, ref: string) => { const handleSuccess = (id: string, ref: string) => {
setOrderId(id) setOrderId(id)
@ -244,11 +224,7 @@ export default function CartDrawer() {
const res = await fetch(BASE + '/api/delivery-quote', { const res = await fetch(BASE + '/api/delivery-quote', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({ address: fullAddress, itemNames: entries.map((e) => e.product.name) }),
address: fullAddress,
itemNames: entries.map((e) => e.product.name),
rateOverride: deliveryRateOverride,
}),
}) })
const data = await res.json() const data = await res.json()
if (!res.ok) { setQuoteErr(data.error ?? 'Could not calculate delivery.'); return } if (!res.ok) { setQuoteErr(data.error ?? 'Could not calculate delivery.'); return }
@ -337,11 +313,6 @@ export default function CartDrawer() {
)} )}
{/* Fulfillment toggle */} {/* Fulfillment toggle */}
{cartRequiresDelivery ? (
<p style={{ fontSize: '0.8rem', color: '#555', marginBottom: '0.75rem', background: '#f5f5f5', padding: '7px 10px', borderRadius: 6 }}>
🚗 One or more items require delivery &amp; setup pickup is not available for this order.
</p>
) : (
<div style={{ display: 'flex', gap: '6px', marginBottom: '0.75rem' }}> <div style={{ display: 'flex', gap: '6px', marginBottom: '0.75rem' }}>
{(['delivery', 'pickup'] as const).map((type) => ( {(['delivery', 'pickup'] as const).map((type) => (
<button <button
@ -351,23 +322,22 @@ export default function CartDrawer() {
style={{ style={{
flex: 1, padding: '7px 4px', fontSize: '0.82rem', flex: 1, padding: '7px 4px', fontSize: '0.82rem',
borderRadius: '6px', cursor: 'pointer', fontFamily: 'inherit', borderRadius: '6px', cursor: 'pointer', fontFamily: 'inherit',
border: `1px solid ${effectiveFulfillment === type ? '#11b3be' : '#d0d0d0'}`, border: `1px solid ${fulfillmentType === type ? '#11b3be' : '#d0d0d0'}`,
background: effectiveFulfillment === type ? '#11b3be' : '#fff', background: fulfillmentType === type ? '#11b3be' : '#fff',
color: effectiveFulfillment === type ? '#fff' : '#555', color: fulfillmentType === type ? '#fff' : '#555',
fontWeight: effectiveFulfillment === type ? 'bold' : 'normal', fontWeight: fulfillmentType === type ? 'bold' : 'normal',
}} }}
> >
{type === 'delivery' ? '🚗 Delivery' : '🏪 Pick Up'} {type === 'delivery' ? '🚗 Delivery' : '🏪 Pick Up'}
</button> </button>
))} ))}
</div> </div>
)}
<button <button
className="button is-info is-fullwidth" className="button is-info is-fullwidth"
onClick={() => setStep('delivery')} onClick={() => setStep('delivery')}
> >
{effectiveFulfillment === 'pickup' ? 'Choose Pickup Time →' : 'Continue to Delivery →'} {fulfillmentType === 'pickup' ? 'Choose Pickup Time →' : 'Continue to Delivery →'}
</button> </button>
</> </>
) )
@ -521,14 +491,14 @@ export default function CartDrawer() {
const deliveryFooter = ( const deliveryFooter = (
<> <>
{effectiveFulfillment === 'delivery' && ( {fulfillmentType === 'delivery' && (
<p style={{ fontSize: '0.72rem', color: '#999', marginBottom: '0.5rem' }}> <p style={{ fontSize: '0.72rem', color: '#999', marginBottom: '0.5rem' }}>
Delivery fee is based on driving distance from our shop. Delivery fee is based on driving distance from our shop.
</p> </p>
)} )}
<button <button
className="button is-info is-fullwidth" className="button is-info is-fullwidth"
disabled={effectiveFulfillment === 'delivery' ? (!quote || !deliverySlot) : !pickupSlot} disabled={fulfillmentType === 'delivery' ? (!quote || !deliverySlot) : !pickupSlot}
onClick={() => setStep('info')} onClick={() => setStep('info')}
> >
Continue to Your Info Continue to Your Info
@ -613,7 +583,7 @@ export default function CartDrawer() {
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
<span>Items</span><span>{fmt(subtotal)}</span> <span>Items</span><span>{fmt(subtotal)}</span>
</div> </div>
{effectiveFulfillment === 'delivery' && quote && ( {fulfillmentType === 'delivery' && quote && (
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
<span>Delivery</span><span>{fmt(quote.totalCents)}</span> <span>Delivery</span><span>{fmt(quote.totalCents)}</span>
</div> </div>
@ -624,12 +594,12 @@ export default function CartDrawer() {
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 'bold', borderTop: '1px solid #ddd', paddingTop: '4px', marginTop: '4px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 'bold', borderTop: '1px solid #ddd', paddingTop: '4px', marginTop: '4px' }}>
<span>Estimated total</span><span>{fmt(grandTotal)}</span> <span>Estimated total</span><span>{fmt(grandTotal)}</span>
</div> </div>
{effectiveFulfillment === 'delivery' && deliverySlot && ( {fulfillmentType === 'delivery' && deliverySlot && (
<p style={{ color: '#555', marginTop: '6px' }}> <p style={{ color: '#555', marginTop: '6px' }}>
Delivery: {deliverySlot.date} at {deliverySlot.label} Delivery: {deliverySlot.date} at {deliverySlot.label}
</p> </p>
)} )}
{effectiveFulfillment === 'pickup' && pickupSlot && ( {fulfillmentType === 'pickup' && pickupSlot && (
<p style={{ color: '#555', marginTop: '6px' }}> <p style={{ color: '#555', marginTop: '6px' }}>
Pickup: {pickupSlot.date} at {pickupSlot.label} Pickup: {pickupSlot.date} at {pickupSlot.label}
</p> </p>
@ -686,7 +656,7 @@ export default function CartDrawer() {
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
<span>Items</span><span>{fmt(subtotal)}</span> <span>Items</span><span>{fmt(subtotal)}</span>
</div> </div>
{effectiveFulfillment === 'delivery' && quote && ( {fulfillmentType === 'delivery' && quote && (
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
<span>Delivery</span><span>{fmt(quote.totalCents)}</span> <span>Delivery</span><span>{fmt(quote.totalCents)}</span>
</div> </div>
@ -697,12 +667,12 @@ export default function CartDrawer() {
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 'bold', borderTop: '1px solid #ddd', paddingTop: '4px', marginTop: '4px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 'bold', borderTop: '1px solid #ddd', paddingTop: '4px', marginTop: '4px' }}>
<span>Total</span><span>{fmt(grandTotal)}</span> <span>Total</span><span>{fmt(grandTotal)}</span>
</div> </div>
{effectiveFulfillment === 'delivery' && deliverySlot && ( {fulfillmentType === 'delivery' && deliverySlot && (
<p style={{ color: '#555', marginTop: '6px' }}> <p style={{ color: '#555', marginTop: '6px' }}>
Delivery: {deliverySlot.date} at {deliverySlot.label} Delivery: {deliverySlot.date} at {deliverySlot.label}
</p> </p>
)} )}
{effectiveFulfillment === 'pickup' && pickupSlot && ( {fulfillmentType === 'pickup' && pickupSlot && (
<p style={{ color: '#555', marginTop: '6px' }}> <p style={{ color: '#555', marginTop: '6px' }}>
Pickup: {pickupSlot.date} at {pickupSlot.label} Pickup: {pickupSlot.date} at {pickupSlot.label}
</p> </p>
@ -714,7 +684,7 @@ export default function CartDrawer() {
const bodyContent: Record<Step, React.ReactNode> = { const bodyContent: Record<Step, React.ReactNode> = {
cart: cartBody, cart: cartBody,
delivery: effectiveFulfillment === 'pickup' ? pickupBody : deliveryBody, delivery: fulfillmentType === 'pickup' ? pickupBody : deliveryBody,
info: infoBody, info: infoBody,
payment: paymentSummary, // PaymentForm rendered separately below, always mounted payment: paymentSummary, // PaymentForm rendered separately below, always mounted
} }
@ -786,7 +756,7 @@ export default function CartDrawer() {
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg> </svg>
)} )}
{step === 'delivery' && effectiveFulfillment === 'pickup' ? 'Pickup Time' : STEP_TITLES[step]} {step === 'delivery' && fulfillmentType === 'pickup' ? 'Pickup Time' : STEP_TITLES[step]}
{step === 'cart' && totalItems > 0 && ` (${totalItems})`} {step === 'cart' && totalItems > 0 && ` (${totalItems})`}
</strong> </strong>
{/* Step indicator dots */} {/* Step indicator dots */}
@ -823,17 +793,17 @@ export default function CartDrawer() {
</p> </p>
<p style={{ color: '#555', fontSize: '0.88rem', marginBottom: '1rem', lineHeight: 1.5 }}> <p style={{ color: '#555', fontSize: '0.88rem', marginBottom: '1rem', lineHeight: 1.5 }}>
Order <strong>#{shortRef}</strong> confirmed.{' '} Order <strong>#{shortRef}</strong> confirmed.{' '}
{effectiveFulfillment === 'pickup' {fulfillmentType === 'pickup'
? <>Your pickup is all set see you at the shop! A confirmation will be sent to <strong>{custEmail}</strong>.</> ? <>Your pickup is all set see you at the shop! A confirmation will be sent to <strong>{custEmail}</strong>.</>
: <>We&rsquo;ll reach out to <strong>{custEmail}</strong> to confirm final delivery details.</> : <>We&rsquo;ll reach out to <strong>{custEmail}</strong> to confirm final delivery details.</>
} }
</p> </p>
{effectiveFulfillment === 'delivery' && deliverySlot && ( {fulfillmentType === 'delivery' && deliverySlot && (
<p style={{ color: '#0d6e75', fontSize: '0.85rem', marginBottom: '1.5rem' }}> <p style={{ color: '#0d6e75', fontSize: '0.85rem', marginBottom: '1.5rem' }}>
Delivery: {deliverySlot.date} at {deliverySlot.label} Delivery: {deliverySlot.date} at {deliverySlot.label}
</p> </p>
)} )}
{effectiveFulfillment === 'pickup' && pickupSlot && ( {fulfillmentType === 'pickup' && pickupSlot && (
<p style={{ color: '#0d6e75', fontSize: '0.85rem', marginBottom: '1.5rem' }}> <p style={{ color: '#0d6e75', fontSize: '0.85rem', marginBottom: '1.5rem' }}>
Pickup: {pickupSlot.date} at {pickupSlot.label} Pickup: {pickupSlot.date} at {pickupSlot.label}
</p> </p>

View File

@ -89,11 +89,6 @@ export default function ProductCard({ item }: Props) {
Only {stock} left Only {stock} left
</p> </p>
)} )}
{item.requiresDelivery && (
<p style={{ fontSize: '0.78rem', color: '#555', fontWeight: 600, marginBottom: '0.35rem' }}>
🚗 Delivery &amp; setup required
</p>
)}
<p className="is-size-7">{item.description}</p> <p className="is-size-7">{item.description}</p>
{item.tags.length > 0 && ( {item.tags.length > 0 && (

View File

@ -46,12 +46,6 @@ export interface CatalogItem {
variations: CatalogVariation[] // all enabled variations; first is the default variations: CatalogVariation[] // all enabled variations; first is the default
/** Unit label for quantity, e.g. "ft". Omitted for plain count items. */ /** Unit label for quantity, e.g. "ft". Omitted for plain count items. */
quantityUnit?: string quantityUnit?: string
/** When true, this item cannot be picked up — delivery is required. */
requiresDelivery?: boolean
/** Per-item delivery base charge override in cents. null = use tier default. */
deliveryBaseOverride?: number | null
/** Per-item per-mile rate override in cents. null = use tier default. */
deliveryPerMileOverride?: number | null
} }
export const MOCK_CATALOG: CatalogItem[] = (([ export const MOCK_CATALOG: CatalogItem[] = (([

View File

@ -24,14 +24,6 @@ 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. */
requiresDelivery?: boolean
/** Override delivery base charge in cents for this item (replaces the tier default). */
deliveryBaseOverride?: number | null
/** Override per-mile rate in cents for this item (replaces the tier default). */
deliveryPerMileOverride?: number | null
} }
export type OverridesMap = Record<string, ItemOverride> export type OverridesMap = Record<string, ItemOverride>