Collect ready items from pickup boxes instead of "I picked this up"
A ready Order Item now gets the lowest-numbered free Pickup Box (or waits for one to free up), shown as "Pickup box N" on its status. The customer opens the box from their order and closes it again; closing marks the item picked up and hands the box to the next waiting item. On the order card the box button comes first, full width on mobile, with a padlock that lifts on open and snaps shut before the button collapses away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,5 +49,9 @@ A capability label on a Machine (e.g. "oven", "grill", "assembly") describing wh
|
|||||||
**Queue**:
|
**Queue**:
|
||||||
The set of Preparation Steps awaiting a Machine carrying a given Machine Tag, ordered by turn. When such a Machine becomes idle, it takes the next eligible Step from that Tag's Queue.
|
The set of Preparation Steps awaiting a Machine carrying a given Machine Tag, ordered by turn. When such a Machine becomes idle, it takes the next eligible Step from that Tag's Queue.
|
||||||
|
|
||||||
|
**Pickup Box**:
|
||||||
|
A numbered locker at a Kitchen that a ready Order Item is placed in for the customer to collect. Each ready Order Item gets the lowest-numbered free box; if none is free, it waits for the next one to free up. The customer opens the box from their Order and closes it again after taking the item out — closing the box is what marks the Order Item picked up and frees the box.
|
||||||
|
_Avoid_: Counter, shelf (the box is a lockable compartment the customer operates themselves)
|
||||||
|
|
||||||
**Operator**:
|
**Operator**:
|
||||||
The human role attached to a Kitchen, responsible only for physically restocking ingredients and repairing machines. Never involved in cooking or fulfilling orders.
|
The human role attached to a Kitchen, responsible only for physically restocking ingredients and repairing machines. Never involved in cooking or fulfilling orders.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE "pickup_boxes" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"number" integer NOT NULL,
|
||||||
|
"is_open" boolean DEFAULT false NOT NULL,
|
||||||
|
CONSTRAINT "pickup_boxes_number_unique" UNIQUE("number")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_items" ADD COLUMN "pickup_box_id" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "order_items" ADD CONSTRAINT "order_items_pickup_box_id_pickup_boxes_id_fk" FOREIGN KEY ("pickup_box_id") REFERENCES "public"."pickup_boxes"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1,942 @@
|
|||||||
|
{
|
||||||
|
"id": "42d1f3c1-18f8-42a3-ac41-9e29a4b14add",
|
||||||
|
"prevId": "1cec3157-9481-4f6a-83b0-a79064d2bed5",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,13 @@
|
|||||||
"when": 1790804884144,
|
"when": 1790804884144,
|
||||||
"tag": "0007_dazzling_norman_osborn",
|
"tag": "0007_dazzling_norman_osborn",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790845995013,
|
||||||
|
"tag": "0008_absurd_the_anarchist",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -299,6 +299,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pickupButton {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-color: $color-text;
|
||||||
|
background: $color-text;
|
||||||
|
color: $color-background;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: $color-text;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-tablet) {
|
||||||
|
flex-basis: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: $space-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.actionIcon {
|
.actionIcon {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { useBasket } from '#/lib/basket.tsx'
|
|||||||
import {
|
import {
|
||||||
advanceSimulatedKitchen,
|
advanceSimulatedKitchen,
|
||||||
cancelOrderItem,
|
cancelOrderItem,
|
||||||
|
closePickupBox,
|
||||||
getOrderStatus,
|
getOrderStatus,
|
||||||
markOrderItemPickedUp,
|
openPickupBox,
|
||||||
} from '#/server/orders.ts'
|
} from '#/server/orders.ts'
|
||||||
|
|
||||||
import styles from './OrderStatusView.module.scss'
|
import styles from './OrderStatusView.module.scss'
|
||||||
@@ -31,6 +32,9 @@ const getItemBanner = (item: OrderItemStatusView) => {
|
|||||||
animationKey: 'done',
|
animationKey: 'done',
|
||||||
variant: 'ready',
|
variant: 'ready',
|
||||||
label: 'Ready for pickup',
|
label: 'Ready for pickup',
|
||||||
|
detail: item.pickupBox
|
||||||
|
? `Pickup box ${item.pickupBox.number}`
|
||||||
|
: 'Waiting for a free pickup box',
|
||||||
} as const
|
} as const
|
||||||
}
|
}
|
||||||
if (item.status === 'picked_up') {
|
if (item.status === 'picked_up') {
|
||||||
@@ -82,11 +86,6 @@ const BackArrowIcon = () => (
|
|||||||
|
|
||||||
const actionIconPaths = {
|
const actionIconPaths = {
|
||||||
buyAgain: ['M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', 'M3 3v5h5'],
|
buyAgain: ['M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', 'M3 3v5h5'],
|
||||||
pickedUp: [
|
|
||||||
'M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z',
|
|
||||||
'M3 6h18',
|
|
||||||
'M16 10a4 4 0 0 1-8 0',
|
|
||||||
],
|
|
||||||
help: [
|
help: [
|
||||||
'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z',
|
'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z',
|
||||||
'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3',
|
'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3',
|
||||||
@@ -112,6 +111,38 @@ const ActionIcon = ({ name }: { name: keyof typeof actionIconPaths }) => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const LOCK_DURATION_SECONDS = 0.25
|
||||||
|
|
||||||
|
// 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
|
||||||
|
? { duration: 0 }
|
||||||
|
: { duration: LOCK_DURATION_SECONDS, ease: 'easeOut' as const }
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={styles.actionIcon}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2Z" />
|
||||||
|
<motion.path
|
||||||
|
d="M7 11V7a5 5 0 0 1 10 0v4"
|
||||||
|
initial={false}
|
||||||
|
animate={{ y: open ? -3 : 0 }}
|
||||||
|
exit={{ y: 0 }}
|
||||||
|
transition={transition}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const StepConnector = ({ filled }: { filled: boolean }) => {
|
const StepConnector = ({ filled }: { filled: boolean }) => {
|
||||||
const reduceMotion = useReducedMotion()
|
const reduceMotion = useReducedMotion()
|
||||||
return (
|
return (
|
||||||
@@ -194,6 +225,34 @@ export const OrderStatusView = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collapses to nothing on both axes, so the row closes the gap whether the
|
||||||
|
// button sits inline (desktop) or on its own full-width line (mobile); the
|
||||||
|
// negative margins cancel the flex gap it leaves behind.
|
||||||
|
const pickupButtonExit = reduceMotion
|
||||||
|
? { opacity: 0, transition: { duration: 0 } }
|
||||||
|
: {
|
||||||
|
opacity: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
marginRight: -8,
|
||||||
|
marginBottom: -8,
|
||||||
|
transition: {
|
||||||
|
delay: LOCK_DURATION_SECONDS + 0.1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeInOut' as const,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePickupBox = (item: OrderItemStatusView) => {
|
||||||
|
const toggle = item.pickupBox?.isOpen ? closePickupBox : openPickupBox
|
||||||
|
return toggle({ data: { orderItemId: item.id, guestAccessToken } })
|
||||||
|
}
|
||||||
|
|
||||||
const runKitchenSimulation = async () => {
|
const runKitchenSimulation = async () => {
|
||||||
setSimulating(true)
|
setSimulating(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -349,6 +408,25 @@ export const OrderStatusView = ({
|
|||||||
<EtaMessage etaSeconds={item.etaSeconds} />
|
<EtaMessage etaSeconds={item.etaSeconds} />
|
||||||
)}
|
)}
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{item.pickupBox && (
|
||||||
|
<motion.button
|
||||||
|
key="pickupBox"
|
||||||
|
type="button"
|
||||||
|
className={styles.pickupButton}
|
||||||
|
disabled={pendingItemId === item.id}
|
||||||
|
onClick={() =>
|
||||||
|
void runAction(item.id, () => togglePickupBox(item))
|
||||||
|
}
|
||||||
|
exit={pickupButtonExit}
|
||||||
|
>
|
||||||
|
<LockIcon open={item.pickupBox.isOpen} />
|
||||||
|
{item.pickupBox.isOpen
|
||||||
|
? `Close box ${item.pickupBox.number}`
|
||||||
|
: `Open box ${item.pickupBox.number}`}
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={pendingItemId === item.id}
|
disabled={pendingItemId === item.id}
|
||||||
@@ -358,21 +436,6 @@ export const OrderStatusView = ({
|
|||||||
Buy again
|
Buy again
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{item.status === 'ready' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={pendingItemId === item.id}
|
|
||||||
onClick={() =>
|
|
||||||
void runAction(item.id, () =>
|
|
||||||
markOrderItemPickedUp({
|
|
||||||
data: { orderItemId: item.id, guestAccessToken },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ActionIcon name="pickedUp" />I picked this up
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Link
|
<Link
|
||||||
to="/help"
|
to="/help"
|
||||||
search={{
|
search={{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
boolean,
|
||||||
integer,
|
integer,
|
||||||
pgTable,
|
pgTable,
|
||||||
serial,
|
serial,
|
||||||
@@ -108,6 +109,14 @@ export const menuItemAllergens = pgTable(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// A locker the customer collects a ready Order Item from, identified to them
|
||||||
|
// by the number on its door.
|
||||||
|
export const pickupBoxes = pgTable('pickup_boxes', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
number: integer().notNull().unique(),
|
||||||
|
isOpen: boolean('is_open').notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
export const orderStatusValues = ['placed', 'paid', 'complete'] as const
|
export const orderStatusValues = ['placed', 'paid', 'complete'] as const
|
||||||
export type OrderStatus = (typeof orderStatusValues)[number]
|
export type OrderStatus = (typeof orderStatusValues)[number]
|
||||||
|
|
||||||
@@ -151,6 +160,8 @@ export const orderItems = pgTable('order_items', {
|
|||||||
status: text().$type<OrderItemStatus>().notNull().default('placed'),
|
status: text().$type<OrderItemStatus>().notNull().default('placed'),
|
||||||
// Only set once status is 'cancelled'.
|
// Only set once status is 'cancelled'.
|
||||||
cancelledBy: text('cancelled_by').$type<OrderItemCancelledBy>(),
|
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),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export const seedGhostKitchen = async (db: NodePgDatabase<typeof schema>) => {
|
|||||||
.values({ name: 'Assembly Station 1', machineTagId: assemblyTag.id })
|
.values({ name: 'Assembly Station 1', machineTagId: assemblyTag.id })
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
|
const pickupBoxes = await db
|
||||||
|
.insert(schema.pickupBoxes)
|
||||||
|
.values([1, 2, 3, 4].map((number) => ({ number })))
|
||||||
|
.returning()
|
||||||
|
|
||||||
const [pizza] = await db
|
const [pizza] = await db
|
||||||
.insert(schema.menuItems)
|
.insert(schema.menuItems)
|
||||||
.values({
|
.values({
|
||||||
@@ -177,6 +182,7 @@ export const seedGhostKitchen = async (db: NodePgDatabase<typeof schema>) => {
|
|||||||
assembly: assemblyTag,
|
assembly: assemblyTag,
|
||||||
},
|
},
|
||||||
machines: { prep1, oven1, grill1, assembly1 },
|
machines: { prep1, oven1, grill1, assembly1 },
|
||||||
|
pickupBoxes,
|
||||||
menuItems: { pizza, burger },
|
menuItems: { pizza, burger },
|
||||||
menuItemOptions: { pizza: pizzaOptions, burger: burgerOptions },
|
menuItemOptions: { pizza: pizzaOptions, burger: burgerOptions },
|
||||||
allergens,
|
allergens,
|
||||||
|
|||||||
+146
-19
@@ -1,4 +1,4 @@
|
|||||||
import { and, asc, desc, eq, inArray } from 'drizzle-orm'
|
import { and, asc, desc, eq, inArray, isNotNull, isNull } from 'drizzle-orm'
|
||||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||||
|
|
||||||
import * as schema from '#/db/schema.ts'
|
import * as schema from '#/db/schema.ts'
|
||||||
@@ -32,6 +32,20 @@ export class OrderItemNotReadyError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NoPickupBoxYetError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('No pickup box is free for this item yet')
|
||||||
|
this.name = 'NoPickupBoxYetError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PickupBoxNotOpenError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Open the pickup box before closing it')
|
||||||
|
this.name = 'PickupBoxNotOpenError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class NoStepInProgressError extends Error {
|
export class NoStepInProgressError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('This item has no Preparation Step currently in progress')
|
super('This item has no Preparation Step currently in progress')
|
||||||
@@ -617,6 +631,7 @@ export const advanceOrderItemStep = async (db: Db, orderItemId: number) => {
|
|||||||
.update(schema.orderItems)
|
.update(schema.orderItems)
|
||||||
.set({ status: 'ready' })
|
.set({ status: 'ready' })
|
||||||
.where(eq(schema.orderItems.id, orderItemId))
|
.where(eq(schema.orderItems.id, orderItemId))
|
||||||
|
await assignFreePickupBoxes(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Freeing this machine may unblock another queued Step for the same Tag.
|
// Freeing this machine may unblock another queued Step for the same Tag.
|
||||||
@@ -750,37 +765,126 @@ export const runKitchenSimulationTick = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const markOrderItemPickedUp = async (
|
// Hands each free Pickup Box to a ready Order Item still waiting for one,
|
||||||
|
// oldest item first.
|
||||||
|
const assignFreePickupBoxes = async (db: DbOrTx) => {
|
||||||
|
const occupiedBoxIds = new Set(
|
||||||
|
(
|
||||||
|
await db
|
||||||
|
.select({ pickupBoxId: schema.orderItems.pickupBoxId })
|
||||||
|
.from(schema.orderItems)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.orderItems.status, 'ready'),
|
||||||
|
isNotNull(schema.orderItems.pickupBoxId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).map((row) => row.pickupBoxId),
|
||||||
|
)
|
||||||
|
const freeBoxes = (
|
||||||
|
await db
|
||||||
|
.select()
|
||||||
|
.from(schema.pickupBoxes)
|
||||||
|
.orderBy(asc(schema.pickupBoxes.number))
|
||||||
|
).filter((box) => !occupiedBoxIds.has(box.id))
|
||||||
|
if (freeBoxes.length === 0) return
|
||||||
|
|
||||||
|
const waitingItems = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.orderItems)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.orderItems.status, 'ready'),
|
||||||
|
isNull(schema.orderItems.pickupBoxId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(asc(schema.orderItems.id))
|
||||||
|
.limit(freeBoxes.length)
|
||||||
|
|
||||||
|
for (const [index, item] of waitingItems.entries()) {
|
||||||
|
await db
|
||||||
|
.update(schema.orderItems)
|
||||||
|
.set({ pickupBoxId: freeBoxes[index].id })
|
||||||
|
.where(eq(schema.orderItems.id, item.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPickupBoxForReadyItem = async (
|
||||||
|
tx: Tx,
|
||||||
|
orderItemId: number,
|
||||||
|
access: OrderAccess,
|
||||||
|
) => {
|
||||||
|
const orderItem = firstRow(
|
||||||
|
await tx
|
||||||
|
.select()
|
||||||
|
.from(schema.orderItems)
|
||||||
|
.where(eq(schema.orderItems.id, orderItemId)),
|
||||||
|
)
|
||||||
|
if (!orderItem) throw new Error('Order item not found')
|
||||||
|
|
||||||
|
const order = firstRow(
|
||||||
|
await tx
|
||||||
|
.select()
|
||||||
|
.from(schema.orders)
|
||||||
|
.where(eq(schema.orders.id, orderItem.orderId)),
|
||||||
|
)
|
||||||
|
if (!order) throw new Error('Order not found')
|
||||||
|
assertOrderAccess(order, access)
|
||||||
|
|
||||||
|
if (orderItem.status !== 'ready') throw new OrderItemNotReadyError()
|
||||||
|
if (orderItem.pickupBoxId === null) throw new NoPickupBoxYetError()
|
||||||
|
|
||||||
|
const box = firstRow(
|
||||||
|
await tx
|
||||||
|
.select()
|
||||||
|
.from(schema.pickupBoxes)
|
||||||
|
.where(eq(schema.pickupBoxes.id, orderItem.pickupBoxId)),
|
||||||
|
)
|
||||||
|
if (!box) throw new Error('Pickup box not found')
|
||||||
|
return { orderItem, box }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stand-in for unlocking the physical box door until hardware integration
|
||||||
|
// lands, like the kitchen simulation does for Machines.
|
||||||
|
export const openPickupBox = async (
|
||||||
db: Db,
|
db: Db,
|
||||||
orderItemId: number,
|
orderItemId: number,
|
||||||
access: OrderAccess,
|
access: OrderAccess,
|
||||||
) => {
|
) => {
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
const orderItem = firstRow(
|
const { box } = await getPickupBoxForReadyItem(tx, orderItemId, access)
|
||||||
await tx
|
await tx
|
||||||
.select()
|
.update(schema.pickupBoxes)
|
||||||
.from(schema.orderItems)
|
.set({ isOpen: true })
|
||||||
.where(eq(schema.orderItems.id, orderItemId)),
|
.where(eq(schema.pickupBoxes.id, box.id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closing the box after taking the item out is what counts as picking it up.
|
||||||
|
export const closePickupBox = async (
|
||||||
|
db: Db,
|
||||||
|
orderItemId: number,
|
||||||
|
access: OrderAccess,
|
||||||
|
) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const { orderItem, box } = await getPickupBoxForReadyItem(
|
||||||
|
tx,
|
||||||
|
orderItemId,
|
||||||
|
access,
|
||||||
)
|
)
|
||||||
if (!orderItem) throw new Error('Order item not found')
|
if (!box.isOpen) throw new PickupBoxNotOpenError()
|
||||||
|
|
||||||
const order = firstRow(
|
|
||||||
await tx
|
|
||||||
.select()
|
|
||||||
.from(schema.orders)
|
|
||||||
.where(eq(schema.orders.id, orderItem.orderId)),
|
|
||||||
)
|
|
||||||
if (!order) throw new Error('Order not found')
|
|
||||||
assertOrderAccess(order, access)
|
|
||||||
|
|
||||||
if (orderItem.status !== 'ready') throw new OrderItemNotReadyError()
|
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(schema.pickupBoxes)
|
||||||
|
.set({ isOpen: false })
|
||||||
|
.where(eq(schema.pickupBoxes.id, box.id))
|
||||||
await tx
|
await tx
|
||||||
.update(schema.orderItems)
|
.update(schema.orderItems)
|
||||||
.set({ status: 'picked_up' })
|
.set({ status: 'picked_up' })
|
||||||
.where(eq(schema.orderItems.id, orderItemId))
|
.where(eq(schema.orderItems.id, orderItemId))
|
||||||
|
|
||||||
await updateOrderCompleteIfEverySiblingIsDone(tx, orderItem.orderId)
|
await updateOrderCompleteIfEverySiblingIsDone(tx, orderItem.orderId)
|
||||||
|
await assignFreePickupBoxes(tx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,6 +908,8 @@ export type OrderItemStatusView = {
|
|||||||
// Menu Item recreates its Options under new ids, so the ids at order time
|
// 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.
|
// can't be reused. Options since removed from the menu are left out.
|
||||||
buyAgainOptionIds: Array<number>
|
buyAgainOptionIds: Array<number>
|
||||||
|
// Only set while the item is ready and waiting in a box.
|
||||||
|
pickupBox: { number: number; isOpen: boolean } | null
|
||||||
etaSeconds: number | null
|
etaSeconds: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,6 +949,20 @@ export const getOrderStatus = async (
|
|||||||
.from(schema.orderItemOptions)
|
.from(schema.orderItemOptions)
|
||||||
.where(inArray(schema.orderItemOptions.orderItemId, orderItemIds))
|
.where(inArray(schema.orderItemOptions.orderItemId, orderItemIds))
|
||||||
|
|
||||||
|
const pickupBoxIds = items
|
||||||
|
.filter((item) => item.status === 'ready')
|
||||||
|
.map((item) => item.pickupBoxId)
|
||||||
|
.filter((id) => id !== null)
|
||||||
|
const pickupBoxesById = new Map(
|
||||||
|
(pickupBoxIds.length === 0
|
||||||
|
? []
|
||||||
|
: await db
|
||||||
|
.select()
|
||||||
|
.from(schema.pickupBoxes)
|
||||||
|
.where(inArray(schema.pickupBoxes.id, pickupBoxIds))
|
||||||
|
).map((box) => [box.id, box]),
|
||||||
|
)
|
||||||
|
|
||||||
const currentOptionRows = await db
|
const currentOptionRows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.menuItemOptions)
|
.from(schema.menuItemOptions)
|
||||||
@@ -1021,6 +1141,10 @@ export const getOrderStatus = async (
|
|||||||
const options = optionRows.filter(
|
const options = optionRows.filter(
|
||||||
(option) => option.orderItemId === item.id,
|
(option) => option.orderItemId === item.id,
|
||||||
)
|
)
|
||||||
|
const pickupBox =
|
||||||
|
item.status === 'ready' && item.pickupBoxId !== null
|
||||||
|
? pickupBoxesById.get(item.pickupBoxId)
|
||||||
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -1045,6 +1169,9 @@ export const getOrderStatus = async (
|
|||||||
options.some((option) => option.name === current.name),
|
options.some((option) => option.name === current.name),
|
||||||
)
|
)
|
||||||
.map((current) => current.id),
|
.map((current) => current.id),
|
||||||
|
pickupBox: pickupBox
|
||||||
|
? { number: pickupBox.number, isOpen: pickupBox.isOpen }
|
||||||
|
: null,
|
||||||
etaSeconds,
|
etaSeconds,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -109,14 +109,16 @@ export const cancelOrderItem = createServerFn({ method: 'POST' })
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const markOrderItemPickedUp = createServerFn({ method: 'POST' })
|
export const openPickupBox = createServerFn({ method: 'POST' })
|
||||||
.validator(accessSchema.extend({ orderItemId: z.number() }))
|
.validator(accessSchema.extend({ orderItemId: z.number() }))
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
ordering.markOrderItemPickedUp(
|
ordering.openPickupBox(db, data.orderItemId, await resolveAccess(data)),
|
||||||
db,
|
)
|
||||||
data.orderItemId,
|
|
||||||
await resolveAccess(data),
|
export const closePickupBox = createServerFn({ method: 'POST' })
|
||||||
),
|
.validator(accessSchema.extend({ orderItemId: z.number() }))
|
||||||
|
.handler(async ({ data }) =>
|
||||||
|
ordering.closePickupBox(db, data.orderItemId, await resolveAccess(data)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Internal stand-in for a Machine finishing a Preparation Step, until
|
// Internal stand-in for a Machine finishing a Preparation Step, until
|
||||||
|
|||||||
+79
-12
@@ -44,6 +44,7 @@ afterEach(async () => {
|
|||||||
await testDb.db.delete(schema.orderItemOptions)
|
await testDb.db.delete(schema.orderItemOptions)
|
||||||
await testDb.db.delete(schema.orderItems)
|
await testDb.db.delete(schema.orderItems)
|
||||||
await testDb.db.delete(schema.orders)
|
await testDb.db.delete(schema.orders)
|
||||||
|
await testDb.db.update(schema.pickupBoxes).set({ isOpen: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
const placeGuestOrder = (items: ordering.PlaceOrderInput['items']) =>
|
const placeGuestOrder = (items: ordering.PlaceOrderInput['items']) =>
|
||||||
@@ -71,6 +72,23 @@ const accessFor = (order: { guestAccessToken: string | null }) => ({
|
|||||||
const getStatus = (order: { id: number; guestAccessToken: string | null }) =>
|
const getStatus = (order: { id: number; guestAccessToken: string | null }) =>
|
||||||
ordering.getOrderStatus(testDb.db, order.id, accessFor(order))
|
ordering.getOrderStatus(testDb.db, order.id, accessFor(order))
|
||||||
|
|
||||||
|
const pickUp = async (orderItemId: number, access: ordering.OrderAccess) => {
|
||||||
|
await ordering.openPickupBox(testDb.db, orderItemId, access)
|
||||||
|
await ordering.closePickupBox(testDb.db, orderItemId, access)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The burger's two Steps each start straight away while no other Order is
|
||||||
|
// cooking, so advancing twice leaves it ready.
|
||||||
|
const placeReadyBurger = async () => {
|
||||||
|
const order = await placeGuestOrder([
|
||||||
|
{ menuItemId: seed.menuItems.burger.id },
|
||||||
|
])
|
||||||
|
const itemId = (await getStatus(order)).items[0].id
|
||||||
|
await ordering.advanceOrderItemStep(testDb.db, itemId)
|
||||||
|
await ordering.advanceOrderItemStep(testDb.db, itemId)
|
||||||
|
return { order, itemId }
|
||||||
|
}
|
||||||
|
|
||||||
describe('menu', () => {
|
describe('menu', () => {
|
||||||
it('lists menu items for a brand with their recipe steps', async () => {
|
it('lists menu items for a brand with their recipe steps', async () => {
|
||||||
const menu = await ordering.getMenu(testDb.db, seed.brand.id)
|
const menu = await ordering.getMenu(testDb.db, seed.brand.id)
|
||||||
@@ -554,11 +572,7 @@ describe('advancing steps and pickup', () => {
|
|||||||
expect(status.items[0].steps[1].status).toBe('done')
|
expect(status.items[0].steps[1].status).toBe('done')
|
||||||
expect(status.items[0].status).toBe('ready')
|
expect(status.items[0].status).toBe('ready')
|
||||||
|
|
||||||
await ordering.markOrderItemPickedUp(
|
await pickUp(burgerItemId, accessFor(order))
|
||||||
testDb.db,
|
|
||||||
burgerItemId,
|
|
||||||
accessFor(order),
|
|
||||||
)
|
|
||||||
|
|
||||||
status = await getStatus(order)
|
status = await getStatus(order)
|
||||||
expect(status.items[0].status).toBe('picked_up')
|
expect(status.items[0].status).toBe('picked_up')
|
||||||
@@ -593,22 +607,75 @@ describe('advancing steps and pickup', () => {
|
|||||||
expect(secondStatus.items[0].steps[0].status).toBe('in_progress')
|
expect(secondStatus.items[0].steps[0].status).toBe('in_progress')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('refuses to mark an item picked up before it is ready', async () => {
|
it('refuses to open a pickup box before the item is ready', async () => {
|
||||||
const order = await placeGuestOrder([
|
const order = await placeGuestOrder([
|
||||||
{ menuItemId: seed.menuItems.pizza.id },
|
{ menuItemId: seed.menuItems.pizza.id },
|
||||||
])
|
])
|
||||||
const status = await getStatus(order)
|
const status = await getStatus(order)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ordering.markOrderItemPickedUp(
|
ordering.openPickupBox(testDb.db, status.items[0].id, accessFor(order)),
|
||||||
testDb.db,
|
|
||||||
status.items[0].id,
|
|
||||||
accessFor(order),
|
|
||||||
),
|
|
||||||
).rejects.toThrow(ordering.OrderItemNotReadyError)
|
).rejects.toThrow(ordering.OrderItemNotReadyError)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('pickup boxes', () => {
|
||||||
|
it('puts a ready item in the lowest-numbered free box', async () => {
|
||||||
|
const { order } = await placeReadyBurger()
|
||||||
|
|
||||||
|
const [item] = (await getStatus(order)).items
|
||||||
|
expect(item.pickupBox).toEqual({ number: 1, isOpen: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('picks the item up once the customer opens and closes its box', async () => {
|
||||||
|
const { order, itemId } = await placeReadyBurger()
|
||||||
|
|
||||||
|
await ordering.openPickupBox(testDb.db, itemId, accessFor(order))
|
||||||
|
expect((await getStatus(order)).items[0].pickupBox).toEqual({
|
||||||
|
number: 1,
|
||||||
|
isOpen: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
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.status).toBe('complete')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("refuses to close a box that hasn't been opened", async () => {
|
||||||
|
const { order, itemId } = await placeReadyBurger()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ordering.closePickupBox(testDb.db, itemId, accessFor(order)),
|
||||||
|
).rejects.toThrow(ordering.PickupBoxNotOpenError)
|
||||||
|
expect((await getStatus(order)).items[0].status).toBe('ready')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('makes a ready item wait for a box while all are taken, then gives it the first one freed', async () => {
|
||||||
|
const filling = []
|
||||||
|
for (const _ of seed.pickupBoxes) filling.push(await placeReadyBurger())
|
||||||
|
const waiting = await placeReadyBurger()
|
||||||
|
|
||||||
|
expect((await getStatus(waiting.order)).items[0].pickupBox).toBeNull()
|
||||||
|
await expect(
|
||||||
|
ordering.openPickupBox(
|
||||||
|
testDb.db,
|
||||||
|
waiting.itemId,
|
||||||
|
accessFor(waiting.order),
|
||||||
|
),
|
||||||
|
).rejects.toThrow(ordering.NoPickupBoxYetError)
|
||||||
|
|
||||||
|
const boxTwo = filling[1]
|
||||||
|
await pickUp(boxTwo.itemId, accessFor(boxTwo.order))
|
||||||
|
|
||||||
|
expect((await getStatus(waiting.order)).items[0].pickupBox).toEqual({
|
||||||
|
number: 2,
|
||||||
|
isOpen: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('kitchen simulation', () => {
|
describe('kitchen simulation', () => {
|
||||||
const policy = (
|
const policy = (
|
||||||
outcome: ReturnType<ordering.KitchenSimulationPolicy['pickOutcome']>,
|
outcome: ReturnType<ordering.KitchenSimulationPolicy['pickOutcome']>,
|
||||||
@@ -935,7 +1002,7 @@ describe('order history', () => {
|
|||||||
await ordering.advanceOrderItemStep(testDb.db, itemId)
|
await ordering.advanceOrderItemStep(testDb.db, itemId)
|
||||||
expect(await progress()).toBe('ready')
|
expect(await progress()).toBe('ready')
|
||||||
|
|
||||||
await ordering.markOrderItemPickedUp(testDb.db, itemId, access)
|
await pickUp(itemId, access)
|
||||||
expect(await progress()).toBe('picked_up')
|
expect(await progress()).toBe('picked_up')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user