@zorgo/next 1.1.7 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/cli.js +344 -1
  2. package/dist/cli.js.map +1 -0
  3. package/dist/cli.mjs +344 -1
  4. package/dist/cli.mjs.map +1 -0
  5. package/dist/components.d.mts +315 -87
  6. package/dist/components.d.ts +315 -87
  7. package/dist/components.js +1216 -820
  8. package/dist/components.js.map +1 -1
  9. package/dist/components.mjs +1195 -832
  10. package/dist/components.mjs.map +1 -1
  11. package/dist/images.d.mts +36 -0
  12. package/dist/images.d.ts +36 -0
  13. package/dist/images.js +62 -0
  14. package/dist/images.js.map +1 -0
  15. package/dist/images.mjs +32 -0
  16. package/dist/images.mjs.map +1 -0
  17. package/dist/index.d.mts +175 -6
  18. package/dist/index.d.ts +175 -6
  19. package/dist/index.js +162 -1
  20. package/dist/index.js.map +1 -0
  21. package/dist/index.mjs +162 -1
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/item-page.d.mts +151 -0
  24. package/dist/item-page.d.ts +151 -0
  25. package/dist/item-page.js +166 -0
  26. package/dist/item-page.js.map +1 -0
  27. package/dist/item-page.mjs +129 -0
  28. package/dist/item-page.mjs.map +1 -0
  29. package/dist/seo.d.mts +31 -1
  30. package/dist/seo.d.ts +31 -1
  31. package/dist/server.js +541 -1
  32. package/dist/server.js.map +1 -0
  33. package/dist/server.mjs +504 -1
  34. package/dist/server.mjs.map +1 -0
  35. package/dist/slugs.js.map +1 -1
  36. package/dist/slugs.mjs.map +1 -1
  37. package/dist/tree/app/components/ClientItemForm.tsx +7 -0
  38. package/dist/tree/app/components/Header.tsx +3 -0
  39. package/dist/tree/app/components/ModalRegistry.tsx +3 -2
  40. package/dist/tree/app/components/index.ts +2 -1
  41. package/dist/tree/app/globals.css.gen.ts +1 -0
  42. package/dist/tree/app/menu/[slug]/page.tsx +16 -48
  43. package/dist/tree/gitignore +1 -0
  44. package/dist/tree/image-loader.ts +4 -0
  45. package/dist/tree/next.config.ts +12 -0
  46. package/package.json +11 -1
  47. package/dist/cli.d.mts +0 -9
  48. package/dist/cli.d.ts +0 -9
  49. package/dist/tree/app/menu/[slug]/ItemCustomizationClient.tsx +0 -18
