Add menu item allergens, drop nav Help link, animate customizer price
- Menu items can now list allergens (shared catalog + join table), shown on the menu grid and item detail pages when applicable, and editable from the admin menu item form. - Removed the top-nav "Help" link now that the footer links to it. - The item customization page's price now uses the sliding AnimatedNumber, matching the basket line price. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "allergens" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "menu_item_allergens" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"menu_item_id" integer NOT NULL,
|
||||
"allergen_id" integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "menu_item_allergens" ADD CONSTRAINT "menu_item_allergens_menu_item_id_menu_items_id_fk" FOREIGN KEY ("menu_item_id") REFERENCES "public"."menu_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "menu_item_allergens" ADD CONSTRAINT "menu_item_allergens_allergen_id_allergens_id_fk" FOREIGN KEY ("allergen_id") REFERENCES "public"."allergens"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "allergens_name_idx" ON "allergens" USING btree ("name");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "menu_item_allergens_item_allergen_idx" ON "menu_item_allergens" USING btree ("menu_item_id","allergen_id");
|
||||
@@ -0,0 +1,870 @@
|
||||
{
|
||||
"id": "4086d6cd-5c93-4617-a15b-a90331b459a1",
|
||||
"prevId": "83dc14cf-82fb-43fc-8b7d-fd377bf48689",
|
||||
"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'"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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.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
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,13 @@
|
||||
"when": 1789636006749,
|
||||
"tag": "0005_tan_power_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1789637974330,
|
||||
"tag": "0006_worried_shooting_star",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -67,6 +67,9 @@ export const AdminMenuListView = ({
|
||||
{item.steps.length === 1 ? '' : 's'} ·{' '}
|
||||
{item.options.length} option
|
||||
{item.options.length === 1 ? '' : 's'}
|
||||
{item.allergens.length > 0
|
||||
? ` · Contains: ${item.allergens.map((allergen) => allergen.name).join(', ')}`
|
||||
: ''}
|
||||
{item.description ? ` · ${item.description}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -117,6 +117,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.allergenList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $space-3;
|
||||
}
|
||||
|
||||
.allergenOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-1;
|
||||
font-size: 0.9rem;
|
||||
color: $color-text;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Link } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
import type { AdminMenuItem, MenuItemInput } from '#/domain/menu.ts'
|
||||
import type {
|
||||
AdminMenuItem,
|
||||
AllergenOption,
|
||||
MenuItemInput,
|
||||
} from '#/domain/menu.ts'
|
||||
import type { MachineTagOption } from '#/domain/kitchen.ts'
|
||||
|
||||
import styles from './MenuItemForm.module.scss'
|
||||
@@ -25,6 +29,7 @@ const toStepRows = (item?: AdminMenuItem): Array<StepRow> =>
|
||||
|
||||
type MenuItemFormProps = {
|
||||
machineTags: Array<MachineTagOption>
|
||||
allergenOptions: Array<AllergenOption>
|
||||
initialItem?: AdminMenuItem
|
||||
submitLabel: string
|
||||
onSubmit: (input: MenuItemInput) => Promise<void>
|
||||
@@ -32,6 +37,7 @@ type MenuItemFormProps = {
|
||||
|
||||
export const MenuItemForm = ({
|
||||
machineTags,
|
||||
allergenOptions,
|
||||
initialItem,
|
||||
submitLabel,
|
||||
onSubmit,
|
||||
@@ -47,6 +53,9 @@ export const MenuItemForm = ({
|
||||
toOptionRows(initialItem),
|
||||
)
|
||||
const [steps, setSteps] = useState<Array<StepRow>>(toStepRows(initialItem))
|
||||
const [allergenIds, setAllergenIds] = useState<Array<number>>(
|
||||
initialItem?.allergens.map((allergen) => allergen.id) ?? [],
|
||||
)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
@@ -58,6 +67,12 @@ export const MenuItemForm = ({
|
||||
setSteps((rows) =>
|
||||
rows.map((row, i) => (i === index ? { ...row, ...patch } : row)),
|
||||
)
|
||||
const toggleAllergen = (allergenId: number) =>
|
||||
setAllergenIds((ids) =>
|
||||
ids.includes(allergenId)
|
||||
? ids.filter((id) => id !== allergenId)
|
||||
: [...ids, allergenId],
|
||||
)
|
||||
|
||||
const submit = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
@@ -83,6 +98,7 @@ export const MenuItemForm = ({
|
||||
machineTagId: Number(step.machineTagId),
|
||||
estimatedDurationSeconds: Math.round(Number(step.durationSeconds)),
|
||||
})),
|
||||
allergenIds,
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
@@ -249,6 +265,24 @@ export const MenuItemForm = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<p className={styles.sectionTitle}>Allergens</p>
|
||||
</div>
|
||||
<div className={styles.allergenList}>
|
||||
{allergenOptions.map((allergen) => (
|
||||
<label key={allergen.id} className={styles.allergenOption}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allergenIds.includes(allergen.id)}
|
||||
onChange={() => toggleAllergen(allergen.id)}
|
||||
/>
|
||||
{allergen.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
|
||||
<div className={styles.actions}>
|
||||
|
||||
@@ -71,7 +71,11 @@ export const MenuItemCustomizer = ({
|
||||
</div>
|
||||
|
||||
<div className={styles.stepperRow}>
|
||||
<span className={styles.price}>{formatPrice(unitPriceCents)}</span>
|
||||
<AnimatedNumber
|
||||
value={unitPriceCents}
|
||||
format={formatPrice}
|
||||
className={styles.price}
|
||||
/>
|
||||
<div className={styles.stepper}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -25,3 +25,9 @@
|
||||
color: $color-text-muted;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.allergens {
|
||||
margin: $space-1 0 0;
|
||||
color: $color-text-muted;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ export const MenuItemDetail = ({
|
||||
<p className={styles.meta}>
|
||||
{formatPrice(item.priceCents)} · {formatDuration(totalSeconds)}
|
||||
</p>
|
||||
{item.allergens.length > 0 && (
|
||||
<p className={styles.allergens}>
|
||||
Contains:{' '}
|
||||
{item.allergens.map((allergen) => allergen.name).join(', ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MenuItemCustomizer
|
||||
|
||||
@@ -68,6 +68,12 @@
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.allergens {
|
||||
margin: $space-1 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -46,6 +46,12 @@ export const MenuList = ({ menu, lines, onAdd, onRemove }: MenuListProps) => (
|
||||
<p className={styles.meta}>
|
||||
{formatPrice(item.priceCents)} · {formatDuration(totalSeconds)}
|
||||
</p>
|
||||
{item.allergens.length > 0 && (
|
||||
<p className={styles.allergens}>
|
||||
Contains:{' '}
|
||||
{item.allergens.map((allergen) => allergen.name).join(', ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -76,6 +76,38 @@ export const menuItemOptions = pgTable('menu_item_options', {
|
||||
priceDeltaCents: integer('price_delta_cents').notNull().default(0),
|
||||
})
|
||||
|
||||
// A shared catalog of allergens (e.g. "Gluten", "Dairy") that Menu Items
|
||||
// draw from — unlike menuItemOptions, this list is reused across items
|
||||
// rather than owned by any one of them.
|
||||
export const allergens = pgTable(
|
||||
'allergens',
|
||||
{
|
||||
id: serial().primaryKey(),
|
||||
name: text().notNull(),
|
||||
},
|
||||
(table) => [uniqueIndex('allergens_name_idx').on(table.name)],
|
||||
)
|
||||
|
||||
// Which Allergens a Menu Item's recipe contains, as ordered by an operator.
|
||||
export const menuItemAllergens = pgTable(
|
||||
'menu_item_allergens',
|
||||
{
|
||||
id: serial().primaryKey(),
|
||||
menuItemId: integer('menu_item_id')
|
||||
.notNull()
|
||||
.references(() => menuItems.id),
|
||||
allergenId: integer('allergen_id')
|
||||
.notNull()
|
||||
.references(() => allergens.id),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('menu_item_allergens_item_allergen_idx').on(
|
||||
table.menuItemId,
|
||||
table.allergenId,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
export const orderStatusValues = ['placed', 'paid', 'complete'] as const
|
||||
export type OrderStatus = (typeof orderStatusValues)[number]
|
||||
|
||||
|
||||
@@ -145,6 +145,29 @@ export const seedGhostKitchen = async (db: NodePgDatabase<typeof schema>) => {
|
||||
])
|
||||
.returning()
|
||||
|
||||
const allergens = await db
|
||||
.insert(schema.allergens)
|
||||
.values([
|
||||
{ name: 'Gluten' },
|
||||
{ name: 'Dairy' },
|
||||
{ name: 'Eggs' },
|
||||
{ name: 'Soy' },
|
||||
{ name: 'Peanuts' },
|
||||
{ name: 'Tree nuts' },
|
||||
{ name: 'Sesame' },
|
||||
])
|
||||
.returning()
|
||||
const allergenByName = Object.fromEntries(
|
||||
allergens.map((allergen) => [allergen.name, allergen]),
|
||||
)
|
||||
|
||||
await db.insert(schema.menuItemAllergens).values([
|
||||
{ menuItemId: pizza.id, allergenId: allergenByName['Gluten'].id },
|
||||
{ menuItemId: pizza.id, allergenId: allergenByName['Dairy'].id },
|
||||
{ menuItemId: burger.id, allergenId: allergenByName['Gluten'].id },
|
||||
{ menuItemId: burger.id, allergenId: allergenByName['Sesame'].id },
|
||||
])
|
||||
|
||||
return {
|
||||
brand,
|
||||
machineTags: {
|
||||
@@ -156,6 +179,7 @@ export const seedGhostKitchen = async (db: NodePgDatabase<typeof schema>) => {
|
||||
machines: { prep1, oven1, grill1, assembly1 },
|
||||
menuItems: { pizza, burger },
|
||||
menuItemOptions: { pizza: pizzaOptions, burger: burgerOptions },
|
||||
allergens,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
-4
@@ -26,6 +26,7 @@ export type AdminMenuItem = {
|
||||
machineTagName: string
|
||||
estimatedDurationSeconds: number
|
||||
}>
|
||||
allergens: Array<{ id: number; name: string }>
|
||||
}
|
||||
|
||||
export type MenuItemInput = {
|
||||
@@ -38,8 +39,19 @@ export type MenuItemInput = {
|
||||
machineTagId: number
|
||||
estimatedDurationSeconds: number
|
||||
}>
|
||||
allergenIds: Array<number>
|
||||
}
|
||||
|
||||
export type AllergenOption = { id: number; name: string }
|
||||
|
||||
export const getAllergens = async (
|
||||
db: Db,
|
||||
): Promise<Array<AllergenOption>> =>
|
||||
db
|
||||
.select({ id: schema.allergens.id, name: schema.allergens.name })
|
||||
.from(schema.allergens)
|
||||
.orderBy(schema.allergens.name)
|
||||
|
||||
const assertHasSteps = (input: MenuItemInput) => {
|
||||
if (input.steps.length === 0) {
|
||||
throw new Error('A menu item needs at least one Preparation Step')
|
||||
@@ -74,6 +86,18 @@ export const getMenuItemsForAdmin = async (
|
||||
)
|
||||
.orderBy(schema.menuItemSteps.sequence)
|
||||
|
||||
const itemAllergens = await db
|
||||
.select({
|
||||
menuItemId: schema.menuItemAllergens.menuItemId,
|
||||
id: schema.allergens.id,
|
||||
name: schema.allergens.name,
|
||||
})
|
||||
.from(schema.menuItemAllergens)
|
||||
.innerJoin(
|
||||
schema.allergens,
|
||||
eq(schema.menuItemAllergens.allergenId, schema.allergens.id),
|
||||
)
|
||||
|
||||
return items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
@@ -89,6 +113,9 @@ export const getMenuItemsForAdmin = async (
|
||||
steps: steps
|
||||
.filter((step) => step.menuItemId === item.id)
|
||||
.map(({ menuItemId: _menuItemId, ...step }) => step),
|
||||
allergens: itemAllergens
|
||||
.filter((allergen) => allergen.menuItemId === item.id)
|
||||
.map(({ id, name }) => ({ id, name })),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -130,14 +157,24 @@ export const createMenuItem = async (
|
||||
})),
|
||||
)
|
||||
|
||||
if (input.allergenIds.length > 0) {
|
||||
await tx.insert(schema.menuItemAllergens).values(
|
||||
input.allergenIds.map((allergenId) => ({
|
||||
menuItemId: item.id,
|
||||
allergenId,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
return item.id
|
||||
})
|
||||
}
|
||||
|
||||
// Options and Steps are replaced wholesale on every edit rather than diffed
|
||||
// — simplest correct behavior for a form that resubmits its full list each
|
||||
// time, and safe because placed Order Items snapshot their own copy (see
|
||||
// orderItemSteps/orderItemOptions) so this never rewrites order history.
|
||||
// Options, Steps, and Allergens are replaced wholesale on every edit rather
|
||||
// than diffed — simplest correct behavior for a form that resubmits its
|
||||
// full list each time, and safe because placed Order Items snapshot their
|
||||
// own copy (see orderItemSteps/orderItemOptions) so this never rewrites
|
||||
// order history.
|
||||
export const updateMenuItem = async (
|
||||
db: Db,
|
||||
menuItemId: number,
|
||||
@@ -161,6 +198,9 @@ export const updateMenuItem = async (
|
||||
await tx
|
||||
.delete(schema.menuItemSteps)
|
||||
.where(eq(schema.menuItemSteps.menuItemId, menuItemId))
|
||||
await tx
|
||||
.delete(schema.menuItemAllergens)
|
||||
.where(eq(schema.menuItemAllergens.menuItemId, menuItemId))
|
||||
|
||||
if (input.options.length > 0) {
|
||||
await tx.insert(schema.menuItemOptions).values(
|
||||
@@ -181,6 +221,12 @@ export const updateMenuItem = async (
|
||||
estimatedDurationSeconds: step.estimatedDurationSeconds,
|
||||
})),
|
||||
)
|
||||
|
||||
if (input.allergenIds.length > 0) {
|
||||
await tx.insert(schema.menuItemAllergens).values(
|
||||
input.allergenIds.map((allergenId) => ({ menuItemId, allergenId })),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,6 +251,9 @@ export const deleteMenuItem = async (
|
||||
await tx
|
||||
.delete(schema.menuItemSteps)
|
||||
.where(eq(schema.menuItemSteps.menuItemId, menuItemId))
|
||||
await tx
|
||||
.delete(schema.menuItemAllergens)
|
||||
.where(eq(schema.menuItemAllergens.menuItemId, menuItemId))
|
||||
// No ON DELETE CASCADE from orderItems.menuItemId — this throws (and
|
||||
// rolls back the deletes above) if any Order ever referenced it.
|
||||
await tx.delete(schema.menuItems).where(eq(schema.menuItems.id, menuItemId))
|
||||
|
||||
@@ -62,6 +62,10 @@ export type MenuItemWithRecipe = {
|
||||
name: string
|
||||
priceDeltaCents: number
|
||||
}>
|
||||
allergens: Array<{
|
||||
id: number
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
|
||||
export const getMenu = async (
|
||||
@@ -99,6 +103,22 @@ export const getMenu = async (
|
||||
.from(schema.menuItemOptions)
|
||||
.where(inArray(schema.menuItemOptions.menuItemId, menuItemIds))
|
||||
|
||||
const itemAllergens =
|
||||
menuItemIds.length === 0
|
||||
? []
|
||||
: await db
|
||||
.select({
|
||||
menuItemId: schema.menuItemAllergens.menuItemId,
|
||||
id: schema.allergens.id,
|
||||
name: schema.allergens.name,
|
||||
})
|
||||
.from(schema.menuItemAllergens)
|
||||
.innerJoin(
|
||||
schema.allergens,
|
||||
eq(schema.menuItemAllergens.allergenId, schema.allergens.id),
|
||||
)
|
||||
.where(inArray(schema.menuItemAllergens.menuItemId, menuItemIds))
|
||||
|
||||
return items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
@@ -115,6 +135,9 @@ export const getMenu = async (
|
||||
options: options
|
||||
.filter((option) => option.menuItemId === item.id)
|
||||
.map(({ id, name, priceDeltaCents }) => ({ id, name, priceDeltaCents })),
|
||||
allergens: itemAllergens
|
||||
.filter((allergen) => allergen.menuItemId === item.id)
|
||||
.map(({ id, name }) => ({ id, name })),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,3 @@
|
||||
align-items: center;
|
||||
gap: $space-2;
|
||||
}
|
||||
|
||||
.helpLink {
|
||||
color: $color-text-muted;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:hover {
|
||||
color: $color-text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,6 @@ const RootDocument = ({ children }: { children: React.ReactNode }) => {
|
||||
<Logo name={brand.name} />
|
||||
</Link>
|
||||
<div className={styles.actions}>
|
||||
<Link to="/help" className={styles.helpLink}>
|
||||
Help
|
||||
</Link>
|
||||
<BasketLink />
|
||||
<BetterAuthHeader />
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,15 @@ import { createFileRoute, notFound, useNavigate } from '@tanstack/react-router'
|
||||
|
||||
import { MenuItemForm } from '#/components/admin/MenuItemForm.tsx'
|
||||
import { withAdminGuard } from '#/lib/admin-guard.ts'
|
||||
import { getAdminMenu, getMachineTagOptions, updateMenuItem } from '#/server/menu-admin.ts'
|
||||
import {
|
||||
getAdminMenu,
|
||||
getAllergenOptions,
|
||||
getMachineTagOptions,
|
||||
updateMenuItem,
|
||||
} from '#/server/menu-admin.ts'
|
||||
|
||||
const EditMenuItemRoute = () => {
|
||||
const { item, machineTags } = Route.useLoaderData()
|
||||
const { item, machineTags, allergenOptions } = Route.useLoaderData()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
@@ -13,6 +18,7 @@ const EditMenuItemRoute = () => {
|
||||
<h1>Edit {item.name}</h1>
|
||||
<MenuItemForm
|
||||
machineTags={machineTags}
|
||||
allergenOptions={allergenOptions}
|
||||
initialItem={item}
|
||||
submitLabel="Save changes"
|
||||
onSubmit={async (input) => {
|
||||
@@ -26,12 +32,16 @@ const EditMenuItemRoute = () => {
|
||||
|
||||
export const Route = createFileRoute('/admin/menu/$menuItemId')({
|
||||
loader: async ({ params }) => {
|
||||
const [items, machineTags] = await withAdminGuard(() =>
|
||||
Promise.all([getAdminMenu(), getMachineTagOptions()]),
|
||||
const [items, machineTags, allergenOptions] = await withAdminGuard(() =>
|
||||
Promise.all([
|
||||
getAdminMenu(),
|
||||
getMachineTagOptions(),
|
||||
getAllergenOptions(),
|
||||
]),
|
||||
)
|
||||
const item = items.find((i) => i.id === Number(params.menuItemId))
|
||||
if (!item) throw notFound()
|
||||
return { item, machineTags }
|
||||
return { item, machineTags, allergenOptions }
|
||||
},
|
||||
component: EditMenuItemRoute,
|
||||
})
|
||||
|
||||
@@ -2,10 +2,14 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
|
||||
import { MenuItemForm } from '#/components/admin/MenuItemForm.tsx'
|
||||
import { withAdminGuard } from '#/lib/admin-guard.ts'
|
||||
import { createMenuItem, getMachineTagOptions } from '#/server/menu-admin.ts'
|
||||
import {
|
||||
createMenuItem,
|
||||
getAllergenOptions,
|
||||
getMachineTagOptions,
|
||||
} from '#/server/menu-admin.ts'
|
||||
|
||||
const NewMenuItemRoute = () => {
|
||||
const machineTags = Route.useLoaderData()
|
||||
const { machineTags, allergenOptions } = Route.useLoaderData()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
@@ -13,6 +17,7 @@ const NewMenuItemRoute = () => {
|
||||
<h1>Add menu item</h1>
|
||||
<MenuItemForm
|
||||
machineTags={machineTags}
|
||||
allergenOptions={allergenOptions}
|
||||
submitLabel="Create item"
|
||||
onSubmit={async (input) => {
|
||||
await createMenuItem({ data: input })
|
||||
@@ -24,6 +29,10 @@ const NewMenuItemRoute = () => {
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/admin/menu/new')({
|
||||
loader: () => withAdminGuard(() => getMachineTagOptions()),
|
||||
loader: () =>
|
||||
withAdminGuard(async () => ({
|
||||
machineTags: await getMachineTagOptions(),
|
||||
allergenOptions: await getAllergenOptions(),
|
||||
})),
|
||||
component: NewMenuItemRoute,
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ const menuItemInputSchema = z.object({
|
||||
estimatedDurationSeconds: z.number().int().positive(),
|
||||
}),
|
||||
),
|
||||
allergenIds: z.array(z.number()),
|
||||
})
|
||||
|
||||
export const getAdminMenu = createServerFn({ method: 'GET' }).handler(
|
||||
@@ -38,6 +39,13 @@ export const getMachineTagOptions = createServerFn({ method: 'GET' }).handler(
|
||||
},
|
||||
)
|
||||
|
||||
export const getAllergenOptions = createServerFn({ method: 'GET' }).handler(
|
||||
async () => {
|
||||
await requireAdmin()
|
||||
return menu.getAllergens(db)
|
||||
},
|
||||
)
|
||||
|
||||
export const createMenuItem = createServerFn({ method: 'POST' })
|
||||
.validator(menuItemInputSchema)
|
||||
.handler(async ({ data }) => {
|
||||
|
||||
Reference in New Issue
Block a user