Group bins by letter, sort by number, drop location
Build and push image / build (push) Successful in 46s

Bins follow an A1/A2/B1 naming convention, so the Bins page now parses
the leading letter prefix as a row group and the trailing number as the
within-row order. Each letter starts a fresh grid section; bins whose
names don't match the pattern fall into a trailing "Other" bucket
sorted alphabetically.

Removes the optional location field from bins end to end: the API
client signatures, server POST/PATCH routes, both product-flow inline
creates, the dropdown labels, the ProductDetail bin row, and the
BinsView header line. The bootstrap query explicitly projects only
id/name/capacity so the dead column doesn't leak through.

The location column stays in the bins table on disk to avoid a
migration on existing deployments — it just isn't read or written.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 22:07:12 -04:00
parent cd7aeb9d09
commit d335525073
9 changed files with 245 additions and 272 deletions
+2 -11
View File
@@ -34,7 +34,6 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
const [newShopName, setNewShopName] = useState("");
const [newShopLocation, setNewShopLocation] = useState("");
const [newBinName, setNewBinName] = useState("");
const [newBinLocation, setNewBinLocation] = useState("");
const [newBinCapacity, setNewBinCapacity] = useState(10);
const [error, setError] = useState<string | null>(null);
@@ -98,7 +97,6 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
if (!newBinName.trim()) throw new Error("New bin name required");
const b = await api.createBin({
name: newBinName.trim(),
location: newBinLocation.trim(),
capacity: newBinCapacity,
});
binId = b.id;
@@ -200,7 +198,7 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
<Field label="Bin">
<Select value={form.binId} onChange={(e) => update("binId", e.target.value)}>
{data.bins.map((b) => (
<option key={b.id} value={b.id}>{b.name} {b.location}</option>
<option key={b.id} value={b.id}>{b.name}</option>
))}
<option value={NEW_BIN}>+ Add new bin</option>
</Select>
@@ -211,14 +209,7 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
<Input
value={newBinName}
onChange={(e) => setNewBinName(e.target.value)}
placeholder="e.g. Top Drawer"
/>
</Field>
<Field label="Location (optional)">
<Input
value={newBinLocation}
onChange={(e) => setNewBinLocation(e.target.value)}
placeholder="e.g. Bedroom"
placeholder="e.g. A1"
/>
</Field>
<Field label="Capacity">
+27 -49
View File
@@ -125,16 +125,14 @@ export function EditBinModal({
bin,
onClose,
}: {
bin: { id: string; name: string; location: string | null; capacity: number };
bin: { id: string; name: string; capacity: number };
onClose: () => void;
}) {
const qc = useQueryClient();
const [name, setName] = useState(bin.name);
const [location, setLocation] = useState(bin.location ?? "");
const [capacity, setCapacity] = useState(bin.capacity);
const update = useMutation({
mutationFn: () =>
api.updateBin(bin.id, { name: name.trim(), location: location.trim(), capacity }),
mutationFn: () => api.updateBin(bin.id, { name: name.trim(), capacity }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["bootstrap"] });
onClose();
@@ -145,7 +143,7 @@ export function EditBinModal({
<ModalBackdrop onClose={onClose}>
<div
style={{
width: "min(560px, 96vw)",
width: "min(480px, 96vw)",
margin: "40px 20px",
background: "var(--bg)",
border: "1px solid var(--line)",
@@ -154,33 +152,24 @@ export function EditBinModal({
}}
>
<ModalHeader title="Edit bin" eyebrow="Storage" onClose={onClose} />
<div style={{ padding: 32, display: "grid", gap: 16 }}>
<div style={{ padding: 32, display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<Field label="Bin name">
<Input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Top Drawer"
placeholder="e.g. A1"
/>
</Field>
<Field label="Capacity">
<Input
type="number"
min={1}
step={1}
value={capacity}
onChange={(e) => setCapacity(Math.max(1, Math.floor(+e.target.value || 1)))}
/>
</Field>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<Field label="Location (optional)">
<Input
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Bedroom"
/>
</Field>
<Field label="Capacity">
<Input
type="number"
min={1}
step={1}
value={capacity}
onChange={(e) => setCapacity(Math.max(1, Math.floor(+e.target.value || 1)))}
/>
</Field>
</div>
</div>
<ModalFooter>
<div />
@@ -204,11 +193,9 @@ export function EditBinModal({
export function AddBinModal({ onClose }: { onClose: () => void }) {
const qc = useQueryClient();
const [name, setName] = useState("");
const [location, setLocation] = useState("");
const [capacity, setCapacity] = useState(10);
const create = useMutation({
mutationFn: () =>
api.createBin({ name: name.trim(), location: location.trim(), capacity }),
mutationFn: () => api.createBin({ name: name.trim(), capacity }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["bootstrap"] });
onClose();
@@ -219,7 +206,7 @@ export function AddBinModal({ onClose }: { onClose: () => void }) {
<ModalBackdrop onClose={onClose}>
<div
style={{
width: "min(560px, 96vw)",
width: "min(480px, 96vw)",
margin: "40px 20px",
background: "var(--bg)",
border: "1px solid var(--line)",
@@ -228,33 +215,24 @@ export function AddBinModal({ onClose }: { onClose: () => void }) {
}}
>
<ModalHeader title="Add a bin" eyebrow="Storage" onClose={onClose} />
<div style={{ padding: 32, display: "grid", gap: 16 }}>
<div style={{ padding: 32, display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<Field label="Bin name">
<Input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Top Drawer"
placeholder="e.g. A1"
/>
</Field>
<Field label="Capacity">
<Input
type="number"
min={1}
step={1}
value={capacity}
onChange={(e) => setCapacity(Math.max(1, Math.floor(+e.target.value || 1)))}
/>
</Field>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<Field label="Location (optional)">
<Input
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Bedroom"
/>
</Field>
<Field label="Capacity">
<Input
type="number"
min={1}
step={1}
value={capacity}
onChange={(e) => setCapacity(Math.max(1, Math.floor(+e.target.value || 1)))}
/>
</Field>
</div>
</div>
<ModalFooter>
<div />
+2 -11
View File
@@ -48,7 +48,6 @@ export function EditProductFlow({
const [newShopName, setNewShopName] = useState("");
const [newShopLocation, setNewShopLocation] = useState("");
const [newBinName, setNewBinName] = useState("");
const [newBinLocation, setNewBinLocation] = useState("");
const [newBinCapacity, setNewBinCapacity] = useState(10);
const [error, setError] = useState<string | null>(null);
@@ -79,7 +78,6 @@ export function EditProductFlow({
if (!newBinName.trim()) throw new Error("New bin name required");
const b = await api.createBin({
name: newBinName.trim(),
location: newBinLocation.trim(),
capacity: newBinCapacity,
});
binId = b.id;
@@ -195,7 +193,7 @@ export function EditProductFlow({
<Field label="Bin">
<Select value={form.binId} onChange={(e) => update("binId", e.target.value)}>
{data.bins.map((b) => (
<option key={b.id} value={b.id}>{b.name} {b.location}</option>
<option key={b.id} value={b.id}>{b.name}</option>
))}
<option value={NEW_BIN}>+ Add new bin</option>
</Select>
@@ -213,14 +211,7 @@ export function EditProductFlow({
<Input
value={newBinName}
onChange={(e) => setNewBinName(e.target.value)}
placeholder="e.g. Top Drawer"
/>
</Field>
<Field label="Location (optional)">
<Input
value={newBinLocation}
onChange={(e) => setNewBinLocation(e.target.value)}
placeholder="e.g. Bedroom"
placeholder="e.g. A1"
/>
</Field>
<Field label="Capacity">