@@ -0,0 +1,36 @@
1
+ interface BaseItemImage {
2
+ image_id: number;
3
+ base_item_id: number;
4
+ alias: string;
5
+ raw_path?: string | null;
6
+ processed_path?: string | null;
7
+ sort_index?: number;
8
+ local_key?: string | null;
9
+ }
10
+
11
+ declare function menuImageUrl(image?: BaseItemImage | null, absolute?: boolean): string | null;
12
+
13
+ /**
14
+ * The variant widths prebuild emits. The loader below and the encoder in
15
+ * internal/itemImages.ts read the same list, so a requested width can never
16
+ * snap to a rung that was never written.
17
+ */
18
+ declare const ITEM_IMAGE_WIDTHS: readonly [384, 640, 1080];
19
+ /** Public prefix menuImageUrl returns for a localized image. */
20
+ declare const ITEM_IMAGE_PREFIX = "/images/items/";
21
+ /**
22
+ * next/image custom loader (`images.loaderFile` in a scaffolded site's
23
+ * next.config.ts). Prebuilt images get the nearest rung that covers the
24
+ * requested width; everything else — a bucket URL for an image prebuild could
25
+ * not localize, the logo, any site-authored asset — passes through untouched
26
+ * and is simply served unoptimized.
27
+ *
28
+ * Sync and pure by contract: next/image calls it during render for every
29
+ * srcset candidate.
30
+ */
31
+ declare function itemImageLoader({ src, width, }: {
32
+ src: string;
33
+ width: number;
34
+ }): string;
35
+
36
+ export { type BaseItemImage, ITEM_IMAGE_PREFIX, ITEM_IMAGE_WIDTHS, itemImageLoader as default, itemImageLoader, menuImageUrl };
@@ -0,0 +1,36 @@
1
+ interface BaseItemImage {
2
+ image_id: number;
3
+ base_item_id: number;
4
+ alias: string;
5
+ raw_path?: string | null;
6
+ processed_path?: string | null;
7
+ sort_index?: number;
8
+ local_key?: string | null;
9
+ }
10
+
11
+ declare function menuImageUrl(image?: BaseItemImage | null, absolute?: boolean): string | null;
12
+
13
+ /**
14
+ * The variant widths prebuild emits. The loader below and the encoder in
15
+ * internal/itemImages.ts read the same list, so a requested width can never
16
+ * snap to a rung that was never written.
17
+ */
18
+ declare const ITEM_IMAGE_WIDTHS: readonly [384, 640, 1080];
19
+ /** Public prefix menuImageUrl returns for a localized image. */
20
+ declare const ITEM_IMAGE_PREFIX = "/images/items/";
21
+ /**
22
+ * next/image custom loader (`images.loaderFile` in a scaffolded site's
23
+ * next.config.ts). Prebuilt images get the nearest rung that covers the
24
+ * requested width; everything else — a bucket URL for an image prebuild could
25
+ * not localize, the logo, any site-authored asset — passes through untouched
26
+ * and is simply served unoptimized.
27
+ *
28
+ * Sync and pure by contract: next/image calls it during render for every
29
+ * srcset candidate.
30
+ */
31
+ declare function itemImageLoader({ src, width, }: {
32
+ src: string;
33
+ width: number;
34
+ }): string;
35
+
36
+ export { type BaseItemImage, ITEM_IMAGE_PREFIX, ITEM_IMAGE_WIDTHS, itemImageLoader as default, itemImageLoader, menuImageUrl };
package/dist/images.js ADDED
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/images.ts
21
+ var images_exports = {};
22
+ __export(images_exports, {
23
+ ITEM_IMAGE_PREFIX: () => ITEM_IMAGE_PREFIX,
24
+ ITEM_IMAGE_WIDTHS: () => ITEM_IMAGE_WIDTHS,
25
+ default: () => itemImageLoader,
26
+ itemImageLoader: () => itemImageLoader,
27
+ menuImageUrl: () => menuImageUrl
28
+ });
29
+ module.exports = __toCommonJS(images_exports);
30
+
31
+ // ../zorgo/universal/constants.ts
32
+ var SUPABASE_URL = "https://dhnpoxorllfdobwftkyb.supabase.co";
33
+ var API_BASE_URL = process.env.NODE_ENV === "production" ? "https://api.zorgotech.com" : "http://localhost:3000";
34
+
35
+ // ../zorgo/universal/utils/menuUtils.ts
36
+ function menuImageUrl(image, absolute = false) {
37
+ if (!image) return null;
38
+ if (image.local_key && !absolute) return `/images/items/${image.local_key}`;
39
+ const path = image.processed_path ?? image.raw_path;
40
+ if (!path) return null;
41
+ return `${SUPABASE_URL}/storage/v1/object/public/product-images/${path}`;
42
+ }
43
+
44
+ // src/images.ts
45
+ var ITEM_IMAGE_WIDTHS = [384, 640, 1080];
46
+ var ITEM_IMAGE_PREFIX = "/images/items/";
47
+ function itemImageLoader({
48
+ src,
49
+ width
50
+ }) {
51
+ if (!src.startsWith(ITEM_IMAGE_PREFIX)) return src;
52
+ const rung = ITEM_IMAGE_WIDTHS.find((candidate) => candidate >= width) ?? ITEM_IMAGE_WIDTHS[ITEM_IMAGE_WIDTHS.length - 1];
53
+ return `${src}-${rung}.avif`;
54
+ }
55
+ // Annotate the CommonJS export names for ESM import in node:
56
+ 0 && (module.exports = {
57
+ ITEM_IMAGE_PREFIX,
58
+ ITEM_IMAGE_WIDTHS,
59
+ itemImageLoader,
60
+ menuImageUrl
61
+ });
62
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/images.ts","../../zorgo/universal/constants.ts","../../zorgo/universal/utils/menuUtils.ts"],"sourcesContent":["export { menuImageUrl } from \"@zorgo/universal/utils/menuUtils\";\nexport type { BaseItemImage } from \"@zorgo/universal/interfaces\";\n\n/**\n * The variant widths prebuild emits. The loader below and the encoder in\n * internal/itemImages.ts read the same list, so a requested width can never\n * snap to a rung that was never written.\n */\nexport const ITEM_IMAGE_WIDTHS = [384, 640, 1080] as const;\n\n/** Public prefix menuImageUrl returns for a localized image. */\nexport const ITEM_IMAGE_PREFIX = \"/images/items/\";\n\n/**\n * next/image custom loader (`images.loaderFile` in a scaffolded site's\n * next.config.ts). Prebuilt images get the nearest rung that covers the\n * requested width; everything else — a bucket URL for an image prebuild could\n * not localize, the logo, any site-authored asset — passes through untouched\n * and is simply served unoptimized.\n *\n * Sync and pure by contract: next/image calls it during render for every\n * srcset candidate.\n */\nexport default function itemImageLoader({\n\tsrc,\n\twidth,\n}: {\n\tsrc: string;\n\twidth: number;\n}): string {\n\tif (!src.startsWith(ITEM_IMAGE_PREFIX)) return src;\n\tconst rung =\n\t\tITEM_IMAGE_WIDTHS.find((candidate) => candidate >= width) ??\n\t\tITEM_IMAGE_WIDTHS[ITEM_IMAGE_WIDTHS.length - 1];\n\treturn `${src}-${rung}.avif`;\n}\n\nexport { itemImageLoader };\n","export const SUPABASE_URL = \"https://dhnpoxorllfdobwftkyb.supabase.co\";\n\nexport const SUPABASE_ANON_KEY =\n\t\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRobnBveG9ybGxmZG9id2Z0a3liIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwNTQxMDgsImV4cCI6MjA1NDYzMDEwOH0.zI8PwbgKwijHZ1QmxJqmaPrFvg02w1vOY2Qcs7SddzI\";\nexport const ZORGO_PURPLE = \"rgb(153,67,233)\";\n\nexport const expoGoGoogleClientId =\n\t\"959654249401-dm6nbi27rj605ul1nkt8o2imb8f7daff.apps.googleusercontent.com\";\n\nexport const API_BASE_URL =\n\tprocess.env.NODE_ENV === \"production\"\n\t\t? \"https://api.zorgotech.com\"\n\t\t: \"http://localhost:3000\";\n\n\n// replace instances of API_BASE_URL with env enum\nexport enum Environments {\n\tProduction = \"https://api.zorgotech.com\",\n\tStaging = \"http://api.zorgo.ai\",\n\tDevelopment = \"http://localhost:3000\",\n}\n/** Emails allowed to access developer-only metrics (e.g. /performance/developer). */\nexport const DEVELOPER_EMAILS = [\"clawsorgo@gmail.com\", \"gmandwee@gmail.com\"] as const;\n\nexport function isDeveloperEmail(email?: string | null): boolean {\n\treturn !!email && (DEVELOPER_EMAILS as readonly string[]).includes(email);\n}\n\nexport const ORDER_STORE_CONSTANTS = {\n\tDEFAULTS: {\n\t\tPREP_MINUTES: 20, // default amount of time for prep\n\t\tPAST_HOURS: 24, // hours into the past orders are fetched for\n\t\tFUTURE_HOURS: 72 // hours into the future orders are fetched for \n\t},\n\tORDER_STATUS: {\n\t\tSTAGING: 10,\n\t\tPENDING: 1,\n\t\tCOMPLETED: 2,\n\t\tCANCELLED: 4,\n\t\tPAID: 6,\n\t\tPARTIALLY_PAID: 7,\n\t\tKITCHEN_DONE: 9,\n\t\tCONFIRMED: 11,\n\t\tPREPARING: 12,\n\t},\n\tPOS_METHOD_ID: 3,\n\tORDER_TYPE: {\n\t\tDINE_IN: 1,\n\t\tTAKEOUT: 2,\n\t\tDELIVERY: 3\n\t},\n\t/** KDS edit highlight duration after a POS modification. */\n\tMODIFICATION_HIGHLIGHT_MS: 8_000,\n\t/** POS order realtime: refetch + recycle postgres_changes channel on this cadence. */\n\tLISTENER_SYNC_INTERVAL_MS: 60_000,\n\tORDER_SELECT: `\n\t*,\n\n\tcharges (\n\t\t*\n\t),\n\n\tlocations (name),\n\n\tcustomers\n\t (customer_id, first_name, last_name, phone_number, email),\n\n\torder_items\n\t (\n\t\torder_item_id, item_id, quantity, price_at_order, order_id, comments, status, applied_promo_id, discount_amount,\n\t\titems (\n\t\t\titem_id, name, price,\n\t\t\tportion_schemes (id, franchise_id, geometry, name, min_divisions, max_divisions),\n\t\t\tbase_items (\n\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t)\n\t\t),\n\t\torder_item_selected_items (\n\t\t\torder_item_id, rule_id, item_id, quantity, upcharge_price, portion,\n\t\t\titems (\n\t\t\t\titem_id, name, price,\n\t\t\t\tbase_items (\n\t\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t\t)\n\t\t\t),\n\t\t\titem_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_selected_components (\n\t\t\torder_item_id, rule_id, component_id, quantity, upcharge_price, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit), item_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_components (\n\t\t\torder_item_id, component_id, quantity_servings, is_base, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit)\n\t\t)\n\t)\n`,\n};\n\nexport const CHARGE_STATUSES = {\n\tSUCCESS: 1,\n\tPENDING: 2,\n\tREFUNDED: 3,\n\tVOIDED: 4,\n\tPAYMENT_QUEUED: 5,\n\tFAILED: 6\n}\n","import { SupabaseClient } from \"@supabase/supabase-js\";\nimport { getDay, isAfter, isBefore, parse, startOfDay } from \"date-fns\";\nimport { SUPABASE_URL } from \"../constants\";\nimport {\n\tBaseItem,\n\tBaseItemImage,\n\tCategory,\n\tComponent,\n\tItemVariant,\n\tMenuItem,\n\tPlatforms,\n\tPromotion,\n\tRule,\n\tVisibilityConditionGroupAssignment,\n\tVisibilityEffect,\n} from \"../interfaces\";\nimport { csvFormat } from \"./misc\";\nimport {\n\tbuildVisibilityEvaluationContext,\n\tevaluateVisibilityCondition,\n} from \"./visibilityConditions\";\n\nexport interface Menu {\n\titems: MenuItem[];\n\tcategories: Category[];\n\tcomponents: Component[];\n\trules: Rule[];\n}\n\nexport interface MenuIds {\n\tbaseItemIds: number[];\n\tcategoryIds: number[];\n\titemIds: number[];\n}\n\ntype MenuIdRows = {\n\tcategories: Pick<Category, \"category_id\">[];\n\tbaseItems: Pick<BaseItem, \"base_item_id\" | \"category_id\">[];\n\titems: Pick<ItemVariant, \"item_id\" | \"base_item_id\">[];\n};\n\nexport async function getMenu(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texclusions: MenuIds = emptyMenuExclusions(),\n\tuseBehaviors = false,\n): Promise<Menu> {\n\tconst categories = await getCategories(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.categoryIds,\n\t);\n\n\tif (categories.length === 0) {\n\t\treturn buildMenu([], [], [], [], []);\n\t}\n\n\tconst baseItems = await getBaseItems(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.baseItemIds,\n\t\tcategories,\n\t);\n\n\tif (baseItems.length === 0) {\n\t\treturn buildMenu([], [], categories, [], []);\n\t}\n\n\tconst items = await getItems(supabase, baseItems, exclusions.itemIds);\n\tconst components = await getComponents(supabase, items, useBehaviors);\n\tconst rules = await getRules(supabase, baseItems, categories, items, useBehaviors);\n\n\treturn buildMenu(items, baseItems, categories, components, rules);\n}\n\nexport async function getCateringBaseItemIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<number[]> {\n\tconst { data } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.not(\"per_person\", \"is\", null)\n\t\t.throwOnError();\n\treturn data.map((row) => row.base_item_id);\n}\n\n/**\n * Attaches targeted promotions to menu cards without mutating the menu.\n * Order-wide promos intentionally stay out of menu display pricing.\n */\nexport function fitPromosToMenu(\n\tmenu: Menu,\n\tactivePromotions: Promotion[],\n): Menu {\n\tconst categoryPromoIds = new Map<number, number>();\n\tconst getDisplayPrice = (price: number, promo: Promotion) => {\n\t\tif (promo.promotion_type_id === 1) {\n\t\t\treturn Math.max(0, Math.floor(price * (1 - promo.discount_value / 100)));\n\t\t}\n\t\tif (promo.promotion_type_id === 2) {\n\t\t\treturn Math.max(0, price - promo.discount_value);\n\t\t}\n\t\treturn price;\n\t};\n\n\tconst items = menu.items.map((item) => {\n\t\t// Bulk discount promos (quantity-dependent sigmoid) attach to the item but never set a fixed display_price\n\t\tconst bulkPromo = activePromotions.find((promo) => {\n\t\t\tif (!promo.is_active || promo.id == null || !promo.discount_target_ids?.length || !promo.min_purchase_quantity) return false;\n\t\t\tconst targets = promo.discount_target_ids;\n\t\t\treturn (\n\t\t\t\t(promo.discount_target_type === \"category\" && targets.includes(item.category_id)) ||\n\t\t\t\t(promo.discount_target_type === \"item\" && targets.includes(item.base_item_id))\n\t\t\t);\n\t\t});\n\n\t\tlet itemPromoId: number | undefined = bulkPromo?.id;\n\n\t\tconst variants = item.variants.map((variant) => {\n\t\t\tlet bestPromo: Promotion | undefined;\n\t\t\tlet bestDisplayPrice = variant.price;\n\n\t\t\tfor (const promo of activePromotions) {\n\t\t\t\tif (\n\t\t\t\t\t!promo.is_active ||\n\t\t\t\t\tpromo.id == null ||\n\t\t\t\t\t!promo.discount_target_ids?.length ||\n\t\t\t\t\tpromo.min_purchase_amount || // order-level threshold — shown via modal, not baked into menu price\n\t\t\t\t\tpromo.min_purchase_quantity // quantity-dependent — handled above, no fixed display_price\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst targets = promo.discount_target_ids;\n\t\t\t\tconst applies =\n\t\t\t\t\t(promo.discount_target_type === \"category\" &&\n\t\t\t\t\t\ttargets.includes(item.category_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item\" &&\n\t\t\t\t\t\ttargets.includes(item.base_item_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item_variant\" &&\n\t\t\t\t\t\ttargets.includes(variant.item_id));\n\n\t\t\t\tif (!applies) continue;\n\n\t\t\t\tconst displayPrice = getDisplayPrice(variant.price, promo);\n\t\t\t\tif (displayPrice < bestDisplayPrice) {\n\t\t\t\t\tbestPromo = promo;\n\t\t\t\t\tbestDisplayPrice = displayPrice;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!bestPromo || bestDisplayPrice >= variant.price) {\n\t\t\t\treturn variant;\n\t\t\t}\n\n\t\t\titemPromoId ??= bestPromo.id!;\n\t\t\tif (bestPromo.discount_target_type === \"category\") {\n\t\t\t\tcategoryPromoIds.set(item.category_id, bestPromo.id!);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\t...variant,\n\t\t\t\tactive_promo_id: bestPromo.id!,\n\t\t\t\tdisplay_price: bestDisplayPrice,\n\t\t\t};\n\t\t});\n\n\t\treturn { ...item, variants, active_promo_id: itemPromoId };\n\t});\n\n\tconst categories = menu.categories.map((category) => {\n\t\tconst { active_promo_id, ...cleanCategory } = category;\n\t\tvoid active_promo_id;\n\n\t\tconst promoId = categoryPromoIds.get(cleanCategory.category_id);\n\n\t\treturn promoId == null\n\t\t\t? cleanCategory\n\t\t\t: { ...cleanCategory, active_promo_id: promoId };\n\t});\n\n\treturn {\n\t\t...menu,\n\t\titems,\n\t\tcategories,\n\t};\n}\n\nexport async function getMenuExclusions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tlocationId: number,\n\tplatform: Platforms,\n): Promise<MenuIds> {\n\tvoid locationId;\n\n\tconst menuIds = await getMenuIds(supabase, franchiseId);\n\tconst activeAssignments = await getActiveVisibilityAssignments(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\tplatform,\n\t);\n\n\tif (activeAssignments.length === 0) {\n\t\treturn emptyMenuExclusions();\n\t}\n\n\tconst categoryEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.category_id,\n\t);\n\tconst baseItemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.base_item_id,\n\t);\n\tconst itemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.item_id,\n\t);\n\n\tconst baseItemById = new Map(\n\t\tmenuIds.baseItems.map((baseItem) => [baseItem.base_item_id, baseItem]),\n\t);\n\tconst itemById = new Map(menuIds.items.map((item) => [item.item_id, item]));\n\n\tconst isCategoryIncluded = (categoryId: number) =>\n\t\tcategoryEffects.get(categoryId) !== \"exclude\";\n\n\tconst isBaseItemIncluded = (baseItemId: number) => {\n\t\tconst effect = baseItemEffects.get(baseItemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst categoryId = baseItemById.get(baseItemId)?.category_id;\n\t\treturn categoryId == null || isCategoryIncluded(categoryId);\n\t};\n\n\tconst isItemIncluded = (itemId: number): boolean => {\n\t\tconst effect = itemEffects.get(itemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst item = itemById.get(itemId);\n\t\tif (item?.base_item_id == null) return true;\n\n\t\treturn isBaseItemIncluded(item.base_item_id);\n\t};\n\n\tconst includedItems = new Set(\n\t\tmenuIds.items\n\t\t\t.filter((item) => isItemIncluded(item.item_id))\n\t\t\t.map((item) => item.item_id),\n\t);\n\tconst includedBaseItems = new Set(\n\t\tmenuIds.baseItems\n\t\t\t.filter((baseItem) => isBaseItemIncluded(baseItem.base_item_id))\n\t\t\t.map((baseItem) => baseItem.base_item_id),\n\t);\n\n\tfor (const item of menuIds.items) {\n\t\tif (includedItems.has(item.item_id) && item.base_item_id != null) {\n\t\t\tincludedBaseItems.add(item.base_item_id);\n\t\t}\n\t}\n\n\tconst includedCategories = new Set(\n\t\tmenuIds.categories\n\t\t\t.filter((category) => isCategoryIncluded(category.category_id))\n\t\t\t.map((category) => category.category_id),\n\t);\n\n\tfor (const baseItem of menuIds.baseItems) {\n\t\tif (includedBaseItems.has(baseItem.base_item_id)) {\n\t\t\tincludedCategories.add(baseItem.category_id);\n\t\t}\n\t}\n\n\treturn {\n\t\tcategoryIds: menuIds.categories\n\t\t\t.map((category) => category.category_id)\n\t\t\t.filter((categoryId) => !includedCategories.has(categoryId)),\n\t\tbaseItemIds: menuIds.baseItems\n\t\t\t.map((baseItem) => baseItem.base_item_id)\n\t\t\t.filter((baseItemId) => !includedBaseItems.has(baseItemId)),\n\t\titemIds: menuIds.items\n\t\t\t.map((item) => item.item_id)\n\t\t\t.filter((itemId) => !includedItems.has(itemId)),\n\t};\n}\n\nexport function getNameFromId(\n\tid: number,\n\ttype: \"category\" | \"item\" | \"item_variant\",\n\tmenu: Partial<Menu>,\n): string {\n\tif (type === \"category\") {\n\t\tconst category = menu.categories?.find(\n\t\t\t(category) => category.category_id === id,\n\t\t);\n\n\t\tif (category?.name) {\n\t\t\treturn category.name;\n\t\t}\n\t} else if (type === \"item\") {\n\t\tconst item = menu.items?.find((item: any) => item.base_item_id === id);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t} else if (type === \"item_variant\") {\n\t\tconst item = menu.items?.find((item: any) =>\n\t\t\titem.variants.find((variant: any) => variant.item_id === id),\n\t\t);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t}\n\n\treturn id.toString(); // default to showing the item id\n}\n\nfunction emptyMenuExclusions(): MenuIds {\n\treturn {\n\t\tcategoryIds: [],\n\t\tbaseItemIds: [],\n\t\titemIds: [],\n\t};\n}\n\nasync function getMenuIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<MenuIdRows> {\n\tconst { data: categories, error: categoriesError } = await supabase\n\t\t.from(\"categories\")\n\t\t.select(\"category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id, category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tconst baseItemIds = (baseItems ?? []).map((item) => item.base_item_id);\n\tconst { data: items, error: itemsError } =\n\t\tbaseItemIds.length > 0\n\t\t\t? await supabase\n\t\t\t\t.from(\"items\")\n\t\t\t\t.select(\"item_id, base_item_id\")\n\t\t\t\t.in(\"base_item_id\", baseItemIds)\n\t\t\t: { data: [], error: null };\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn {\n\t\tcategories: categories ?? [],\n\t\tbaseItems: baseItems ?? [],\n\t\titems: items ?? [],\n\t};\n}\n\nasync function getActiveVisibilityAssignments(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n): Promise<VisibilityConditionGroupAssignment[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"visibility_condition_group_assignments\")\n\t\t.select(\"*, visibility_condition_groups (*)\")\n\t\t.eq(\"visibility_condition_groups.franchise_id\", franchiseId)\n\t\t.eq(\"visibility_condition_groups.platform\", platform);\n\n\tif (error || !data) {\n\t\treturn [];\n\t}\n\n\tconst context = buildVisibilityEvaluationContext({ platform });\n\n\treturn (data as VisibilityConditionGroupAssignment[]).filter((assignment) => {\n\t\tconst group = assignment.visibility_condition_groups;\n\n\t\tif (!group?.condition || group.platform !== context.platform) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\treturn evaluateVisibilityCondition(group.condition, context);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t});\n}\n\nfunction buildTargetEffectMap(\n\tassignments: VisibilityConditionGroupAssignment[],\n\tselector: (\n\t\tassignment: VisibilityConditionGroupAssignment,\n\t) => number | null | undefined,\n): Map<number, VisibilityEffect> {\n\tconst effectsByTarget = new Map<number, VisibilityEffect[]>();\n\n\tfor (const assignment of assignments) {\n\t\tconst targetId = selector(assignment);\n\t\tif (targetId == null) continue;\n\n\t\teffectsByTarget.set(targetId, [\n\t\t\t...(effectsByTarget.get(targetId) ?? []),\n\t\t\tassignment.effect,\n\t\t]);\n\t}\n\n\treturn new Map(\n\t\tArray.from(effectsByTarget.entries()).map(([targetId, effects]) => [\n\t\t\ttargetId,\n\t\t\teffects.includes(\"exclude\") ? \"exclude\" : \"include\",\n\t\t]),\n\t);\n}\n\nasync function getCategories(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n): Promise<Category[]> {\n\tlet categoriesQuery = supabase\n\t\t.from(\"categories\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tcategoriesQuery = categoriesQuery.not(\n\t\t\t\"category_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tconst { data: categories, error: categoriesError } = await categoriesQuery;\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\treturn categories ?? [];\n}\n\nasync function getBaseItems(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n\tcategories?: Category[],\n): Promise<BaseItem[]> {\n\tif (categories && categories.length === 0) {\n\t\treturn [];\n\t}\n\n\tlet baseItemsQuery = supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tbaseItemsQuery = baseItemsQuery.not(\n\t\t\t\"base_item_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tif (categories) {\n\t\tbaseItemsQuery = baseItemsQuery.in(\n\t\t\t\"category_id\",\n\t\t\tcategories.map((category) => category.category_id),\n\t\t);\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await baseItemsQuery;\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tif (!baseItems || baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst { data: images, error: imagesError } = await supabase\n\t\t.from(\"base_item_images\")\n\t\t.select(\"*\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t)\n\t\t.order(\"sort_index\", { ascending: true })\n\t\t.order(\"image_id\", { ascending: true });\n\n\tif (imagesError) {\n\t\tthrow imagesError;\n\t}\n\n\treturn attachImages(baseItems, images ?? []);\n}\n\n/**\n * Group images onto their base items. `images` must already be in the order\n * callers should see them (sort_index, then image_id) — grouping preserves it,\n * so every render site can take `images[0]` as the primary image.\n */\nexport function attachImages<T extends { base_item_id: number }>(\n\tbaseItems: T[],\n\timages: BaseItemImage[],\n): Array<T & { images: BaseItemImage[] }> {\n\tconst byBaseItemId = new Map<number, BaseItemImage[]>();\n\tfor (const image of images) {\n\t\tconst group = byBaseItemId.get(image.base_item_id);\n\t\tif (group) group.push(image);\n\t\telse byBaseItemId.set(image.base_item_id, [image]);\n\t}\n\n\treturn baseItems.map((baseItem) => ({\n\t\t...baseItem,\n\t\timages: byBaseItemId.get(baseItem.base_item_id) ?? [],\n\t}));\n}\n\nexport function menuImageUrl(\n\timage?: BaseItemImage | null,\n\tabsolute = false,\n): string | null {\n\tif (!image) return null;\n\tif (image.local_key && !absolute) return `/images/items/${image.local_key}`;\n\tconst path = image.processed_path ?? image.raw_path;\n\tif (!path) return null;\n\treturn `${SUPABASE_URL}/storage/v1/object/public/product-images/${path}`;\n}\n\nasync function getItems(\n\tsupabase: SupabaseClient,\n\tbaseItems: BaseItem[],\n\texcludedIds?: number[],\n): Promise<ItemVariant[]> {\n\tif (baseItems && baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\t// the scheme rides along so the prebuilt menu carries it — no runtime fetch\n\t// on the item page just to know how a pizza can be cut\n\tlet itemsQuery = supabase\n\t\t.from(\"items\")\n\t\t.select(\"*, portion_scheme:portion_schemes(*)\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\titemsQuery = itemsQuery.not(\"item_id\", \"in\", csvFormat(excludedIds));\n\t}\n\n\tconst { data: items, error: itemsError } = await itemsQuery;\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn items ?? [];\n}\n\nexport async function getComponents(\n\tsupabase: SupabaseClient,\n\titems: ItemVariant[] | number[],\n\tuseBehaviors = false,\n): Promise<Component[]> {\n\tconst itemIds = items.length > 0 && typeof items[0] === \"number\"\n\t\t? (items as number[])\n\t\t: (items as ItemVariant[]).map((item) => item.item_id);\n\n\tif (itemIds.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst select = useBehaviors\n\t\t? \"components(*), item_id, servings_per_item, behavior_id, component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: \"components(*), item_id, servings_per_item\";\n\n\tconst { data: components } = await supabase\n\t\t.from(\"item_components\")\n\t\t.select(select)\n\t\t.in(\"item_id\", itemIds)\n\t\t.throwOnError();\n\n\treturn components.map((component: any) => ({\n\t\tservings_per_item: component.servings_per_item,\n\t\tis_base: component.components?.is_base,\n\t\tserving_weight: component.components?.serving_weight,\n\t\tcomponent_type_id: component.components?.component_type_id,\n\t\tid: component.components?.id,\n\t\tname: component.components?.name,\n\t\titem_id: component.item_id,\n\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t}));\n}\n\n/**\n * A rule's options — components and items — as one keyed list. The key is what\n * `portionKey` builds on and what `weightedUpcharges` prices, so the picker and\n * the server's repricing read option prices through this same function.\n */\nexport function ruleOptionDefs(rule: Rule) {\n\treturn [\n\t\t...(rule.components ?? []).map((c) => ({\n\t\t\tkey: `component:${c.component_id}`,\n\t\t\tid: c.component_id,\n\t\t\ttype: \"component\" as const,\n\t\t\tname: c.name,\n\t\t\tbasePrice: c.upcharge_price || 0,\n\t\t\tdescription: c.description,\n\t\t})),\n\t\t...(rule.items ?? []).map((i) => ({\n\t\t\tkey: `item:${i.item_id}`,\n\t\t\tid: i.item_id,\n\t\t\ttype: \"item\" as const,\n\t\t\tname: i.name,\n\t\t\tbasePrice: i.upcharge_price || 0,\n\t\t\tdescription: i.description,\n\t\t})),\n\t];\n}\n\n/**\n * Rules attach at item, base-item, or category level; `ruleIds` fetches them by\n * primary key instead, for when you already hold the ids (repricing an order's\n * selections server-side).\n */\nexport async function getRules(\n\tsupabase: SupabaseClient,\n\tbaseItems?: BaseItem[],\n\tcategories?: Category[],\n\titems?: ItemVariant[],\n\tuseBehaviors = false,\n\truleIds?: number[],\n): Promise<Rule[]> {\n\tconst itemIds = items?.map((item) => item.item_id) ?? [];\n\tconst baseItemIds = baseItems?.map((baseItem) => baseItem.base_item_id) ?? [];\n\tconst categoryIds = categories?.map((category) => category.category_id) ?? [];\n\n\tif (\n\t\titemIds.length === 0 &&\n\t\tbaseItemIds.length === 0 &&\n\t\tcategoryIds.length === 0 &&\n\t\t!ruleIds?.length\n\t) {\n\t\treturn [];\n\t}\n\n\tconst orFilter = [\n\t\t`item_id.in.(${itemIds.length > 0 ? itemIds.join(\",\") : -1})`,\n\t\t`base_item_id.in.(${baseItemIds.length > 0 ? baseItemIds.join(\",\") : -1})`,\n\t\t`category_id.in.(${categoryIds.length > 0 ? categoryIds.join(\",\") : -1})`,\n\t\t...(ruleIds?.length ? [`rule_id.in.(${ruleIds.join(\",\")})`] : []),\n\t].join(\",\");\n\n\tconst behaviorSelect = useBehaviors\n\t\t? \"component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: null;\n\n\tconst ruleComponentsSelect = [\n\t\t\"component_id, upcharge_price, components(name, description, serving_weight)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst ruleItemsSelect = [\n\t\t\"item_id, upcharge_price, items(item_id, name, description)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst { data: ruleData, error: rulesError } = await supabase\n\t\t.from(\"item_rules\")\n\t\t.select(\n\t\t\t`\n rule_id,\n category_id,\n item_id,\n base_item_id,\n group_name,\n min_selection,\n max_selection,\n included_quantity,\n is_portionable,\n rule_components (${ruleComponentsSelect}),\n rule_items (${ruleItemsSelect})\n `,\n\t\t)\n\t\t.or(orFilter);\n\n\tif (rulesError) {\n\t\tthrow rulesError;\n\t}\n\n\treturn (ruleData ?? []).map((rule: any) => ({\n\t\trule_id: rule.rule_id,\n\t\tgroup_name: rule.group_name,\n\t\tmin_selection: rule.min_selection,\n\t\tmax_selection: rule.max_selection,\n\t\tincluded_quantity: rule.included_quantity,\n\t\tis_portionable: rule.is_portionable,\n\t\tcategory_id: rule.category_id,\n\t\titem_id: rule.item_id,\n\t\tbase_item_id: rule.base_item_id,\n\n\t\tcomponents: (rule.rule_components ?? []).map((component: any) => ({\n\t\t\tcomponent_id: component.component_id,\n\t\t\tupcharge_price: component.upcharge_price ?? 0,\n\t\t\tname: component.components?.name ?? \"\",\n\t\t\tdescription: component.components?.description ?? undefined,\n\t\t\tserving_weight: component.components?.serving_weight ?? undefined,\n\t\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t\t})),\n\n\t\titems: (rule.rule_items ?? []).map((item: any) => ({\n\t\t\titem_id: item.item_id,\n\t\t\tname: item.items?.name ?? \"\",\n\t\t\tdescription: item.items?.description ?? undefined,\n\t\t\tupcharge_price: item.upcharge_price ?? 0,\n\t\t\t...(useBehaviors && { behavior: item.component_quantity_behaviors ?? undefined }),\n\t\t})),\n\t}));\n}\n\nfunction buildMenu(\n\titems: ItemVariant[],\n\tbaseItems: BaseItem[],\n\tcategories: Category[],\n\tcomponents: Component[],\n\trules: Rule[],\n): Menu {\n\tconst menu: Menu = {\n\t\titems: [],\n\t\tcategories: [],\n\t\trules,\n\t\tcomponents,\n\t};\n\n\tmenu.items = baseItems.map((baseItem) => {\n\t\tconst categoryRules = rules.filter((rule) => rule.category_id === baseItem.category_id);\n\t\tconst baseItemRules = rules.filter((rule) => rule.base_item_id === baseItem.base_item_id);\n\t\treturn {\n\t\t\tis_catering: (baseItem as any).per_person !== null,\n\t\t\t...baseItem,\n\t\t\tvariants: items\n\t\t\t\t.filter((item) => item.base_item_id === baseItem.base_item_id)\n\t\t\t\t.map((item) => ({\n\t\t\t\t\t...item,\n\t\t\t\t\tcomponents: components.filter(\n\t\t\t\t\t\t(component) => component.item_id === item.item_id,\n\t\t\t\t\t),\n\t\t\t\t\trules: [\n\t\t\t\t\t\t...categoryRules,\n\t\t\t\t\t\t...baseItemRules,\n\t\t\t\t\t\t...rules.filter((rule) => rule.item_id === item.item_id),\n\t\t\t\t\t],\n\t\t\t\t})),\n\t\t\trules: baseItemRules,\n\t\t};\n\t});\n\n\tmenu.categories = categories.map((category) => ({\n\t\t...category,\n\t\trules: rules.filter((rule) => rule.category_id === category.category_id),\n\t}));\n\n\treturn menu;\n}\n\nexport function getBaseItemFromVariant(\n\tmenu: MenuItem[],\n\tvariant: ItemVariant,\n\tbaseItemId?: number,\n): MenuItem | undefined {\n\tif (baseItemId != null) {\n\t\treturn menu.find((i) => i.base_item_id === baseItemId);\n\t}\n\treturn menu.find((i) => i.variants.some((v) => v.item_id === variant.item_id));\n}\n\nexport function getMenuItemByVariantId(menu: MenuItem[], variantId: number): MenuItem | undefined {\n\treturn menu.find((item) =>\n\t\titem.variants?.some((v) => v.item_id === variantId)\n\t);\n}\n\nexport function filterBySchedule(\n\tpromos: Promotion[],\n\tplatform: Platforms,\n\tlocationId: number | undefined,\n\tnow: Date = new Date(),\n): Promotion[] {\n\treturn promos.filter((promo) => {\n\t\tif (promo.start_date && new Date(promo.start_date) > now) return false;\n\t\tif (promo.end_date && new Date(promo.end_date) < now) return false;\n\n\t\tif (promo.platforms && promo.platforms.length > 0) {\n\t\t\tif (!promo.platforms.includes(platform)) return false;\n\t\t}\n\n\t\tif (!promo.is_automatic) return false;\n\n\t\tif (promo.location_id != null && locationId !== undefined) {\n\t\t\tif (promo.location_id !== locationId) return false;\n\t\t} else if (promo.location_id != null && locationId === undefined) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (promo.schedule_details_json) {\n\t\t\tconst { interval, days, start_time, end_time } =\n\t\t\t\tpromo.schedule_details_json;\n\n\t\t\tif (interval === \"weekly\" && days && !days.includes(getDay(now) + 1)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst dayStart = startOfDay(now);\n\t\t\tif (start_time && isBefore(now, parse(start_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (end_time && isAfter(now, parse(end_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t});\n}\n\nexport async function getActivePromotions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n\tlocationId?: number,\n): Promise<Promotion[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"promotions\")\n\t\t.select(\"*\")\n\t\t.eq(\"is_active\", true)\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (error) throw error;\n\n\treturn filterBySchedule((data || []) as Promotion[], platform, locationId);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,eAAe;AASrB,IAAM,eACZ,QAAQ,IAAI,aAAa,eACtB,8BACA;;;AC8gBG,SAAS,aACf,OACA,WAAW,OACK;AAChB,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,MAAM,aAAa,CAAC,SAAU,QAAO,iBAAiB,MAAM,SAAS;AACzE,QAAM,OAAO,MAAM,kBAAkB,MAAM;AAC3C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,GAAG,YAAY,4CAA4C,IAAI;AACvE;;;AF3hBO,IAAM,oBAAoB,CAAC,KAAK,KAAK,IAAI;AAGzC,IAAM,oBAAoB;AAYlB,SAAR,gBAAiC;AAAA,EACvC;AAAA,EACA;AACD,GAGW;AACV,MAAI,CAAC,IAAI,WAAW,iBAAiB,EAAG,QAAO;AAC/C,QAAM,OACL,kBAAkB,KAAK,CAAC,cAAc,aAAa,KAAK,KACxD,kBAAkB,kBAAkB,SAAS,CAAC;AAC/C,SAAO,GAAG,GAAG,IAAI,IAAI;AACtB;","names":[]}
@@ -0,0 +1,32 @@
1
+ // ../zorgo/universal/constants.ts
2
+ var SUPABASE_URL = "https://dhnpoxorllfdobwftkyb.supabase.co";
3
+ var API_BASE_URL = process.env.NODE_ENV === "production" ? "https://api.zorgotech.com" : "http://localhost:3000";
4
+
5
+ // ../zorgo/universal/utils/menuUtils.ts
6
+ function menuImageUrl(image, absolute = false) {
7
+ if (!image) return null;
8
+ if (image.local_key && !absolute) return `/images/items/${image.local_key}`;
9
+ const path = image.processed_path ?? image.raw_path;
10
+ if (!path) return null;
11
+ return `${SUPABASE_URL}/storage/v1/object/public/product-images/${path}`;
12
+ }
13
+
14
+ // src/images.ts
15
+ var ITEM_IMAGE_WIDTHS = [384, 640, 1080];
16
+ var ITEM_IMAGE_PREFIX = "/images/items/";
17
+ function itemImageLoader({
18
+ src,
19
+ width
20
+ }) {
21
+ if (!src.startsWith(ITEM_IMAGE_PREFIX)) return src;
22
+ const rung = ITEM_IMAGE_WIDTHS.find((candidate) => candidate >= width) ?? ITEM_IMAGE_WIDTHS[ITEM_IMAGE_WIDTHS.length - 1];
23
+ return `${src}-${rung}.avif`;
24
+ }
25
+ export {
26
+ ITEM_IMAGE_PREFIX,
27
+ ITEM_IMAGE_WIDTHS,
28
+ itemImageLoader as default,
29
+ itemImageLoader,
30
+ menuImageUrl
31
+ };
32
+ //# sourceMappingURL=images.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../zorgo/universal/constants.ts","../../zorgo/universal/utils/menuUtils.ts","../src/images.ts"],"sourcesContent":["export const SUPABASE_URL = \"https://dhnpoxorllfdobwftkyb.supabase.co\";\n\nexport const SUPABASE_ANON_KEY =\n\t\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRobnBveG9ybGxmZG9id2Z0a3liIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwNTQxMDgsImV4cCI6MjA1NDYzMDEwOH0.zI8PwbgKwijHZ1QmxJqmaPrFvg02w1vOY2Qcs7SddzI\";\nexport const ZORGO_PURPLE = \"rgb(153,67,233)\";\n\nexport const expoGoGoogleClientId =\n\t\"959654249401-dm6nbi27rj605ul1nkt8o2imb8f7daff.apps.googleusercontent.com\";\n\nexport const API_BASE_URL =\n\tprocess.env.NODE_ENV === \"production\"\n\t\t? \"https://api.zorgotech.com\"\n\t\t: \"http://localhost:3000\";\n\n\n// replace instances of API_BASE_URL with env enum\nexport enum Environments {\n\tProduction = \"https://api.zorgotech.com\",\n\tStaging = \"http://api.zorgo.ai\",\n\tDevelopment = \"http://localhost:3000\",\n}\n/** Emails allowed to access developer-only metrics (e.g. /performance/developer). */\nexport const DEVELOPER_EMAILS = [\"clawsorgo@gmail.com\", \"gmandwee@gmail.com\"] as const;\n\nexport function isDeveloperEmail(email?: string | null): boolean {\n\treturn !!email && (DEVELOPER_EMAILS as readonly string[]).includes(email);\n}\n\nexport const ORDER_STORE_CONSTANTS = {\n\tDEFAULTS: {\n\t\tPREP_MINUTES: 20, // default amount of time for prep\n\t\tPAST_HOURS: 24, // hours into the past orders are fetched for\n\t\tFUTURE_HOURS: 72 // hours into the future orders are fetched for \n\t},\n\tORDER_STATUS: {\n\t\tSTAGING: 10,\n\t\tPENDING: 1,\n\t\tCOMPLETED: 2,\n\t\tCANCELLED: 4,\n\t\tPAID: 6,\n\t\tPARTIALLY_PAID: 7,\n\t\tKITCHEN_DONE: 9,\n\t\tCONFIRMED: 11,\n\t\tPREPARING: 12,\n\t},\n\tPOS_METHOD_ID: 3,\n\tORDER_TYPE: {\n\t\tDINE_IN: 1,\n\t\tTAKEOUT: 2,\n\t\tDELIVERY: 3\n\t},\n\t/** KDS edit highlight duration after a POS modification. */\n\tMODIFICATION_HIGHLIGHT_MS: 8_000,\n\t/** POS order realtime: refetch + recycle postgres_changes channel on this cadence. */\n\tLISTENER_SYNC_INTERVAL_MS: 60_000,\n\tORDER_SELECT: `\n\t*,\n\n\tcharges (\n\t\t*\n\t),\n\n\tlocations (name),\n\n\tcustomers\n\t (customer_id, first_name, last_name, phone_number, email),\n\n\torder_items\n\t (\n\t\torder_item_id, item_id, quantity, price_at_order, order_id, comments, status, applied_promo_id, discount_amount,\n\t\titems (\n\t\t\titem_id, name, price,\n\t\t\tportion_schemes (id, franchise_id, geometry, name, min_divisions, max_divisions),\n\t\t\tbase_items (\n\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t)\n\t\t),\n\t\torder_item_selected_items (\n\t\t\torder_item_id, rule_id, item_id, quantity, upcharge_price, portion,\n\t\t\titems (\n\t\t\t\titem_id, name, price,\n\t\t\t\tbase_items (\n\t\t\t\t\tbase_item_id, category_id, name,\n\t\t\t\t\tcategories (category_id, name, is_catering)\n\t\t\t\t)\n\t\t\t),\n\t\t\titem_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_selected_components (\n\t\t\torder_item_id, rule_id, component_id, quantity, upcharge_price, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit), item_rules (rule_id, group_name)\n\t\t),\n\t\torder_item_components (\n\t\t\torder_item_id, component_id, quantity_servings, is_base, portion,\n\t\t\tcomponents (id, name, component_type_id, serving_weight, measurement_type, display_unit)\n\t\t)\n\t)\n`,\n};\n\nexport const CHARGE_STATUSES = {\n\tSUCCESS: 1,\n\tPENDING: 2,\n\tREFUNDED: 3,\n\tVOIDED: 4,\n\tPAYMENT_QUEUED: 5,\n\tFAILED: 6\n}\n","import { SupabaseClient } from \"@supabase/supabase-js\";\nimport { getDay, isAfter, isBefore, parse, startOfDay } from \"date-fns\";\nimport { SUPABASE_URL } from \"../constants\";\nimport {\n\tBaseItem,\n\tBaseItemImage,\n\tCategory,\n\tComponent,\n\tItemVariant,\n\tMenuItem,\n\tPlatforms,\n\tPromotion,\n\tRule,\n\tVisibilityConditionGroupAssignment,\n\tVisibilityEffect,\n} from \"../interfaces\";\nimport { csvFormat } from \"./misc\";\nimport {\n\tbuildVisibilityEvaluationContext,\n\tevaluateVisibilityCondition,\n} from \"./visibilityConditions\";\n\nexport interface Menu {\n\titems: MenuItem[];\n\tcategories: Category[];\n\tcomponents: Component[];\n\trules: Rule[];\n}\n\nexport interface MenuIds {\n\tbaseItemIds: number[];\n\tcategoryIds: number[];\n\titemIds: number[];\n}\n\ntype MenuIdRows = {\n\tcategories: Pick<Category, \"category_id\">[];\n\tbaseItems: Pick<BaseItem, \"base_item_id\" | \"category_id\">[];\n\titems: Pick<ItemVariant, \"item_id\" | \"base_item_id\">[];\n};\n\nexport async function getMenu(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texclusions: MenuIds = emptyMenuExclusions(),\n\tuseBehaviors = false,\n): Promise<Menu> {\n\tconst categories = await getCategories(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.categoryIds,\n\t);\n\n\tif (categories.length === 0) {\n\t\treturn buildMenu([], [], [], [], []);\n\t}\n\n\tconst baseItems = await getBaseItems(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\texclusions.baseItemIds,\n\t\tcategories,\n\t);\n\n\tif (baseItems.length === 0) {\n\t\treturn buildMenu([], [], categories, [], []);\n\t}\n\n\tconst items = await getItems(supabase, baseItems, exclusions.itemIds);\n\tconst components = await getComponents(supabase, items, useBehaviors);\n\tconst rules = await getRules(supabase, baseItems, categories, items, useBehaviors);\n\n\treturn buildMenu(items, baseItems, categories, components, rules);\n}\n\nexport async function getCateringBaseItemIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<number[]> {\n\tconst { data } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.not(\"per_person\", \"is\", null)\n\t\t.throwOnError();\n\treturn data.map((row) => row.base_item_id);\n}\n\n/**\n * Attaches targeted promotions to menu cards without mutating the menu.\n * Order-wide promos intentionally stay out of menu display pricing.\n */\nexport function fitPromosToMenu(\n\tmenu: Menu,\n\tactivePromotions: Promotion[],\n): Menu {\n\tconst categoryPromoIds = new Map<number, number>();\n\tconst getDisplayPrice = (price: number, promo: Promotion) => {\n\t\tif (promo.promotion_type_id === 1) {\n\t\t\treturn Math.max(0, Math.floor(price * (1 - promo.discount_value / 100)));\n\t\t}\n\t\tif (promo.promotion_type_id === 2) {\n\t\t\treturn Math.max(0, price - promo.discount_value);\n\t\t}\n\t\treturn price;\n\t};\n\n\tconst items = menu.items.map((item) => {\n\t\t// Bulk discount promos (quantity-dependent sigmoid) attach to the item but never set a fixed display_price\n\t\tconst bulkPromo = activePromotions.find((promo) => {\n\t\t\tif (!promo.is_active || promo.id == null || !promo.discount_target_ids?.length || !promo.min_purchase_quantity) return false;\n\t\t\tconst targets = promo.discount_target_ids;\n\t\t\treturn (\n\t\t\t\t(promo.discount_target_type === \"category\" && targets.includes(item.category_id)) ||\n\t\t\t\t(promo.discount_target_type === \"item\" && targets.includes(item.base_item_id))\n\t\t\t);\n\t\t});\n\n\t\tlet itemPromoId: number | undefined = bulkPromo?.id;\n\n\t\tconst variants = item.variants.map((variant) => {\n\t\t\tlet bestPromo: Promotion | undefined;\n\t\t\tlet bestDisplayPrice = variant.price;\n\n\t\t\tfor (const promo of activePromotions) {\n\t\t\t\tif (\n\t\t\t\t\t!promo.is_active ||\n\t\t\t\t\tpromo.id == null ||\n\t\t\t\t\t!promo.discount_target_ids?.length ||\n\t\t\t\t\tpromo.min_purchase_amount || // order-level threshold — shown via modal, not baked into menu price\n\t\t\t\t\tpromo.min_purchase_quantity // quantity-dependent — handled above, no fixed display_price\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst targets = promo.discount_target_ids;\n\t\t\t\tconst applies =\n\t\t\t\t\t(promo.discount_target_type === \"category\" &&\n\t\t\t\t\t\ttargets.includes(item.category_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item\" &&\n\t\t\t\t\t\ttargets.includes(item.base_item_id)) ||\n\t\t\t\t\t(promo.discount_target_type === \"item_variant\" &&\n\t\t\t\t\t\ttargets.includes(variant.item_id));\n\n\t\t\t\tif (!applies) continue;\n\n\t\t\t\tconst displayPrice = getDisplayPrice(variant.price, promo);\n\t\t\t\tif (displayPrice < bestDisplayPrice) {\n\t\t\t\t\tbestPromo = promo;\n\t\t\t\t\tbestDisplayPrice = displayPrice;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!bestPromo || bestDisplayPrice >= variant.price) {\n\t\t\t\treturn variant;\n\t\t\t}\n\n\t\t\titemPromoId ??= bestPromo.id!;\n\t\t\tif (bestPromo.discount_target_type === \"category\") {\n\t\t\t\tcategoryPromoIds.set(item.category_id, bestPromo.id!);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\t...variant,\n\t\t\t\tactive_promo_id: bestPromo.id!,\n\t\t\t\tdisplay_price: bestDisplayPrice,\n\t\t\t};\n\t\t});\n\n\t\treturn { ...item, variants, active_promo_id: itemPromoId };\n\t});\n\n\tconst categories = menu.categories.map((category) => {\n\t\tconst { active_promo_id, ...cleanCategory } = category;\n\t\tvoid active_promo_id;\n\n\t\tconst promoId = categoryPromoIds.get(cleanCategory.category_id);\n\n\t\treturn promoId == null\n\t\t\t? cleanCategory\n\t\t\t: { ...cleanCategory, active_promo_id: promoId };\n\t});\n\n\treturn {\n\t\t...menu,\n\t\titems,\n\t\tcategories,\n\t};\n}\n\nexport async function getMenuExclusions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tlocationId: number,\n\tplatform: Platforms,\n): Promise<MenuIds> {\n\tvoid locationId;\n\n\tconst menuIds = await getMenuIds(supabase, franchiseId);\n\tconst activeAssignments = await getActiveVisibilityAssignments(\n\t\tsupabase,\n\t\tfranchiseId,\n\t\tplatform,\n\t);\n\n\tif (activeAssignments.length === 0) {\n\t\treturn emptyMenuExclusions();\n\t}\n\n\tconst categoryEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.category_id,\n\t);\n\tconst baseItemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.base_item_id,\n\t);\n\tconst itemEffects = buildTargetEffectMap(\n\t\tactiveAssignments,\n\t\t(assignment) => assignment.item_id,\n\t);\n\n\tconst baseItemById = new Map(\n\t\tmenuIds.baseItems.map((baseItem) => [baseItem.base_item_id, baseItem]),\n\t);\n\tconst itemById = new Map(menuIds.items.map((item) => [item.item_id, item]));\n\n\tconst isCategoryIncluded = (categoryId: number) =>\n\t\tcategoryEffects.get(categoryId) !== \"exclude\";\n\n\tconst isBaseItemIncluded = (baseItemId: number) => {\n\t\tconst effect = baseItemEffects.get(baseItemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst categoryId = baseItemById.get(baseItemId)?.category_id;\n\t\treturn categoryId == null || isCategoryIncluded(categoryId);\n\t};\n\n\tconst isItemIncluded = (itemId: number): boolean => {\n\t\tconst effect = itemEffects.get(itemId);\n\t\tif (effect === \"include\") return true;\n\t\tif (effect === \"exclude\") return false;\n\n\t\tconst item = itemById.get(itemId);\n\t\tif (item?.base_item_id == null) return true;\n\n\t\treturn isBaseItemIncluded(item.base_item_id);\n\t};\n\n\tconst includedItems = new Set(\n\t\tmenuIds.items\n\t\t\t.filter((item) => isItemIncluded(item.item_id))\n\t\t\t.map((item) => item.item_id),\n\t);\n\tconst includedBaseItems = new Set(\n\t\tmenuIds.baseItems\n\t\t\t.filter((baseItem) => isBaseItemIncluded(baseItem.base_item_id))\n\t\t\t.map((baseItem) => baseItem.base_item_id),\n\t);\n\n\tfor (const item of menuIds.items) {\n\t\tif (includedItems.has(item.item_id) && item.base_item_id != null) {\n\t\t\tincludedBaseItems.add(item.base_item_id);\n\t\t}\n\t}\n\n\tconst includedCategories = new Set(\n\t\tmenuIds.categories\n\t\t\t.filter((category) => isCategoryIncluded(category.category_id))\n\t\t\t.map((category) => category.category_id),\n\t);\n\n\tfor (const baseItem of menuIds.baseItems) {\n\t\tif (includedBaseItems.has(baseItem.base_item_id)) {\n\t\t\tincludedCategories.add(baseItem.category_id);\n\t\t}\n\t}\n\n\treturn {\n\t\tcategoryIds: menuIds.categories\n\t\t\t.map((category) => category.category_id)\n\t\t\t.filter((categoryId) => !includedCategories.has(categoryId)),\n\t\tbaseItemIds: menuIds.baseItems\n\t\t\t.map((baseItem) => baseItem.base_item_id)\n\t\t\t.filter((baseItemId) => !includedBaseItems.has(baseItemId)),\n\t\titemIds: menuIds.items\n\t\t\t.map((item) => item.item_id)\n\t\t\t.filter((itemId) => !includedItems.has(itemId)),\n\t};\n}\n\nexport function getNameFromId(\n\tid: number,\n\ttype: \"category\" | \"item\" | \"item_variant\",\n\tmenu: Partial<Menu>,\n): string {\n\tif (type === \"category\") {\n\t\tconst category = menu.categories?.find(\n\t\t\t(category) => category.category_id === id,\n\t\t);\n\n\t\tif (category?.name) {\n\t\t\treturn category.name;\n\t\t}\n\t} else if (type === \"item\") {\n\t\tconst item = menu.items?.find((item: any) => item.base_item_id === id);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t} else if (type === \"item_variant\") {\n\t\tconst item = menu.items?.find((item: any) =>\n\t\t\titem.variants.find((variant: any) => variant.item_id === id),\n\t\t);\n\n\t\tif (item?.name) {\n\t\t\treturn item.name;\n\t\t}\n\t}\n\n\treturn id.toString(); // default to showing the item id\n}\n\nfunction emptyMenuExclusions(): MenuIds {\n\treturn {\n\t\tcategoryIds: [],\n\t\tbaseItemIds: [],\n\t\titemIds: [],\n\t};\n}\n\nasync function getMenuIds(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n): Promise<MenuIdRows> {\n\tconst { data: categories, error: categoriesError } = await supabase\n\t\t.from(\"categories\")\n\t\t.select(\"category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id, category_id\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tconst baseItemIds = (baseItems ?? []).map((item) => item.base_item_id);\n\tconst { data: items, error: itemsError } =\n\t\tbaseItemIds.length > 0\n\t\t\t? await supabase\n\t\t\t\t.from(\"items\")\n\t\t\t\t.select(\"item_id, base_item_id\")\n\t\t\t\t.in(\"base_item_id\", baseItemIds)\n\t\t\t: { data: [], error: null };\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn {\n\t\tcategories: categories ?? [],\n\t\tbaseItems: baseItems ?? [],\n\t\titems: items ?? [],\n\t};\n}\n\nasync function getActiveVisibilityAssignments(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n): Promise<VisibilityConditionGroupAssignment[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"visibility_condition_group_assignments\")\n\t\t.select(\"*, visibility_condition_groups (*)\")\n\t\t.eq(\"visibility_condition_groups.franchise_id\", franchiseId)\n\t\t.eq(\"visibility_condition_groups.platform\", platform);\n\n\tif (error || !data) {\n\t\treturn [];\n\t}\n\n\tconst context = buildVisibilityEvaluationContext({ platform });\n\n\treturn (data as VisibilityConditionGroupAssignment[]).filter((assignment) => {\n\t\tconst group = assignment.visibility_condition_groups;\n\n\t\tif (!group?.condition || group.platform !== context.platform) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\treturn evaluateVisibilityCondition(group.condition, context);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t});\n}\n\nfunction buildTargetEffectMap(\n\tassignments: VisibilityConditionGroupAssignment[],\n\tselector: (\n\t\tassignment: VisibilityConditionGroupAssignment,\n\t) => number | null | undefined,\n): Map<number, VisibilityEffect> {\n\tconst effectsByTarget = new Map<number, VisibilityEffect[]>();\n\n\tfor (const assignment of assignments) {\n\t\tconst targetId = selector(assignment);\n\t\tif (targetId == null) continue;\n\n\t\teffectsByTarget.set(targetId, [\n\t\t\t...(effectsByTarget.get(targetId) ?? []),\n\t\t\tassignment.effect,\n\t\t]);\n\t}\n\n\treturn new Map(\n\t\tArray.from(effectsByTarget.entries()).map(([targetId, effects]) => [\n\t\t\ttargetId,\n\t\t\teffects.includes(\"exclude\") ? \"exclude\" : \"include\",\n\t\t]),\n\t);\n}\n\nasync function getCategories(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n): Promise<Category[]> {\n\tlet categoriesQuery = supabase\n\t\t.from(\"categories\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tcategoriesQuery = categoriesQuery.not(\n\t\t\t\"category_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tconst { data: categories, error: categoriesError } = await categoriesQuery;\n\n\tif (categoriesError) {\n\t\tthrow categoriesError;\n\t}\n\n\treturn categories ?? [];\n}\n\nasync function getBaseItems(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\texcludedIds?: number[],\n\tcategories?: Category[],\n): Promise<BaseItem[]> {\n\tif (categories && categories.length === 0) {\n\t\treturn [];\n\t}\n\n\tlet baseItemsQuery = supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"*\")\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\tbaseItemsQuery = baseItemsQuery.not(\n\t\t\t\"base_item_id\",\n\t\t\t\"in\",\n\t\t\tcsvFormat(excludedIds),\n\t\t);\n\t}\n\n\tif (categories) {\n\t\tbaseItemsQuery = baseItemsQuery.in(\n\t\t\t\"category_id\",\n\t\t\tcategories.map((category) => category.category_id),\n\t\t);\n\t}\n\n\tconst { data: baseItems, error: baseItemsError } = await baseItemsQuery;\n\n\tif (baseItemsError) {\n\t\tthrow baseItemsError;\n\t}\n\n\tif (!baseItems || baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst { data: images, error: imagesError } = await supabase\n\t\t.from(\"base_item_images\")\n\t\t.select(\"*\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t)\n\t\t.order(\"sort_index\", { ascending: true })\n\t\t.order(\"image_id\", { ascending: true });\n\n\tif (imagesError) {\n\t\tthrow imagesError;\n\t}\n\n\treturn attachImages(baseItems, images ?? []);\n}\n\n/**\n * Group images onto their base items. `images` must already be in the order\n * callers should see them (sort_index, then image_id) — grouping preserves it,\n * so every render site can take `images[0]` as the primary image.\n */\nexport function attachImages<T extends { base_item_id: number }>(\n\tbaseItems: T[],\n\timages: BaseItemImage[],\n): Array<T & { images: BaseItemImage[] }> {\n\tconst byBaseItemId = new Map<number, BaseItemImage[]>();\n\tfor (const image of images) {\n\t\tconst group = byBaseItemId.get(image.base_item_id);\n\t\tif (group) group.push(image);\n\t\telse byBaseItemId.set(image.base_item_id, [image]);\n\t}\n\n\treturn baseItems.map((baseItem) => ({\n\t\t...baseItem,\n\t\timages: byBaseItemId.get(baseItem.base_item_id) ?? [],\n\t}));\n}\n\nexport function menuImageUrl(\n\timage?: BaseItemImage | null,\n\tabsolute = false,\n): string | null {\n\tif (!image) return null;\n\tif (image.local_key && !absolute) return `/images/items/${image.local_key}`;\n\tconst path = image.processed_path ?? image.raw_path;\n\tif (!path) return null;\n\treturn `${SUPABASE_URL}/storage/v1/object/public/product-images/${path}`;\n}\n\nasync function getItems(\n\tsupabase: SupabaseClient,\n\tbaseItems: BaseItem[],\n\texcludedIds?: number[],\n): Promise<ItemVariant[]> {\n\tif (baseItems && baseItems.length === 0) {\n\t\treturn [];\n\t}\n\n\t// the scheme rides along so the prebuilt menu carries it — no runtime fetch\n\t// on the item page just to know how a pizza can be cut\n\tlet itemsQuery = supabase\n\t\t.from(\"items\")\n\t\t.select(\"*, portion_scheme:portion_schemes(*)\")\n\t\t.in(\n\t\t\t\"base_item_id\",\n\t\t\tbaseItems.map((baseItem) => baseItem.base_item_id),\n\t\t);\n\n\tif (excludedIds && excludedIds.length > 0) {\n\t\titemsQuery = itemsQuery.not(\"item_id\", \"in\", csvFormat(excludedIds));\n\t}\n\n\tconst { data: items, error: itemsError } = await itemsQuery;\n\n\tif (itemsError) {\n\t\tthrow itemsError;\n\t}\n\n\treturn items ?? [];\n}\n\nexport async function getComponents(\n\tsupabase: SupabaseClient,\n\titems: ItemVariant[] | number[],\n\tuseBehaviors = false,\n): Promise<Component[]> {\n\tconst itemIds = items.length > 0 && typeof items[0] === \"number\"\n\t\t? (items as number[])\n\t\t: (items as ItemVariant[]).map((item) => item.item_id);\n\n\tif (itemIds.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst select = useBehaviors\n\t\t? \"components(*), item_id, servings_per_item, behavior_id, component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: \"components(*), item_id, servings_per_item\";\n\n\tconst { data: components } = await supabase\n\t\t.from(\"item_components\")\n\t\t.select(select)\n\t\t.in(\"item_id\", itemIds)\n\t\t.throwOnError();\n\n\treturn components.map((component: any) => ({\n\t\tservings_per_item: component.servings_per_item,\n\t\tis_base: component.components?.is_base,\n\t\tserving_weight: component.components?.serving_weight,\n\t\tcomponent_type_id: component.components?.component_type_id,\n\t\tid: component.components?.id,\n\t\tname: component.components?.name,\n\t\titem_id: component.item_id,\n\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t}));\n}\n\n/**\n * A rule's options — components and items — as one keyed list. The key is what\n * `portionKey` builds on and what `weightedUpcharges` prices, so the picker and\n * the server's repricing read option prices through this same function.\n */\nexport function ruleOptionDefs(rule: Rule) {\n\treturn [\n\t\t...(rule.components ?? []).map((c) => ({\n\t\t\tkey: `component:${c.component_id}`,\n\t\t\tid: c.component_id,\n\t\t\ttype: \"component\" as const,\n\t\t\tname: c.name,\n\t\t\tbasePrice: c.upcharge_price || 0,\n\t\t\tdescription: c.description,\n\t\t})),\n\t\t...(rule.items ?? []).map((i) => ({\n\t\t\tkey: `item:${i.item_id}`,\n\t\t\tid: i.item_id,\n\t\t\ttype: \"item\" as const,\n\t\t\tname: i.name,\n\t\t\tbasePrice: i.upcharge_price || 0,\n\t\t\tdescription: i.description,\n\t\t})),\n\t];\n}\n\n/**\n * Rules attach at item, base-item, or category level; `ruleIds` fetches them by\n * primary key instead, for when you already hold the ids (repricing an order's\n * selections server-side).\n */\nexport async function getRules(\n\tsupabase: SupabaseClient,\n\tbaseItems?: BaseItem[],\n\tcategories?: Category[],\n\titems?: ItemVariant[],\n\tuseBehaviors = false,\n\truleIds?: number[],\n): Promise<Rule[]> {\n\tconst itemIds = items?.map((item) => item.item_id) ?? [];\n\tconst baseItemIds = baseItems?.map((baseItem) => baseItem.base_item_id) ?? [];\n\tconst categoryIds = categories?.map((category) => category.category_id) ?? [];\n\n\tif (\n\t\titemIds.length === 0 &&\n\t\tbaseItemIds.length === 0 &&\n\t\tcategoryIds.length === 0 &&\n\t\t!ruleIds?.length\n\t) {\n\t\treturn [];\n\t}\n\n\tconst orFilter = [\n\t\t`item_id.in.(${itemIds.length > 0 ? itemIds.join(\",\") : -1})`,\n\t\t`base_item_id.in.(${baseItemIds.length > 0 ? baseItemIds.join(\",\") : -1})`,\n\t\t`category_id.in.(${categoryIds.length > 0 ? categoryIds.join(\",\") : -1})`,\n\t\t...(ruleIds?.length ? [`rule_id.in.(${ruleIds.join(\",\")})`] : []),\n\t].join(\",\");\n\n\tconst behaviorSelect = useBehaviors\n\t\t? \"component_quantity_behaviors(headcount_threshold, starting_amount, step_size, step_interval, maximum)\"\n\t\t: null;\n\n\tconst ruleComponentsSelect = [\n\t\t\"component_id, upcharge_price, components(name, description, serving_weight)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst ruleItemsSelect = [\n\t\t\"item_id, upcharge_price, items(item_id, name, description)\",\n\t\tbehaviorSelect,\n\t].filter(Boolean).join(\", \");\n\n\tconst { data: ruleData, error: rulesError } = await supabase\n\t\t.from(\"item_rules\")\n\t\t.select(\n\t\t\t`\n rule_id,\n category_id,\n item_id,\n base_item_id,\n group_name,\n min_selection,\n max_selection,\n included_quantity,\n is_portionable,\n rule_components (${ruleComponentsSelect}),\n rule_items (${ruleItemsSelect})\n `,\n\t\t)\n\t\t.or(orFilter);\n\n\tif (rulesError) {\n\t\tthrow rulesError;\n\t}\n\n\treturn (ruleData ?? []).map((rule: any) => ({\n\t\trule_id: rule.rule_id,\n\t\tgroup_name: rule.group_name,\n\t\tmin_selection: rule.min_selection,\n\t\tmax_selection: rule.max_selection,\n\t\tincluded_quantity: rule.included_quantity,\n\t\tis_portionable: rule.is_portionable,\n\t\tcategory_id: rule.category_id,\n\t\titem_id: rule.item_id,\n\t\tbase_item_id: rule.base_item_id,\n\n\t\tcomponents: (rule.rule_components ?? []).map((component: any) => ({\n\t\t\tcomponent_id: component.component_id,\n\t\t\tupcharge_price: component.upcharge_price ?? 0,\n\t\t\tname: component.components?.name ?? \"\",\n\t\t\tdescription: component.components?.description ?? undefined,\n\t\t\tserving_weight: component.components?.serving_weight ?? undefined,\n\t\t\t...(useBehaviors && { behavior: component.component_quantity_behaviors ?? undefined }),\n\t\t})),\n\n\t\titems: (rule.rule_items ?? []).map((item: any) => ({\n\t\t\titem_id: item.item_id,\n\t\t\tname: item.items?.name ?? \"\",\n\t\t\tdescription: item.items?.description ?? undefined,\n\t\t\tupcharge_price: item.upcharge_price ?? 0,\n\t\t\t...(useBehaviors && { behavior: item.component_quantity_behaviors ?? undefined }),\n\t\t})),\n\t}));\n}\n\nfunction buildMenu(\n\titems: ItemVariant[],\n\tbaseItems: BaseItem[],\n\tcategories: Category[],\n\tcomponents: Component[],\n\trules: Rule[],\n): Menu {\n\tconst menu: Menu = {\n\t\titems: [],\n\t\tcategories: [],\n\t\trules,\n\t\tcomponents,\n\t};\n\n\tmenu.items = baseItems.map((baseItem) => {\n\t\tconst categoryRules = rules.filter((rule) => rule.category_id === baseItem.category_id);\n\t\tconst baseItemRules = rules.filter((rule) => rule.base_item_id === baseItem.base_item_id);\n\t\treturn {\n\t\t\tis_catering: (baseItem as any).per_person !== null,\n\t\t\t...baseItem,\n\t\t\tvariants: items\n\t\t\t\t.filter((item) => item.base_item_id === baseItem.base_item_id)\n\t\t\t\t.map((item) => ({\n\t\t\t\t\t...item,\n\t\t\t\t\tcomponents: components.filter(\n\t\t\t\t\t\t(component) => component.item_id === item.item_id,\n\t\t\t\t\t),\n\t\t\t\t\trules: [\n\t\t\t\t\t\t...categoryRules,\n\t\t\t\t\t\t...baseItemRules,\n\t\t\t\t\t\t...rules.filter((rule) => rule.item_id === item.item_id),\n\t\t\t\t\t],\n\t\t\t\t})),\n\t\t\trules: baseItemRules,\n\t\t};\n\t});\n\n\tmenu.categories = categories.map((category) => ({\n\t\t...category,\n\t\trules: rules.filter((rule) => rule.category_id === category.category_id),\n\t}));\n\n\treturn menu;\n}\n\nexport function getBaseItemFromVariant(\n\tmenu: MenuItem[],\n\tvariant: ItemVariant,\n\tbaseItemId?: number,\n): MenuItem | undefined {\n\tif (baseItemId != null) {\n\t\treturn menu.find((i) => i.base_item_id === baseItemId);\n\t}\n\treturn menu.find((i) => i.variants.some((v) => v.item_id === variant.item_id));\n}\n\nexport function getMenuItemByVariantId(menu: MenuItem[], variantId: number): MenuItem | undefined {\n\treturn menu.find((item) =>\n\t\titem.variants?.some((v) => v.item_id === variantId)\n\t);\n}\n\nexport function filterBySchedule(\n\tpromos: Promotion[],\n\tplatform: Platforms,\n\tlocationId: number | undefined,\n\tnow: Date = new Date(),\n): Promotion[] {\n\treturn promos.filter((promo) => {\n\t\tif (promo.start_date && new Date(promo.start_date) > now) return false;\n\t\tif (promo.end_date && new Date(promo.end_date) < now) return false;\n\n\t\tif (promo.platforms && promo.platforms.length > 0) {\n\t\t\tif (!promo.platforms.includes(platform)) return false;\n\t\t}\n\n\t\tif (!promo.is_automatic) return false;\n\n\t\tif (promo.location_id != null && locationId !== undefined) {\n\t\t\tif (promo.location_id !== locationId) return false;\n\t\t} else if (promo.location_id != null && locationId === undefined) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (promo.schedule_details_json) {\n\t\t\tconst { interval, days, start_time, end_time } =\n\t\t\t\tpromo.schedule_details_json;\n\n\t\t\tif (interval === \"weekly\" && days && !days.includes(getDay(now) + 1)) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst dayStart = startOfDay(now);\n\t\t\tif (start_time && isBefore(now, parse(start_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tif (end_time && isAfter(now, parse(end_time, \"HH:mm\", dayStart))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t});\n}\n\nexport async function getActivePromotions(\n\tsupabase: SupabaseClient,\n\tfranchiseId: number,\n\tplatform: Platforms,\n\tlocationId?: number,\n): Promise<Promotion[]> {\n\tconst { data, error } = await supabase\n\t\t.from(\"promotions\")\n\t\t.select(\"*\")\n\t\t.eq(\"is_active\", true)\n\t\t.eq(\"franchise_id\", franchiseId);\n\n\tif (error) throw error;\n\n\treturn filterBySchedule((data || []) as Promotion[], platform, locationId);\n}\n","export { menuImageUrl } from \"@zorgo/universal/utils/menuUtils\";\nexport type { BaseItemImage } from \"@zorgo/universal/interfaces\";\n\n/**\n * The variant widths prebuild emits. The loader below and the encoder in\n * internal/itemImages.ts read the same list, so a requested width can never\n * snap to a rung that was never written.\n */\nexport const ITEM_IMAGE_WIDTHS = [384, 640, 1080] as const;\n\n/** Public prefix menuImageUrl returns for a localized image. */\nexport const ITEM_IMAGE_PREFIX = \"/images/items/\";\n\n/**\n * next/image custom loader (`images.loaderFile` in a scaffolded site's\n * next.config.ts). Prebuilt images get the nearest rung that covers the\n * requested width; everything else — a bucket URL for an image prebuild could\n * not localize, the logo, any site-authored asset — passes through untouched\n * and is simply served unoptimized.\n *\n * Sync and pure by contract: next/image calls it during render for every\n * srcset candidate.\n */\nexport default function itemImageLoader({\n\tsrc,\n\twidth,\n}: {\n\tsrc: string;\n\twidth: number;\n}): string {\n\tif (!src.startsWith(ITEM_IMAGE_PREFIX)) return src;\n\tconst rung =\n\t\tITEM_IMAGE_WIDTHS.find((candidate) => candidate >= width) ??\n\t\tITEM_IMAGE_WIDTHS[ITEM_IMAGE_WIDTHS.length - 1];\n\treturn `${src}-${rung}.avif`;\n}\n\nexport { itemImageLoader };\n"],"mappings":";AAAO,IAAM,eAAe;AASrB,IAAM,eACZ,QAAQ,IAAI,aAAa,eACtB,8BACA;;;AC8gBG,SAAS,aACf,OACA,WAAW,OACK;AAChB,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,MAAM,aAAa,CAAC,SAAU,QAAO,iBAAiB,MAAM,SAAS;AACzE,QAAM,OAAO,MAAM,kBAAkB,MAAM;AAC3C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,GAAG,YAAY,4CAA4C,IAAI;AACvE;;;AC3hBO,IAAM,oBAAoB,CAAC,KAAK,KAAK,IAAI;AAGzC,IAAM,oBAAoB;AAYlB,SAAR,gBAAiC;AAAA,EACvC;AAAA,EACA;AACD,GAGW;AACV,MAAI,CAAC,IAAI,WAAW,iBAAiB,EAAG,QAAO;AAC/C,QAAM,OACL,kBAAkB,KAAK,CAAC,cAAc,aAAa,KAAK,KACxD,kBAAkB,kBAAkB,SAAS,CAAC;AAC/C,SAAO,GAAG,GAAG,IAAI,IAAI;AACtB;","names":[]}
package/dist/index.d.mts CHANGED
@@ -2,8 +2,45 @@ import * as _stripe_stripe_js from '@stripe/stripe-js';
2
2
  import { StripeElementsOptions } from '@stripe/stripe-js';
