Keep the closed box visible and tidy the order item card

- After closing, the box button stays disabled as "Box N closed" for
  5 minutes, timed from a new picked_up_at so it survives a reload
- Picked-up items keep showing their pickup box number
- Hide the steps toggle on ready and picked-up items
- Move "Buy again" and "Get help" into a dots menu in the item header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 23:11:47 +02:00
co-authored by Claude Opus 5.5
parent d68806aaf8
commit 440f9e5696
8 changed files with 1124 additions and 42 deletions
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "order_items" ADD COLUMN "picked_up_at" timestamp;
+948
View File
@@ -0,0 +1,948 @@
{
"id": "4826f90c-c779-4b2d-ab6e-491e72dbaee8",
"prevId": "42d1f3c1-18f8-42a3-ac41-9e29a4b14add",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.allergens": {
"name": "allergens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"allergens_name_idx": {
"name": "allergens_name_idx",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.brands": {
"name": "brands",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.contact_messages": {
"name": "contact_messages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.machine_tags": {
"name": "machine_tags",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_tags_name_idx": {
"name": "machine_tags_name_idx",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.machines": {
"name": "machines",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"machines_machine_tag_id_machine_tags_id_fk": {
"name": "machines_machine_tag_id_machine_tags_id_fk",
"tableFrom": "machines",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.menu_item_allergens": {
"name": "menu_item_allergens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"menu_item_id": {
"name": "menu_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"allergen_id": {
"name": "allergen_id",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"menu_item_allergens_item_allergen_idx": {
"name": "menu_item_allergens_item_allergen_idx",
"columns": [
{
"expression": "menu_item_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "allergen_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"menu_item_allergens_menu_item_id_menu_items_id_fk": {
"name": "menu_item_allergens_menu_item_id_menu_items_id_fk",
"tableFrom": "menu_item_allergens",
"tableTo": "menu_items",
"columnsFrom": [
"menu_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"menu_item_allergens_allergen_id_allergens_id_fk": {
"name": "menu_item_allergens_allergen_id_allergens_id_fk",
"tableFrom": "menu_item_allergens",
"tableTo": "allergens",
"columnsFrom": [
"allergen_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.menu_item_options": {
"name": "menu_item_options",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"menu_item_id": {
"name": "menu_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"price_delta_cents": {
"name": "price_delta_cents",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"menu_item_options_menu_item_id_menu_items_id_fk": {
"name": "menu_item_options_menu_item_id_menu_items_id_fk",
"tableFrom": "menu_item_options",
"tableTo": "menu_items",
"columnsFrom": [
"menu_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.menu_item_steps": {
"name": "menu_item_steps",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"menu_item_id": {
"name": "menu_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"estimated_duration_seconds": {
"name": "estimated_duration_seconds",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"menu_item_steps_menu_item_id_menu_items_id_fk": {
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
"tableFrom": "menu_item_steps",
"tableTo": "menu_items",
"columnsFrom": [
"menu_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
"tableFrom": "menu_item_steps",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.menu_items": {
"name": "menu_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"brand_id": {
"name": "brand_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"price_cents": {
"name": "price_cents",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"menu_items_brand_id_brands_id_fk": {
"name": "menu_items_brand_id_brands_id_fk",
"tableFrom": "menu_items",
"tableTo": "brands",
"columnsFrom": [
"brand_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.order_item_options": {
"name": "order_item_options",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_item_id": {
"name": "order_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"price_delta_cents": {
"name": "price_delta_cents",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"order_item_options_order_item_id_order_items_id_fk": {
"name": "order_item_options_order_item_id_order_items_id_fk",
"tableFrom": "order_item_options",
"tableTo": "order_items",
"columnsFrom": [
"order_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.order_item_steps": {
"name": "order_item_steps",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_item_id": {
"name": "order_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"estimated_duration_seconds": {
"name": "estimated_duration_seconds",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"order_item_steps_order_item_id_order_items_id_fk": {
"name": "order_item_steps_order_item_id_order_items_id_fk",
"tableFrom": "order_item_steps",
"tableTo": "order_items",
"columnsFrom": [
"order_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
"tableFrom": "order_item_steps",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.order_items": {
"name": "order_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_id": {
"name": "order_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"menu_item_id": {
"name": "menu_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"price_cents": {
"name": "price_cents",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'placed'"
},
"cancelled_by": {
"name": "cancelled_by",
"type": "text",
"primaryKey": false,
"notNull": false
},
"pickup_box_id": {
"name": "pickup_box_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"picked_up_at": {
"name": "picked_up_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"order_items_order_id_orders_id_fk": {
"name": "order_items_order_id_orders_id_fk",
"tableFrom": "order_items",
"tableTo": "orders",
"columnsFrom": [
"order_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"order_items_menu_item_id_menu_items_id_fk": {
"name": "order_items_menu_item_id_menu_items_id_fk",
"tableFrom": "order_items",
"tableTo": "menu_items",
"columnsFrom": [
"menu_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"order_items_pickup_box_id_pickup_boxes_id_fk": {
"name": "order_items_pickup_box_id_pickup_boxes_id_fk",
"tableFrom": "order_items",
"tableTo": "pickup_boxes",
"columnsFrom": [
"pickup_box_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.orders": {
"name": "orders",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"customer_id": {
"name": "customer_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"guest_name": {
"name": "guest_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"guest_contact": {
"name": "guest_contact",
"type": "text",
"primaryKey": false,
"notNull": false
},
"guest_access_token": {
"name": "guest_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'placed'"
},
"payment_charge_id": {
"name": "payment_charge_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.pickup_boxes": {
"name": "pickup_boxes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"number": {
"name": "number",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"is_open": {
"name": "is_open",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"pickup_boxes_number_unique": {
"name": "pickup_boxes_number_unique",
"nullsNotDistinct": false,
"columns": [
"number"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.preparation_step_instances": {
"name": "preparation_step_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_item_id": {
"name": "order_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"estimated_duration_seconds": {
"name": "estimated_duration_seconds",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"delay_seconds": {
"name": "delay_seconds",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'queued'"
},
"machine_id": {
"name": "machine_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"done_at": {
"name": "done_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"preparation_step_instances_order_item_id_order_items_id_fk": {
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
"tableFrom": "preparation_step_instances",
"tableTo": "order_items",
"columnsFrom": [
"order_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
"tableFrom": "preparation_step_instances",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"preparation_step_instances_machine_id_machines_id_fk": {
"name": "preparation_step_instances_machine_id_machines_id_fk",
"tableFrom": "preparation_step_instances",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+7
View File
@@ -64,6 +64,13 @@
"when": 1790845995013,
"tag": "0008_absurd_the_anarchist",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1790888552762,
"tag": "0009_damp_ender_wiggin",
"breakpoints": true
}
]
}
@@ -94,10 +94,81 @@
.itemHeader {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
margin-bottom: $space-2;
}
.menuButton {
display: inline-flex;
flex-shrink: 0;
margin: (-$space-2) (-$space-2) (-$space-2) $space-2;
padding: $space-2;
border: none;
border-radius: $radius;
background: none;
color: $color-text-muted;
cursor: pointer;
svg {
width: 1.25rem;
height: 1.25rem;
stroke-width: 3;
}
&:hover {
color: $color-text;
}
}
.menu {
position: fixed;
position-area: bottom span-left;
position-try-fallbacks: flip-block;
inset: auto;
margin: $space-1 0;
padding: $space-1;
min-width: 160px;
border: 1px solid $color-border;
border-radius: $radius;
background: $color-surface;
color: $color-text;
font-weight: 400;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
.menuItem {
display: flex;
align-items: center;
gap: $space-2;
width: 100%;
padding: $space-2 $space-3;
border: none;
border-radius: calc(#{$radius} - 2px);
background: none;
color: inherit;
font: inherit;
font-size: 0.9rem;
text-align: left;
text-decoration: none;
cursor: pointer;
&:hover:not(:disabled) {
background: $color-background;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
svg {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
}
.itemOptions {
margin: 0 0 $space-2;
font-size: 0.85rem;
+82 -36
View File
@@ -1,6 +1,6 @@
import { Link, useNavigate, useRouter } from '@tanstack/react-router'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { HoldButton } from '#/components/ui/HoldButton.tsx'
import type { OrderStatus } from '#/db/schema.ts'
@@ -43,6 +43,9 @@ const getItemBanner = (item: OrderItemStatusView) => {
animationKey: 'done',
variant: 'pickedUp',
label: 'Picked up',
detail: item.pickupBox
? `Pickup box ${item.pickupBox.number}`
: undefined,
} as const
}
if (item.status === 'cancelled') {
@@ -93,6 +96,7 @@ const actionIconPaths = {
'M12 17h.01',
],
cancel: ['M18 6 6 18', 'M6 6l12 12'],
more: ['M12 12h.01', 'M19 12h.01', 'M5 12h.01'],
}
const ActionIcon = ({ name }: { name: keyof typeof actionIconPaths }) => (
@@ -113,13 +117,12 @@ const ActionIcon = ({ name }: { name: keyof typeof actionIconPaths }) => (
)
const LOCK_DURATION_SECONDS = 0.25
const CLOSED_BOX_VISIBLE_MS = 5 * 60 * 1000
// Same commands in both so the shackle morphs: unlocked, the left leg
// stretches up from the body and the right leg shrinks to a stub.
const LOCKED_SHACKLE = 'M7 11V7a5 5 0 0 1 10 0v4'
const UNLOCKED_SHACKLE = 'M7 11V4a5 5 0 0 1 10 0v1'
// The shackle lifts while the box is open, and drops back first when the
// button leaves after closing, so the lock visibly snaps shut.
const LockIcon = ({ open }: { open: boolean }) => {
const reduceMotion = useReducedMotion()
const transition = reduceMotion
@@ -140,7 +143,6 @@ const LockIcon = ({ open }: { open: boolean }) => {
<motion.path
initial={false}
animate={{ d: open ? UNLOCKED_SHACKLE : LOCKED_SHACKLE }}
exit={{ d: LOCKED_SHACKLE }}
transition={transition}
/>
</svg>
@@ -192,6 +194,24 @@ export const OrderStatusView = ({
const [initialBannerKeys] = useState(
() => new Set(status.items.map(bannerKey).filter((key) => key !== null)),
)
// A closed box's button stays on screen, disabled and locked, for a while
// after pickup; re-render when the next one is due to animate out.
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
const hideAt = status.items
.flatMap((item) =>
item.pickedUpAt
? [item.pickedUpAt.getTime() + CLOSED_BOX_VISIBLE_MS]
: [],
)
.filter((time) => time > now)
if (hideAt.length === 0) return
const timeout = setTimeout(
() => setNow(Date.now()),
Math.min(...hideAt) - Date.now(),
)
return () => clearTimeout(timeout)
}, [status.items, now])
const [expandedItemIds, setExpandedItemIds] = useState<ReadonlySet<number>>(
new Set(),
)
@@ -246,11 +266,7 @@ export const OrderStatusView = ({
marginTop: 0,
marginRight: -8,
marginBottom: -8,
transition: {
delay: LOCK_DURATION_SECONDS + 0.1,
duration: 0.3,
ease: 'easeInOut' as const,
},
transition: { duration: 0.3, ease: 'easeInOut' as const },
}
const togglePickupBox = (item: OrderItemStatusView) => {
@@ -328,10 +344,58 @@ export const OrderStatusView = ({
<ul className={styles.items}>
{status.items.map((item) => {
const banner = getItemBanner(item)
const boxClosed = item.status !== 'ready'
const pickupBox =
item.status === 'ready' ||
(item.pickedUpAt !== null &&
now - item.pickedUpAt.getTime() < CLOSED_BOX_VISIBLE_MS)
? item.pickupBox
: null
return (
<li key={item.id} className={styles.item}>
<div className={styles.itemHeader}>
<span className={styles.itemName}>{item.menuItemName}</span>
<button
type="button"
className={styles.menuButton}
aria-label={`More actions for ${item.menuItemName}`}
aria-haspopup="menu"
popoverTarget={`item-menu-${item.id}`}
style={{ anchorName: `--item-menu-${item.id}` }}
>
<ActionIcon name="more" />
</button>
<div
id={`item-menu-${item.id}`}
popover="auto"
role="menu"
className={styles.menu}
style={{ positionAnchor: `--item-menu-${item.id}` }}
>
<button
type="button"
role="menuitem"
className={styles.menuItem}
disabled={pendingItemId === item.id}
onClick={() => buyAgain(item)}
>
<ActionIcon name="buyAgain" />
Buy again
</button>
<Link
role="menuitem"
className={styles.menuItem}
to="/help"
search={{
orderId: status.id,
item: item.menuItemName,
status: item.status,
}}
>
<ActionIcon name="help" />
Get help
</Link>
</div>
</div>
{item.options.length > 0 && (
<p className={styles.itemOptions}>
@@ -396,7 +460,7 @@ export const OrderStatusView = ({
)
}
/>
{isSettled(item) && (
{item.status === 'cancelled' && (
<button
type="button"
className={styles.stepsToggle}
@@ -414,43 +478,25 @@ export const OrderStatusView = ({
)}
<div className={styles.actions}>
<AnimatePresence initial={false}>
{item.pickupBox && (
{pickupBox && (
<HoldButton
key="pickupBox"
className={styles.pickupButton}
disabled={pendingItemId === item.id}
disabled={pendingItemId === item.id || boxClosed}
onHold={() =>
void runAction(item.id, () => togglePickupBox(item))
}
exit={pickupButtonExit}
>
<LockIcon open={item.pickupBox.isOpen} />
{item.pickupBox.isOpen
? `Hold to close box ${item.pickupBox.number}`
: `Hold to open box ${item.pickupBox.number}`}
<LockIcon open={pickupBox.isOpen} />
{boxClosed
? `Box ${pickupBox.number} closed`
: pickupBox.isOpen
? `Hold to close box ${pickupBox.number}`
: `Hold to open box ${pickupBox.number}`}
</HoldButton>
)}
</AnimatePresence>
<button
type="button"
disabled={pendingItemId === item.id}
onClick={() => buyAgain(item)}
>
<ActionIcon name="buyAgain" />
Buy again
</button>
<Link
to="/help"
search={{
orderId: status.id,
item: item.menuItemName,
status: item.status,
}}
>
<ActionIcon name="help" />
Get help
</Link>
{item.cancellable && (
<button
type="button"
+1
View File
@@ -162,6 +162,7 @@ export const orderItems = pgTable('order_items', {
cancelledBy: text('cancelled_by').$type<OrderItemCancelledBy>(),
// Set once the item is ready and a Pickup Box is free; kept after pickup.
pickupBoxId: integer('pickup_box_id').references(() => pickupBoxes.id),
pickedUpAt: timestamp('picked_up_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
+12 -5
View File
@@ -880,7 +880,7 @@ export const closePickupBox = async (
.where(eq(schema.pickupBoxes.id, box.id))
await tx
.update(schema.orderItems)
.set({ status: 'picked_up' })
.set({ status: 'picked_up', pickedUpAt: new Date() })
.where(eq(schema.orderItems.id, orderItemId))
await updateOrderCompleteIfEverySiblingIsDone(tx, orderItem.orderId)
@@ -908,8 +908,10 @@ export type OrderItemStatusView = {
// Menu Item recreates its Options under new ids, so the ids at order time
// can't be reused. Options since removed from the menu are left out.
buyAgainOptionIds: Array<number>
// Only set while the item is ready and waiting in a box.
// The box a ready item waits in, still reported once it's picked up (with
// isOpen false, since the box may since have gone to another item).
pickupBox: { number: number; isOpen: boolean } | null
pickedUpAt: Date | null
etaSeconds: number | null
}
@@ -950,7 +952,7 @@ export const getOrderStatus = async (
.where(inArray(schema.orderItemOptions.orderItemId, orderItemIds))
const pickupBoxIds = items
.filter((item) => item.status === 'ready')
.filter((item) => item.status === 'ready' || item.status === 'picked_up')
.map((item) => item.pickupBoxId)
.filter((id) => id !== null)
const pickupBoxesById = new Map(
@@ -1142,7 +1144,8 @@ export const getOrderStatus = async (
(option) => option.orderItemId === item.id,
)
const pickupBox =
item.status === 'ready' && item.pickupBoxId !== null
(item.status === 'ready' || item.status === 'picked_up') &&
item.pickupBoxId !== null
? pickupBoxesById.get(item.pickupBoxId)
: undefined
@@ -1170,8 +1173,12 @@ export const getOrderStatus = async (
)
.map((current) => current.id),
pickupBox: pickupBox
? { number: pickupBox.number, isOpen: pickupBox.isOpen }
? {
number: pickupBox.number,
isOpen: item.status === 'ready' && pickupBox.isOpen,
}
: null,
pickedUpAt: item.pickedUpAt,
etaSeconds,
}
})
+2 -1
View File
@@ -639,7 +639,8 @@ describe('pickup boxes', () => {
await ordering.closePickupBox(testDb.db, itemId, accessFor(order))
const status = await getStatus(order)
expect(status.items[0].status).toBe('picked_up')
expect(status.items[0].pickupBox).toBeNull()
expect(status.items[0].pickupBox).toEqual({ number: 1, isOpen: false })
expect(status.items[0].pickedUpAt).toBeInstanceOf(Date)
expect(status.status).toBe('complete')
})