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:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "order_items" ADD COLUMN "picked_up_at" timestamp;
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user