3
3
  import * as React from 'react';
4
4
  import React__default, { RefObject } from 'react';
5
- import * as react_jsx_runtime from 'react/jsx-runtime';
6
5
  import { z } from 'zod';
6
+ import * as react_jsx_runtime from 'react/jsx-runtime';
7
+
8
+ /**
9
+ * How the section picker draws itself. Closed set — each value needs a renderer,
10
+ * so adding one is a code change (and an `alter type portion_geometry add value`).
11
+ *
12
+ * Section order is fixed per geometry, and consecutive indices must be spatially
13
+ * adjacent so any divisor of the division count partitions cleanly (sections 1–3
14
+ * of 6 are exactly section 1 of 2). Without that, switching a customer from
15
+ * sixths to halves has no defined answer.
16
+ *
17
+ * - `radial` — contiguous arcs from 12 o'clock, counter-clockwise
18
+ * - `grid` — column-major: down the left column, then the next column
19
+ * - `linear` — one end to the other
20
+ * - `count` — equal shares of a countable quantity, no geometry
21
+ */
22
+ declare const PortionGeometrySchema: z.ZodEnum<{
23
+ radial: "radial";
24
+ grid: "grid";
25
+ linear: "linear";
26
+ count: "count";
27
+ }>;
28
+ type PortionGeometry = z.infer<typeof PortionGeometrySchema>;
29
+ /** A row of `portion_schemes` — a franchise-owned, franchise-named menu entity. */
30
+ declare const PortionSchemeSchema: z.ZodObject<{
31
+ id: z.ZodNumber;
32
+ franchise_id: z.ZodNumber;
33
+ geometry: z.ZodEnum<{
34
+ radial: "radial";
35
+ grid: "grid";
36
+ linear: "linear";
37
+ count: "count";
38
+ }>;
39
+ name: z.ZodString;
40
+ min_divisions: z.ZodDefault<z.ZodNumber>;
41
+ max_divisions: z.ZodNumber;
42
+ }, z.core.$strip>;
43
+ type PortionScheme = z.infer<typeof PortionSchemeSchema>;
7
44
 
