Archived
An office box is a total, not an assortment — and every size says who it feeds
I had built in an assumption nobody stated, and a ticked list made it a claim: that a $24 box contains
mini muffins AND mini scones AND mini cinnamon rolls. It doesn't. It is twelve items, mixed in sixes,
from those three. The page was describing the offer wrongly — worse than the spreadsheet, which at least
didn't say either way.
The fix keeps one rule for the whole page rather than a mode for this table: a line is something you get,
and a choice within a line is written into its name. The parties and wedding tables already read that way
("Cupcakes or sugar cookies"), so the office table becomes one line — "Any mix of mini muffins, mini
scones and mini cinnamon rolls" — with the size's item count as its cell. Three ticks became one, and the
"6+6" detail moved to the note where the bakery's own minimum already lives: "Baked in sixes, so a dozen
can be six muffins and six scones."
Confirmed, not guessed, for the other two: parties are a cake AND a dozen to hand round; a wedding
package is everything listed. Their ticked lists were already right.
SERVES. "Small" answers nothing for someone ordering for an office, which is the whole question. Columns
gain an optional `serves` — schema, view, a box in the table editor, a line under the price. The office
numbers are estimates from the item counts and are meant to be corrected by the people who pack the
boxes. The party and wedding columns are already named by head count, so theirs stay empty rather than
saying it twice.
53 tests: the office table is one mixed line, its columns say who they feed, and the head-count tables
don't repeat themselves. The blank-cell test builds its own table now, since no seeded line is blank
across any more.
This commit is contained in:
@@ -78,6 +78,7 @@ public class AdminCateringController {
|
||||
private Long id;
|
||||
private String label = "";
|
||||
private String price = "";
|
||||
private String serves = "";
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
@@ -87,6 +88,9 @@ public class AdminCateringController {
|
||||
|
||||
public String getPrice() { return price; }
|
||||
public void setPrice(String price) { this.price = price; }
|
||||
|
||||
public String getServes() { return serves; }
|
||||
public void setServes(String serves) { this.serves = serves; }
|
||||
}
|
||||
|
||||
public static class LineForm {
|
||||
@@ -172,7 +176,7 @@ public class AdminCateringController {
|
||||
form.getName(),
|
||||
form.getBlurb(),
|
||||
form.getColumns().stream()
|
||||
.map(c -> new CateringMenu.TierEdit(c.getId(), c.getLabel(), c.getPrice()))
|
||||
.map(c -> new CateringMenu.TierEdit(c.getId(), c.getLabel(), c.getPrice(), c.getServes()))
|
||||
.toList(),
|
||||
form.getLines().stream()
|
||||
.map(l -> new CateringMenu.RowEdit(l.getId(), l.getLabel(), l.getValues()))
|
||||
@@ -302,6 +306,7 @@ public class AdminCateringController {
|
||||
// The price comes back written out ("$24") and goes out again as whatever is left in the box;
|
||||
// Money reads either.
|
||||
column.setPrice(tier.price() == null ? "" : tier.price());
|
||||
column.setServes(tier.serves() == null ? "" : tier.serves());
|
||||
return column;
|
||||
}).collect(Collectors.toCollection(ArrayList::new)));
|
||||
form.setLines(table.rows().stream().map(row -> {
|
||||
|
||||
@@ -34,8 +34,11 @@ public class CateringMenu {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
/** A column. {@code price} is ready to print ("$24"), and null when the column doesn't state one. */
|
||||
public record TierView(Long id, String label, String price) {}
|
||||
/**
|
||||
* A column. {@code price} is ready to print ("$24") and null when the column doesn't state one;
|
||||
* {@code serves} is roughly how many people it feeds, and null when the label already says.
|
||||
*/
|
||||
public record TierView(Long id, String label, String price, String serves) {}
|
||||
|
||||
/** A line, with one entry per column, in column order. */
|
||||
public record RowView(Long id, String label, List<String> values) {}
|
||||
@@ -48,7 +51,7 @@ public class CateringMenu {
|
||||
|
||||
// What the admin screen sends back. A whole table at a time — see CateringPackage#arrange.
|
||||
// `price` is the raw text from the box ("24", "$24.50", ""); Money decides what it means.
|
||||
public record TierEdit(Long id, String label, String price) {}
|
||||
public record TierEdit(Long id, String label, String price, String serves) {}
|
||||
|
||||
public record RowEdit(Long id, String label, List<String> values) {}
|
||||
|
||||
@@ -99,7 +102,7 @@ public class CateringMenu {
|
||||
table.replaceNotes(clean(edit.notes()));
|
||||
table.arrange(
|
||||
orEmpty(edit.tiers()).stream()
|
||||
.map(t -> new CateringPackage.Heading(t.id(), t.label(), t.price()))
|
||||
.map(t -> new CateringPackage.Heading(t.id(), t.label(), t.price(), t.serves()))
|
||||
.toList(),
|
||||
orEmpty(edit.rows()).stream()
|
||||
.map(r -> new CateringPackage.Line(r.id(), r.label(), orEmpty(r.values())))
|
||||
@@ -171,7 +174,7 @@ public class CateringMenu {
|
||||
|
||||
private static PackageView toView(CateringPackage table) {
|
||||
List<TierView> tiers = table.getTiers().stream()
|
||||
.map(t -> new TierView(t.getId(), t.getLabel(), t.getPrice()))
|
||||
.map(t -> new TierView(t.getId(), t.getLabel(), t.getPrice(), t.getServes()))
|
||||
.toList();
|
||||
List<RowView> rows = table.getRows().stream()
|
||||
.map(r -> new RowView(r.getId(), r.getLabel(), r.getValues()))
|
||||
|
||||
@@ -78,7 +78,7 @@ public class CateringPackage extends BaseEntity {
|
||||
* A column as the editor left it — its heading and its price. A null {@code id} is one they just
|
||||
* added. (Named for the heading rather than the column so as not to shadow {@code @Column}.)
|
||||
*/
|
||||
public record Heading(Long id, String label, String price) {}
|
||||
public record Heading(Long id, String label, String price, String serves) {}
|
||||
|
||||
/** A line as the editor left it, with one value per column — blanks included. */
|
||||
public record Line(Long id, String label, List<String> values) {}
|
||||
@@ -136,7 +136,7 @@ public class CateringPackage extends BaseEntity {
|
||||
int columnNumber = 1;
|
||||
for (Heading column : columns) {
|
||||
CateringTier tier = column.id() == null ? new CateringTier(this) : tier(column.id());
|
||||
tier.describe(column.label(), column.price());
|
||||
tier.describe(column.label(), column.price(), column.serves());
|
||||
tier.moveTo(columnNumber++);
|
||||
arrangedTiers.add(tier);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@ public class CateringTier extends BaseEntity {
|
||||
@Column(name = "price_cents")
|
||||
private Integer priceCents;
|
||||
|
||||
/**
|
||||
* Roughly how many people this column feeds — "About 6–8 people".
|
||||
*
|
||||
* <p>Free text, and optional. A column named "Small" says nothing about how many it feeds, which is
|
||||
* the question anyone ordering for an office actually has; a column named "125 people" has already
|
||||
* answered it and leaves this blank rather than saying it twice.
|
||||
*/
|
||||
@Column(length = 80)
|
||||
private String serves;
|
||||
|
||||
@Column(name = "position", nullable = false)
|
||||
private int position;
|
||||
|
||||
@@ -42,11 +52,16 @@ public class CateringTier extends BaseEntity {
|
||||
this.cateringPackage = cateringPackage;
|
||||
}
|
||||
|
||||
/** @param price as the editor typed it; empty for a column that doesn't state one */
|
||||
void describe(String label, String price) {
|
||||
/**
|
||||
* @param price as the editor typed it; empty for a column that doesn't state one
|
||||
* @param serves roughly how many people, or empty
|
||||
*/
|
||||
void describe(String label, String price, String serves) {
|
||||
this.label = Text.required(label, 120,
|
||||
"Every column needs a heading — a size like \"Large\", or who it feeds like \"15–20 people\".");
|
||||
this.priceCents = Money.cents(price);
|
||||
String trimmed = Text.optional(serves, 80);
|
||||
this.serves = trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
void moveTo(int position) {
|
||||
@@ -66,6 +81,10 @@ public class CateringTier extends BaseEntity {
|
||||
return Money.format(priceCents);
|
||||
}
|
||||
|
||||
public String getServes() {
|
||||
return serves;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
-- Two things the spreadsheet never said out loud, and the page was getting wrong.
|
||||
--
|
||||
-- 1. AN OFFICE BOX IS A TOTAL, NOT AN ASSORTMENT. A Small box is twelve items, mixed in sixes from the
|
||||
-- minis — not twelve muffins AND twelve scones AND some cinnamon rolls, which is what three ticked
|
||||
-- lines on a card claimed. The bakery confirmed it: mix them, in sixes, to the size's item count.
|
||||
--
|
||||
-- The fix keeps the shape everything else uses rather than inventing a mode for this one table: the
|
||||
-- alternative goes INTO the line, the way "Cupcakes or sugar cookies" already does in the parties and
|
||||
-- wedding tables. One line, "Any mix of…", and the size's cell carries the count. So the rule for the
|
||||
-- whole page is now sayable in one sentence: lines are things you get (and), and a choice within a
|
||||
-- line is written into its name (or).
|
||||
--
|
||||
-- 2. "SMALL" DOESN'T TELL ANYONE HOW MANY PEOPLE IT FEEDS. The party and wedding tables size themselves
|
||||
-- by head count; the office table sizes itself by box. `serves` gives every column somewhere to say
|
||||
-- it, and the admin gets a box for it. The office numbers below are estimates from the item counts and
|
||||
-- are meant to be corrected by the people who pack the boxes.
|
||||
|
||||
alter table catering_tier add column serves varchar(80);
|
||||
|
||||
-- --- 1. the office box's contents ---------------------------------------------------------------------
|
||||
|
||||
update catering_row r
|
||||
set label = 'Any mix of mini muffins, mini scones and mini cinnamon rolls'
|
||||
from catering_package p
|
||||
where p.id = r.package_id
|
||||
and p.name = 'Office boxes'
|
||||
and r.label = 'Mini muffins';
|
||||
|
||||
update catering_row_value v set value = '12 items'
|
||||
from catering_row r
|
||||
where r.id = v.row_id and r.label like 'Any mix of%' and v.position = 0 and v.value = 'A dozen';
|
||||
update catering_row_value v set value = '18 items'
|
||||
from catering_row r
|
||||
where r.id = v.row_id and r.label like 'Any mix of%' and v.position = 1 and v.value = 'Eighteen';
|
||||
update catering_row_value v set value = '24 items'
|
||||
from catering_row r
|
||||
where r.id = v.row_id and r.label like 'Any mix of%' and v.position = 2 and v.value = 'Two dozen';
|
||||
|
||||
-- The other two lines are now named inside the first one. Guarded on the wording V6 left, so a line the
|
||||
-- bakery has since edited stays and can be tidied by hand.
|
||||
delete from catering_row r
|
||||
using catering_package p
|
||||
where p.id = r.package_id
|
||||
and p.name = 'Office boxes'
|
||||
and r.label in ('Mini scones', 'Mini cinnamon rolls');
|
||||
|
||||
-- "6+6" was the spreadsheet showing how a dozen scones splits by flavour. With one mixed line, that
|
||||
-- belongs in the note, where the bakery's own minimum already lives.
|
||||
update catering_package_note
|
||||
set body = 'Baked in sixes, so a dozen can be six muffins and six scones.'
|
||||
where body = 'Everything is baked in sixes, so each item comes in multiples of six.';
|
||||
|
||||
-- --- 2. how many people each column feeds -------------------------------------------------------------
|
||||
|
||||
update catering_tier t set serves = 'About 6–8 people'
|
||||
from catering_package p
|
||||
where p.id = t.package_id and p.name = 'Office boxes' and t.label = 'Small' and t.serves is null;
|
||||
update catering_tier t set serves = 'About 10–12 people'
|
||||
from catering_package p
|
||||
where p.id = t.package_id and p.name = 'Office boxes' and t.label = 'Medium' and t.serves is null;
|
||||
update catering_tier t set serves = 'About 15–20 people'
|
||||
from catering_package p
|
||||
where p.id = t.package_id and p.name = 'Office boxes' and t.label = 'Large' and t.serves is null;
|
||||
|
||||
-- The party and wedding columns are already named by head count ("15–20 people", "125 people"), so a
|
||||
-- `serves` line under the price would say it twice. Left null on purpose.
|
||||
@@ -47,6 +47,9 @@
|
||||
th:aria-label="|Heading for column ${c.count}|">
|
||||
<input class="field mt-1" th:field="*{columns[__${c.index}__].price}"
|
||||
placeholder="$24 — leave empty to ask" th:aria-label="|Price for column ${c.count}|">
|
||||
<!--/* Optional: only worth filling in when the heading isn't already a head count. */-->
|
||||
<input class="field mt-1" th:field="*{columns[__${c.index}__].serves}"
|
||||
placeholder="Serves — optional" th:aria-label="|How many people column ${c.count} feeds|">
|
||||
<div class="mt-1 flex justify-center gap-1">
|
||||
<button type="submit" name="do" th:value="|move-column:${c.index}:-1|" class="btn-icon"
|
||||
th:disabled="${c.first}" aria-label="Move this column left">←</button>
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
<p th:if="${tier.price}" class="mt-2 font-adbhashitha text-4xl text-bakery-900" th:text="${tier.price}">$24</p>
|
||||
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
|
||||
<p th:unless="${tier.price}" class="mt-2 font-adbhashitha text-2xl text-bakery-700">Ask us</p>
|
||||
<!--/* "Small" doesn't tell anyone how many it feeds. A column whose heading is already a
|
||||
head count leaves this blank rather than saying it twice. */-->
|
||||
<p th:if="${tier.serves}" class="mt-1 text-sm text-bakery-600" th:text="${tier.serves}">About 6–8 people</p>
|
||||
|
||||
<ul class="mt-6 space-y-3 text-bakery-800">
|
||||
<li th:each="row : ${table.rows}" class="flex gap-3">
|
||||
|
||||
Reference in New Issue
Block a user