8
45
  type LocationStatus = "active" | "permanently_closed";
9
46
  interface Location {
@@ -53,16 +90,27 @@ interface ItemVariant {
53
90
  components?: Component[];
54
91
  base_item_id?: number;
55
92
  rules?: Rule[];
93
+ portion_scheme_id?: number | null;
94
+ portion_scheme?: PortionScheme | null;
56
95
  display_price?: number;
57
96
  active_promo_id?: number;
58
97
  }
98
+ interface BaseItemImage {
99
+ image_id: number;
100
+ base_item_id: number;
101
+ alias: string;
102
+ raw_path?: string | null;
103
+ processed_path?: string | null;
104
+ sort_index?: number;
105
+ local_key?: string | null;
106
+ }
59
107
  interface MenuItem {
60
108
  base_item_id: number;
61
109
  category_id: number;
62
110
  franchise_id: number;
63
111
  name: string;
64
112
  per_person?: boolean | null;
65
- image_path?: string | null;
113
+ images?: BaseItemImage[];
66
114
  variants: ItemVariant[];
67
115
  rules: Rule[];
68
116
  description?: string | null;
@@ -100,6 +148,7 @@ interface Rule {
100
148
  min_selection: number;
101
149
  max_selection?: number;
102
150
  included_quantity: number;
151
+ is_portionable?: boolean;
103
152
  items?: RuleItem[];
104
153
  components?: RuleComponent[];
105
154
  }
@@ -130,14 +179,25 @@ interface CustomizationOption {
130
179
  selected: boolean;
131
180
  disabled: boolean;
132
181
  toggle: () => void;
182
+ /** Section this option currently targets; null when editing the whole item. */
183
+ portion?: string | null;
184
+ /** Section indices this option already covers, for shading the picker. */
185
+ coveredSections?: number[];
186
+ /** Already applied to the whole item, so individual sections are moot. */
187
+ coveredWhole?: boolean;
133
188
  }
134
189
  interface CustomizationRule extends Rule {
135
190
  rule_id: number;
136
191
  max: number;
192
+ /** Weighted, not a row count: two halves of one topping count as 1. */
137
193
  selectedCount: number;
138
194
  satisfied: boolean;
195
+ /** "Choose 1. Select up to 5. 1 included" — the rule's limits in words. */
196
+ subtext: string;
139
197
  message: () => string | null;
140
198
  options: CustomizationOption[];
199
+ /** This rule can be applied per-section under the item's scheme. */
200
+ portionable?: boolean;
141
201
  }
142
202
  interface OrderItem {
143
203
  item_id: number;
@@ -155,6 +215,7 @@ interface OrderItem {
155
215
  components?: OrderComponent[];
156
216
  selections?: RuleSelection[];
157
217
  comments?: string;
218
+ portion_scheme?: PortionScheme | null;
158
219
  status?: "removed" | "modified";
159
220
  applied_promo_id?: number;
160
221
  discount_amount?: number;
@@ -163,6 +224,7 @@ interface OrderComponent extends Component {
163
224
  quantity_servings: number;
164
225
  upcharge?: number;
165
226
  rule_id?: number;
227
+ portion?: string | null;
166
228
  }
167
229
  interface RuleSelection {
168
230
  rule_id: number;
@@ -177,6 +239,7 @@ interface Selection {
177
239
  serving_price?: number;
178
240
  name: string;
179
241
  status?: "removed" | "modified" | "added";
242
+ portion?: string | null;
180
243
  component_type_id?: number;
181
244
  serving_weight?: number;
182
245
  measurement_type?: "weight" | "count";
@@ -383,7 +446,6 @@ declare function useMenu(): {
383
446
  items: MenuItem[];
384
447
  allItems: MenuItem[];
385
448
  categories: Category[];
386
- getItemImageUrl: (imagePath?: string | null) => string | null;
387
449
  };
388
450
  /**
389
451
  * Seeds a store with prebuilt data (see the app's prebuild script) during the
@@ -432,11 +494,27 @@ declare function useItemCustomization(baseItemId: number, seed?: OrderItem, edit
432
494
  unmetRule: CustomizationRule | null;
433
495
  canSubmit: boolean;
434
496
  submit: () => OrderItem | null;
497
+ portionScheme: {
498
+ id: number;
499
+ franchise_id: number;
500
+ geometry: "radial" | "grid" | "linear" | "count";
501
+ name: string;
502
+ min_divisions: number;
503
+ max_divisions: number;
504
+ } | null;
505
+ placeOption: (ruleId: number, baseKey: string, portion: string | null, max?: number) => void;
506
+ clearOption: (ruleId: number, baseKey: string) => void;
507
+ recutSelections: (next: number) => void;
435
508
  quantity: number;
436
- setQuantity: React.Dispatch<React.SetStateAction<number>>;
509
+ setQuantity: (next: number | ((current: number) => number)) => void;
437
510
  comments: string;
438
511
  setComments: React.Dispatch<React.SetStateAction<string>>;
439
- setComponentServings: (componentId: number, qty: number) => void;
512
+ /**
513
+ * Set a component's servings for the whole item, or for one section when
514
+ * `portion` is given. 0 removes it — "no cheese", or "no cheese on the
515
+ * left half".
516
+ */
517
+ setComponentServings: (componentId: number, qty: number, portion?: string | null) => void;
440
518
  price: number;
441
519
  salePrice: number | undefined;
442
520
  isEditMode: boolean;
@@ -1105,4 +1183,95 @@ declare function useFormValues(): Record<string, unknown>;
1105
1183
 
1106
1184
  declare function useFormValidity(): boolean;
1107
1185
 
1108
- export { type Category, CheckboxField, ClickableField, ConditionalRenderer, type Customer, type CustomizationOption, type CustomizationRule, DateField, DateTime, DateTimeRange, DisclaimerText, DiscreteNumberField, DynamicFormShell, EmailField, FieldError, FieldLabel, GuestCountField, Header, IncrementField, type ItemVariant, type Location, LocationField, type Menu, type MenuItem, NameFields, type Order, type OrderComponent, type OrderItem, PhoneField, Platforms, PerPersonSummary as PricePreview, type Promotion, type RecommendationRequest, type RecommendationResult, type RecommendationStrategy, type Rule, type RuleComponent, type RuleItem, type RuleSelection, SectionLabel, type Selection, SliderField, SubmitButton, TextField, type TimeConstraints, TimeField, setZorgoBoot, setZorgoPathnameHook, useCart, useCateringConfig, useCustomer, useField, useFormStatus, useFormValidity, useFormValues, useHydrateMenu, useHydrateStore, useItemCustomization, useLocation, useMenu, useModal, usePayment, usePromotions, useRecommendations, useRegisterField };
1186
+ declare function parsePortion(portion: string): {
1187
+ index: number;
1188
+ divisions: number;
1189
+ };
1190
+ /** A section's share of the whole. No portion = the whole item = 1. */
1191
+ declare function portionWeight(portion?: string | null): number;
1192
+ /**
1193
+ * Selection keys carry their portion as a suffix: `component:123@1/2`. A bare
1194
+ * key is the whole item, which is what every pre-portion key already is — so
1195
+ * existing state parses unchanged.
1196
+ */
1197
+ declare function portionKey(baseKey: string, portion?: string | null): string;
1198
+ declare function parsePortionKey(key: string): {
1199
+ baseKey: string;
1200
+ portion: string | null;
1201
+ };
1202
+ /**
1203
+ * A section's share of a whole-item upcharge, in cents. Rounds up, so covering
1204
+ * every section can cost a cent or two more than the whole — which is why a
1205
+ * selection covering everything collapses to a single portion-less row instead.
1206
+ */
1207
+ declare function splitUpcharge(cents: number, divisions: number): number;
1208
+ /** Every division count a scheme allows, for the picker's cut selector. */
1209
+ declare function divisionsFor(scheme: Pick<PortionScheme, "min_divisions" | "max_divisions">): number[];
1210
+ /**
1211
+ * The one place a section index becomes words. The picker, the cart line, the
1212
+ * receipt and the KDS ticket all call this on the same scheme, so the index is
1213
+ * the only token the UI and the data share.
1214
+ */
1215
+ declare function portionLabel(portion: string | null | undefined, scheme: Pick<PortionScheme, "geometry">): string;
1216
+ /**
1217
+ * Expand per-section overrides on one keyed entity into one row per section —
1218
+ * how "no cheese on the left half" becomes a 0-serving row for that section and
1219
+ * a normal row for the rest. No overrides, or the same value on every section,
1220
+ * is a single whole-item row (§3 collapse).
1221
+ */
1222
+ declare function portionRows(baseKey: string, values: Record<string, number>, fallback: number): {
1223
+ portion: string | null;
1224
+ value: number;
1225
+ }[];
1226
+ /** Weighted count of a rule's selections: two halves of one topping count as 1. */
1227
+ declare function selectionWeight(keys: string[]): number;
1228
+ /**
1229
+ * Spend `included_quantity` on selection keys in pick order. A topping is a
1230
+ * topping: it takes one included slot no matter how little of the pizza it covers,
1231
+ * so putting pepperoni on one half never leaves half a free slot behind to
1232
+ * discount everything else — and its rows share that one slot, so two halves of
1233
+ * pepperoni consume one included topping, not two. With no portions this is
1234
+ * exactly the old positional rule (`position < included ? 0 : basePrice`).
1235
+ */
1236
+ declare function weightedUpcharges(keys: string[], basePriceOf: (baseKey: string) => number, included: number): Map<string, number>;
1237
+ /**
1238
+ * Re-express a portion under a different cut, for when the customer switches
1239
+ * from halves to quarters mid-order. Returns null when neither count divides the
1240
+ * other (halves → thirds), meaning the selection can't be carried over.
1241
+ */
1242
+ declare function remapPortion(portion: string, nextDivisions: number): string[] | null;
1243
+ /**
1244
+ * Positional section wording, geometry-free — deliberately not `portionLabel`.
1245
+ * Derived words ("Left", "Top left") only exist for a few geometry/division
1246
+ * pairs and need the scheme; a receipt has to say something for every cut.
1247
+ */
1248
+ declare function sectionLabel(portion?: string | null): string;
1249
+ /** A text glyph for a section, or null when unicode has no character for it. */
1250
+ declare function portionGlyph(portion: string | null | undefined, geometry?: PortionGeometry | null): string | null;
1251
+ interface PortionGroup {
1252
+ portion: string | null;
1253
+ label: string;
1254
+ glyph: string | null;
1255
+ /** The item's rules, each carrying only the selections that land on this section. */
1256
+ selections: RuleSelection[];
1257
+ removals: OrderComponent[];
1258
+ /**
1259
+ * Components present on this section. Whole-item components are the base
1260
+ * recipe — implied by the item's name — so only portioned ones land here,
1261
+ * where "pepperoni on the left" is the whole point of the line.
1262
+ */
1263
+ additions: OrderComponent[];
1264
+ }
1265
+ /**
1266
+ * One group per covered section: the whole-item group first, then sections in
1267
+ * index order, rules keeping their original order inside each. An item with no
1268
+ * portions anywhere yields exactly one whole-item group, so a surface can render
1269
+ * this unconditionally and show the heading only when `portion_scheme` is set.
1270
+ *
1271
+ * It groups and nothing else — every selection it is handed comes back out. What
1272
+ * counts as worth printing is the surface's call, not this function's.
1273
+ */
1274
+ declare function groupItemPortions(item: Pick<OrderItem, "selections" | "components" | "portion_scheme">): PortionGroup[];
1275
+ declare function radialSectionPath(index: number, divisions: number, r?: number): string;
1276
+
1277
+ export { type Category, CheckboxField, ClickableField, ConditionalRenderer, type Customer, type CustomizationOption, type CustomizationRule, DateField, DateTime, DateTimeRange, DisclaimerText, DiscreteNumberField, DynamicFormShell, EmailField, FieldError, FieldLabel, GuestCountField, Header, IncrementField, type ItemVariant, type Location, LocationField, type Menu, type MenuItem, NameFields, type Order, type OrderComponent, type OrderItem, PhoneField, Platforms, type PortionGroup, PerPersonSummary as PricePreview, type Promotion, type RecommendationRequest, type RecommendationResult, type RecommendationStrategy, type Rule, type RuleComponent, type RuleItem, type RuleSelection, SectionLabel, type Selection, SliderField, SubmitButton, TextField, type TimeConstraints, TimeField, divisionsFor, groupItemPortions, parsePortion, parsePortionKey, portionGlyph, portionKey, portionLabel, portionRows, portionWeight, radialSectionPath, remapPortion, sectionLabel, selectionWeight, setZorgoBoot, setZorgoPathnameHook, splitUpcharge, useCart, useCateringConfig, useCustomer, useField, useFormStatus, useFormValidity, useFormValues, useHydrateMenu, useHydrateStore, useItemCustomization, useLocation, useMenu, useModal, usePayment, usePromotions, useRecommendations, useRegisterField, weightedUpcharges };