@feedmepos/mf-order-setting 0.0.84 → 0.0.85

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 (109) hide show
  1. package/dist/DisplaySystemView-hmlT9gsb.js +6485 -0
  2. package/dist/{KioskDevicesView-DdwFuenU.js → KioskDevicesView-fW6M9Hao.js} +1 -1
  3. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-CxtLm_oh.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-GrsnYo80.js} +3 -3
  4. package/dist/KioskSettingView-BdEPftHH.js +839 -0
  5. package/dist/{KioskView-fwBwUGCQ.js → KioskView-Bwk9S0tc.js} +82 -70
  6. package/dist/{OrderSettingsView-RhlyZmzs.js → OrderSettingsView-DVXfVdXj.js} +30858 -30699
  7. package/dist/{app-ClOkRxOZ.js → app-BnrruYco.js} +74 -12
  8. package/dist/app.js +1 -1
  9. package/dist/display-preview/assets/BandBoard-DIcfXsNV.js +1 -0
  10. package/dist/display-preview/assets/CarouselBlock-XAhTyEWB.js +1 -0
  11. package/dist/display-preview/assets/{CategoryBoard-DClMUGfS.js → CategoryBoard-DBKe18hv.js} +1 -1
  12. package/dist/display-preview/assets/ImageBlock-BCbnYSHq.js +1 -0
  13. package/dist/display-preview/assets/PickupMode-Df_TPCh8.js +1 -0
  14. package/dist/display-preview/assets/{StatusBoard-BVESD28J.js → StatusBoard-28z3bPOf.js} +1 -1
  15. package/dist/display-preview/assets/TextBlock-CvJKITtx.js +1 -0
  16. package/dist/display-preview/assets/VideoBlock-Dop43wDv.js +1 -0
  17. package/dist/display-preview/assets/index-C6DET5zJ.css +1 -0
  18. package/dist/display-preview/assets/index-cqMoOpSN.js +2 -0
  19. package/dist/display-preview/assets/pickupView-DRMVXqrc.js +1 -0
  20. package/dist/display-preview/index.html +2 -2
  21. package/dist/frontend/mf-order/src/app.d.ts +62 -0
  22. package/dist/frontend/mf-order/src/main.d.ts +62 -0
  23. package/dist/frontend/mf-order/src/modules/display-system/interface.d.ts +21 -0
  24. package/dist/frontend/mf-order/src/modules/order-setting/kiosk/coverCarousel.d.ts +69 -0
  25. package/dist/frontend/mf-order/src/modules/order-setting/kiosk/interface.d.ts +5 -1
  26. package/dist/frontend/mf-order/src/stores/order-setting/index.d.ts +90 -3
  27. package/dist/frontend/mf-order/src/views/display-system/DisplayFieldControl.vue.d.ts +3 -0
  28. package/dist/frontend/mf-order/src/views/display-system/DisplayInspector.vue.d.ts +3 -13
  29. package/dist/frontend/mf-order/src/views/order-settings/auto-send-kitchen.d.ts +58 -0
  30. package/dist/frontend/mf-order/src/views/order-settings/delivery/inhouse/InHouseDelivery.vue.d.ts +16 -1
  31. package/dist/frontend/mf-order/src/views/order-settings/pickup/PickUpSettingDialogContent.vue.d.ts +16 -1
  32. package/dist/frontend/mf-order/src/views/order-settings/useAutoSendGuard.d.ts +48 -0
  33. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  34. package/dist/{index-CKEbr9Es.js → index-D5wABuUx.js} +1 -1
  35. package/dist/{index-5k7kGCpJ.js → index-D7IWjZdy.js} +2 -2
  36. package/dist/{index-I2YGevce.js → index-LyvBqKnv.js} +1 -1
  37. package/dist/{order.dto-Dbn-hRrm.js → order.dto-D7HGiueX.js} +3579 -3428
  38. package/dist/package/entity/alert/investigation.dto.d.ts +25 -6
  39. package/dist/package/entity/alert/lark-callback.dto.d.ts +31 -12
  40. package/dist/package/entity/alert/suggestion.dto.d.ts +4 -4
  41. package/dist/package/entity/display/blocks.d.ts +42 -76
  42. package/dist/package/entity/incoming-order/incoming-order-to-bill.dto.d.ts +47 -36
  43. package/dist/package/entity/incoming-order/incoming-order.dto.d.ts +1 -0
  44. package/dist/package/entity/index.d.ts +2 -0
  45. package/dist/package/entity/index.frontend.d.ts +2 -0
  46. package/dist/package/entity/order/effects/effect.dto.d.ts +266 -0
  47. package/dist/package/entity/order/order-item/order-item.dto.d.ts +36 -36
  48. package/dist/package/entity/order/order.do.d.ts +7 -64
  49. package/dist/package/entity/order/order.dto.d.ts +13006 -795
  50. package/dist/package/entity/order-platform/grabfood/grabfood-settings.do.d.ts +13 -0
  51. package/dist/package/entity/order-platform/grabfood/grabfood.dto.d.ts +3 -0
  52. package/dist/package/entity/order-platform/menu.dto.d.ts +10 -10
  53. package/dist/package/entity/order-platform/shopeefood/shopeefood-order.dto.d.ts +11 -8
  54. package/dist/package/entity/order-setting/auto-accept.dto.d.ts +94 -0
  55. package/dist/package/entity/order-setting/auto-send.resolve.d.ts +110 -0
  56. package/dist/package/entity/order-setting/delivery/inhouse-delivery.do.d.ts +3 -0
  57. package/dist/package/entity/order-setting/delivery/inhouse-delivery.dto.d.ts +3 -0
  58. package/dist/package/entity/order-setting/dine-in/dine-in.do.d.ts +3 -0
  59. package/dist/package/entity/order-setting/dine-in/dine-in.dto.d.ts +3 -0
  60. package/dist/package/entity/order-setting/kiosk/kiosk.do.d.ts +26 -0
  61. package/dist/package/entity/order-setting/kiosk/kiosk.dto.d.ts +39 -0
  62. package/dist/package/entity/order-setting/order-setting.do.d.ts +57 -4
  63. package/dist/package/entity/order-setting/order-setting.dto.d.ts +259 -9
  64. package/dist/package/entity/order-setting/pickup/pickup.do.d.ts +3 -0
  65. package/dist/package/entity/order-setting/pickup/pickup.dto.d.ts +3 -0
  66. package/dist/package/entity/order-setting/queue/queue.do.d.ts +2 -2
  67. package/dist/package/entity/order-setting/queue/queue.dto.d.ts +2 -2
  68. package/dist/package/entity/queue/queue.dto.d.ts +48 -48
  69. package/dist/package/entity/restaurant/restaurant.dto.d.ts +46 -8
  70. package/dist/package/entity/websocket/websocket.dto.d.ts +88 -0
  71. package/dist/style.css +1 -1
  72. package/package.json +2 -2
  73. package/src/locales/en-US.json +22 -2
  74. package/src/locales/ja-JP.json +12 -1
  75. package/src/locales/th-TH.json +12 -1
  76. package/src/locales/zh-CN.json +22 -2
  77. package/src/modules/display-system/interface.spec.ts +32 -18
  78. package/src/modules/display-system/interface.ts +32 -0
  79. package/src/modules/order-setting/kiosk/coverCarousel.spec.ts +172 -0
  80. package/src/modules/order-setting/kiosk/coverCarousel.ts +186 -0
  81. package/src/modules/order-setting/kiosk/interface.ts +138 -127
  82. package/src/stores/order-setting/mapper.spec.ts +26 -0
  83. package/src/stores/order-setting/mapper.ts +305 -293
  84. package/src/views/display-system/DisplayFieldControl.vue +46 -0
  85. package/src/views/display-system/DisplayInspector.vue +8 -3
  86. package/src/views/display-system/DisplaySystemView.vue +7 -3
  87. package/src/views/display-system/DisplayTemplateEditor.vue +28 -2
  88. package/src/views/kiosk/settings/KioskSettingView.vue +176 -60
  89. package/src/views/order-settings/auto-send-kitchen.spec.ts +137 -0
  90. package/src/views/order-settings/auto-send-kitchen.ts +92 -0
  91. package/src/views/order-settings/auto-send-resolve.spec.ts +123 -0
  92. package/src/views/order-settings/delivery/DeliverySetting.vue +10 -0
  93. package/src/views/order-settings/delivery/inhouse/InHouseDelivery.vue +40 -5
  94. package/src/views/order-settings/dine-in/DineInSetting.vue +38 -5
  95. package/src/views/order-settings/pickup/PickUpSetting.vue +1 -0
  96. package/src/views/order-settings/pickup/PickUpSettingDialogContent.vue +38 -5
  97. package/src/views/order-settings/queue/QueueSetting.vue +6 -9
  98. package/src/views/order-settings/useAutoSendGuard.ts +142 -0
  99. package/tsconfig.app.json +10 -1
  100. package/dist/DisplaySystemView-4Bz8p26A.js +0 -5892
  101. package/dist/KioskSettingView-D57KtQ-t.js +0 -711
  102. package/dist/display-preview/assets/BandBoard-BrDTYDc9.js +0 -1
  103. package/dist/display-preview/assets/CarouselBlock-CrkGxoGk.js +0 -1
  104. package/dist/display-preview/assets/ImageBlock-DtgfHd5G.js +0 -1
  105. package/dist/display-preview/assets/PickupMode-DKmqGlG8.js +0 -1
  106. package/dist/display-preview/assets/TextBlock-gAfeCCmE.js +0 -1
  107. package/dist/display-preview/assets/index-BB0ReVXy.js +0 -2
  108. package/dist/display-preview/assets/index-BWVpFSIw.css +0 -1
  109. package/dist/display-preview/assets/pickupView-C-BGcOu8.js +0 -1
@@ -0,0 +1,92 @@
1
+ import type { OrderSettingChannelAutoAccept } from '@entity'
2
+ import { resolveAutoAccept } from '@entity'
3
+
4
+ /**
5
+ * Portal-side helpers for the per-channel "Auto Send Kitchen" control.
6
+ *
7
+ * The RESOLUTION itself is not here — it lives in
8
+ * `@entity/order-setting/auto-send.resolve`, shared with the backend stamper so
9
+ * a settings page and the order it produces can never disagree. What is left
10
+ * here is presentation: select mapping, label keys, and when to raise the
11
+ * confirmation dialog.
12
+ *
13
+ * The dialog is now a teaching moment, not a safety mechanism. The backend
14
+ * enforces "a bill is only closed automatically if the kitchen was notified" at
15
+ * the point of resolution, which covers the paths no page can reach — old
16
+ * portal bundles, direct API writes, and pairs assembled ACROSS pages (the
17
+ * Queue page owns queue's auto-send while Dine In owns its auto-close). The
18
+ * dialog survives because offering "turn on both" is a better answer than
19
+ * letting a merchant save something inert.
20
+ */
21
+
22
+ /** Tri-state select value. 'null' = unset, i.e. follow the channel's auto-accept. */
23
+ export type AutoSendSelectValue = 'null' | 'true' | 'false'
24
+
25
+ export function autoSendSelectValue(
26
+ autoSendKitchen: boolean | null | undefined
27
+ ): AutoSendSelectValue {
28
+ return autoSendKitchen === null || autoSendKitchen === undefined
29
+ ? 'null'
30
+ : (String(autoSendKitchen) as 'true' | 'false')
31
+ }
32
+
33
+ export function autoSendFromSelectValue(v: string): boolean | null {
34
+ return v === 'null' ? null : v === 'true'
35
+ }
36
+
37
+ /**
38
+ * True when turning auto-close on would have NO effect, so the merchant should
39
+ * be offered kitchen-send alongside it.
40
+ *
41
+ * Evaluated for a PAID order deliberately. An unpaid bill never auto-closes
42
+ * anyway (`closeBill` no-ops while `balance > 0`), so the only question worth
43
+ * asking is whether the setting does anything for the orders that CAN close.
44
+ * That keeps paid-only channels dialog-free — their paid orders do reach the
45
+ * kitchen — while still asking on a channel whose auto-accept is off entirely.
46
+ *
47
+ * An EXPLICIT choice decides on its own, before the channel is consulted. When
48
+ * the merchant has picked Enabled or Disabled (or dine-in has, for a channel
49
+ * that inherits), `resolveAutoSend` never reaches the auto-accept fallback, so
50
+ * the answer is fully determined and reading the channel would only be a chance
51
+ * to get it wrong. Checking the channel first is precisely the regression this
52
+ * guard shipped with: a stored `Disabled` plus an unreadable channel produced
53
+ * no dialog, no warning sublabel, and no reverse-clear — the merchant saved the
54
+ * inert pair with no feedback anywhere.
55
+ *
56
+ * Only a genuinely unresolved value falls through to the channel, and an
57
+ * unknown channel then fails OPEN: the backend invariant is the safety net now,
58
+ * so a page that cannot read the channel must not nag on a guess.
59
+ *
60
+ * `inheritFrom` carries dine-in's value for the channels that inherit it
61
+ * (queue, kiosk); omit it for channels that own their setting.
62
+ */
63
+ export function requiresAutoSendConfirmation(
64
+ autoSendKitchen: boolean | null | undefined,
65
+ channel: OrderSettingChannelAutoAccept | null | undefined,
66
+ inheritFrom?: boolean | null
67
+ ): boolean {
68
+ const explicit = autoSendKitchen ?? inheritFrom
69
+ if (explicit !== null && explicit !== undefined) return !explicit
70
+ const autoAccept = resolveAutoAccept(channel, true)
71
+ if (autoAccept === null) return false
72
+ return !autoAccept
73
+ }
74
+
75
+ /**
76
+ * i18n key for the auto-close sublabel.
77
+ *
78
+ * A merchant can arrive already in the broken state — auto-close stored on,
79
+ * kitchen send resolving false — without ever touching a control, so the
80
+ * confirmation dialog never fires for them. This row is the only thing that
81
+ * tells them, which is why it reads the STORED pair rather than a change.
82
+ */
83
+ export function autoCloseSublabelKey(
84
+ autoSendKitchen: boolean | null | undefined,
85
+ channel: OrderSettingChannelAutoAccept | null | undefined,
86
+ autoCloseBill: boolean | null | undefined,
87
+ inheritFrom?: boolean | null
88
+ ): string {
89
+ return autoCloseBill && requiresAutoSendConfirmation(autoSendKitchen, channel, inheritFrom)
90
+ ? 'order.autoCloseBillRequiresAutoSend'
91
+ : 'order.autoCloseBillSublabel'
92
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { OrderSettingChannelAutoAccept } from '@entity'
3
+ import { resolveAutoAccept, resolveAutoCloseBill, resolveAutoSend } from '@entity'
4
+
5
+ /**
6
+ * The resolver is the ONLY definition of what a channel does to an order, and
7
+ * every producer of an IncomingOrder config calls it, so these are the rules the
8
+ * stamped config is held to.
9
+ *
10
+ * Ground truth for the auto-accept column is `isAutoAcceptable`
11
+ * (backend/libs/incoming-order/incoming-order.service.ts): paid-only returns
12
+ * `hasSucceedPayment(order)`, so an UNPAID order on a paid-only channel is
13
+ * accepted by hand, not automatically.
14
+ */
15
+ const OFF: OrderSettingChannelAutoAccept = { autoAccept: false, autoAcceptPaidOnly: false }
16
+ const ON: OrderSettingChannelAutoAccept = { autoAccept: true, autoAcceptPaidOnly: false }
17
+ const PAID_ONLY: OrderSettingChannelAutoAccept = { autoAccept: true, autoAcceptPaidOnly: true }
18
+
19
+ describe('resolveAutoAccept', () => {
20
+ it('is unknown, never false, when the channel could not be read', () => {
21
+ // Failing OPEN matters: a restaurant whose CouchDB documents do not exist
22
+ // yet must not have its settings page claim auto-accept is off.
23
+ expect(resolveAutoAccept(null, true)).toBeNull()
24
+ expect(resolveAutoAccept(undefined, false)).toBeNull()
25
+ })
26
+
27
+ it('ignores payment unless the channel is paid-only', () => {
28
+ expect(resolveAutoAccept(OFF, true)).toBe(false)
29
+ expect(resolveAutoAccept(OFF, false)).toBe(false)
30
+ expect(resolveAutoAccept(ON, true)).toBe(true)
31
+ expect(resolveAutoAccept(ON, false)).toBe(true)
32
+ })
33
+
34
+ it('makes paid-only a per-ORDER verdict', () => {
35
+ expect(resolveAutoAccept(PAID_ONLY, true)).toBe(true)
36
+ expect(resolveAutoAccept(PAID_ONLY, false)).toBe(false)
37
+ })
38
+
39
+ it('ignores a stranded paidOnly when auto-accept is off', () => {
40
+ // `{autoAccept: false, autoAcceptPaidOnly: true}` is a real stored shape —
41
+ // the two POS checkboxes project one pair, so turning paid-only on and then
42
+ // auto-accept off leaves paidOnly behind. `isAutoAcceptable` gates on
43
+ // `?.autoAccept` before it ever reads paidOnly and falls through to
44
+ // `return false`, so the stranded flag is dead there; this must agree, or
45
+ // the preview would promise auto-accept for prepaid orders that get none.
46
+ const STRANDED = { autoAccept: false, autoAcceptPaidOnly: true }
47
+ expect(resolveAutoAccept(STRANDED, true)).toBe(false)
48
+ expect(resolveAutoAccept(STRANDED, false)).toBe(false)
49
+ // i.e. indistinguishable from plain "off" for every order.
50
+ expect(resolveAutoAccept(STRANDED, true)).toBe(resolveAutoAccept(OFF, true))
51
+ expect(resolveAutoAccept(STRANDED, false)).toBe(resolveAutoAccept(OFF, false))
52
+ })
53
+ })
54
+
55
+ describe('resolveAutoSend', () => {
56
+ it('falls back to auto-accept when unset — the legacy binding, unchanged', () => {
57
+ expect(resolveAutoSend(null, true)).toBe(true)
58
+ expect(resolveAutoSend(undefined, false)).toBe(false)
59
+ })
60
+
61
+ it('lets an explicit false WIN over a channel that auto-accepts', () => {
62
+ // `||` here would silently discard the merchant's choice; `??` is load-bearing.
63
+ expect(resolveAutoSend(false, true)).toBe(false)
64
+ })
65
+
66
+ it('lets an explicit true decouple the kitchen from manual accept', () => {
67
+ // The workflow this branch exists to enable: a human accepts each order,
68
+ // the kitchen still fires automatically.
69
+ expect(resolveAutoSend(true, false)).toBe(true)
70
+ })
71
+
72
+ it('consults inheritFrom before auto-accept, and only when unset', () => {
73
+ expect(resolveAutoSend(null, true, false)).toBe(false)
74
+ expect(resolveAutoSend(null, false, true)).toBe(true)
75
+ expect(resolveAutoSend(false, true, true)).toBe(false)
76
+ expect(resolveAutoSend(true, false, false)).toBe(true)
77
+ // A null inheritFrom must fall through rather than pin anything.
78
+ expect(resolveAutoSend(null, true, null)).toBe(true)
79
+ })
80
+ })
81
+
82
+ describe('resolveAutoCloseBill — the invariant', () => {
83
+ it('never closes a bill the kitchen was not told about', () => {
84
+ expect(resolveAutoCloseBill(true, false)).toBe(false)
85
+ })
86
+
87
+ it('leaves every coherent combination alone', () => {
88
+ expect(resolveAutoCloseBill(true, true)).toBe(true)
89
+ expect(resolveAutoCloseBill(false, true)).toBe(false)
90
+ expect(resolveAutoCloseBill(null, true)).toBe(false)
91
+ expect(resolveAutoCloseBill(undefined, false)).toBe(false)
92
+ })
93
+ })
94
+
95
+
96
+ describe('the reported bug and its fix, end to end through the chain', () => {
97
+ it('suppresses the close that used to happen with the kitchen never notified', () => {
98
+ // Auto-accept off, merchant turned auto-close on: the exact merchant report.
99
+ // Before the invariant this stamped `autoCloseBill: true` and POS closed the
100
+ // paid bill without ever sending it.
101
+ const autoAccept = resolveAutoAccept(OFF, true) as boolean
102
+ const autoSend = resolveAutoSend(null, autoAccept)
103
+ expect(autoSend).toBe(false)
104
+ expect(resolveAutoCloseBill(true, autoSend)).toBe(false)
105
+ })
106
+
107
+ it('gives the merchant the workflow they asked for once kitchen send is on', () => {
108
+ // Manual accept, auto send, auto complete.
109
+ const autoAccept = resolveAutoAccept(OFF, true) as boolean
110
+ const autoSend = resolveAutoSend(true, autoAccept)
111
+ expect(autoAccept).toBe(false)
112
+ expect(autoSend).toBe(true)
113
+ expect(resolveAutoCloseBill(true, autoSend)).toBe(true)
114
+ })
115
+
116
+ it('closes the queue hole: an explicit queue false cannot outlive the invariant', () => {
117
+ // queueSettings.autoSendKitchen = false beats the pinned dine-in true, which
118
+ // is how the Queue page reopened the bug — but the close is suppressed now.
119
+ const autoSend = resolveAutoSend(false, true, true)
120
+ expect(autoSend).toBe(false)
121
+ expect(resolveAutoCloseBill(true, autoSend)).toBe(false)
122
+ })
123
+ })
@@ -105,6 +105,7 @@
105
105
  :initial-value="inHouseComponentProps.initialValue"
106
106
  :address="formatAddress((currentRestaurant as any).profile.address)"
107
107
  :order-inhouse-delivery-settings="orderInhouseDeliverySettings"
108
+ :channel-auto-accept="orderSetting?.channelAutoAccept?.inhouseDelivery ?? null"
108
109
  @update:model-value="handleModelValueUpdateInHouse"
109
110
  @update:order-settings="handleOrderInhouseDeliverySettingsUpdate"
110
111
  />
@@ -487,6 +488,15 @@ async function updateFeedmeExpress({ key, setting }: IntegratedDeliveryRow) {
487
488
  async function updateInHouse({ key, setting }: IntegratedDeliveryRow) {
488
489
  clearComponentProps()
489
490
 
491
+ // Drop the previous restaurant's settings BEFORE mounting the side sheet.
492
+ // The mount below is synchronous while the GET at the end of this function is
493
+ // not, so a stale value here would be read by `InHouseDelivery`'s auto-send
494
+ // guard — which reads `channelAutoAccept` — and it would decide whether to
495
+ // raise the auto-close dialog against the restaurant the merchant just
496
+ // navigated away from. Undefined means "unknown", which fails open and claims
497
+ // nothing until the real data lands.
498
+ orderSetting.value = undefined
499
+
490
500
  const inhouseSetting = setting as FdoRestaurantInHouseDelivery
491
501
 
492
502
  inHouseComponentProps.value = {
@@ -9,7 +9,8 @@ import type {
9
9
  FdoOfflinePaymentMethod,
10
10
  FdoRestaurantInHouseDelivery
11
11
  } from '@feedmepos/core/entity'
12
- import type { OrderInhouseDeliverySettingsDto } from '@entity'
12
+ import type { OrderInhouseDeliverySettingsDto, OrderSettingChannelAutoAccept } from '@entity'
13
+ import { useAutoSendGuard } from '../../useAutoSendGuard'
13
14
  import { F_FEATURE } from '@feedmepos/core/entity'
14
15
  import { FdoProductTaxSetting } from '@feedmepos/entity'
15
16
  import { computed, reactive, watch, type PropType } from 'vue'
@@ -81,12 +82,33 @@ const props = defineProps({
81
82
  orderInhouseDeliverySettings: {
82
83
  type: Object as PropType<OrderInhouseDeliverySettingsDto>,
83
84
  default: () => ({})
85
+ },
86
+ // Read-only projection of `setting.inHouseDelivery`'s auto-accept pair,
87
+ // injected on the order-settings read. null = the backend could not resolve
88
+ // it; the auto-close guard then fails OPEN rather than disabling on a guess.
89
+ channelAutoAccept: {
90
+ type: Object as PropType<OrderSettingChannelAutoAccept | null>,
91
+ default: null
84
92
  }
85
93
  })
86
94
 
87
- function updateAutoCloseBill(autoCloseBill: boolean) {
88
- emits('update:order-settings', { ...props.orderInhouseDeliverySettings, autoCloseBill })
89
- }
95
+ // Select + auto-close coupling live in one shared implementation; this view only
96
+ // says how to PERSIST a change. `null` stays unset so the backend keeps
97
+ // inheriting `setting.inHouseDelivery`'s auto-accept — `false` would pin it.
98
+ const {
99
+ autoSendKitchenValue,
100
+ selectItems: autoSendSelectItems,
101
+ autoCloseSublabel,
102
+ autoCloseSwitchKey,
103
+ updateAutoSendKitchen,
104
+ updateAutoCloseBill
105
+ } = useAutoSendGuard({
106
+ channel: () => props.channelAutoAccept,
107
+ autoSendKitchen: () => props.orderInhouseDeliverySettings.autoSendKitchen,
108
+ autoCloseBill: () => props.orderInhouseDeliverySettings.autoCloseBill,
109
+ apply: (patch) =>
110
+ emits('update:order-settings', { ...props.orderInhouseDeliverySettings, ...patch })
111
+ })
90
112
 
91
113
  function updatePrintReceiptAfterClose(printReceiptAfterClose: boolean) {
92
114
  emits('update:order-settings', { ...props.orderInhouseDeliverySettings, printReceiptAfterClose })
@@ -269,12 +291,25 @@ const actualDeliveryTime = computed({
269
291
  />
270
292
  </div>
271
293
 
294
+ <div class="mb-5 flex flex-col gap-2">
295
+ <span class="fm-typo-en-body-md-600">{{ t('order.autoSendKitchen') }}</span>
296
+ <span class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
297
+ {{ t('order.autoSendKitchenChannelSublabel') }}
298
+ </span>
299
+ <FmSelect
300
+ :model-value="autoSendKitchenValue"
301
+ :items="autoSendSelectItems"
302
+ @update:model-value="updateAutoSendKitchen"
303
+ />
304
+ </div>
305
+
272
306
  <div class="mb-5">
273
307
  <FmSwitch
308
+ :key="autoCloseSwitchKey"
274
309
  :model-value="orderInhouseDeliverySettings.autoCloseBill ?? false"
275
310
  value="autoCloseBill"
276
311
  :label="t('order.autoCloseBill')"
277
- :sublabel="t('order.autoCloseBillSublabel')"
312
+ :sublabel="autoCloseSublabel"
278
313
  label-placement="right"
279
314
  @update:model-value="updateAutoCloseBill"
280
315
  />
@@ -115,10 +115,23 @@
115
115
  @update:model-value="updateOverridePax"
116
116
  ></FmSwitch>
117
117
 
118
+ <div class="flex flex-col gap-2">
119
+ <span class="fm-typo-en-body-md-600">{{ t('order.autoSendKitchen') }}</span>
120
+ <span class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
121
+ {{ t('order.autoSendKitchenChannelSublabel') }}
122
+ </span>
123
+ <FmSelect
124
+ :model-value="autoSendKitchenValue"
125
+ :items="autoSendSelectItems"
126
+ @update:model-value="updateAutoSendKitchen"
127
+ />
128
+ </div>
129
+
118
130
  <FmSwitch
131
+ :key="autoCloseSwitchKey"
119
132
  :model-value="orderSetting?.dineInSettings?.autoCloseBill ?? false"
120
133
  :label="t('order.autoCloseBill')"
121
- :sublabel="t('order.autoCloseBillSublabel')"
134
+ :sublabel="autoCloseSublabel"
122
135
  label-placement="right"
123
136
  @update:model-value="updateAutoCloseBill"
124
137
  ></FmSwitch>
@@ -173,6 +186,7 @@ import semver from 'semver'
173
186
  import { useI18n } from '@feedmepos/mf-common'
174
187
  import { OrderSettingApi } from '@/api/order-setting'
175
188
  import { type OrderSettingsDto } from '@entity'
189
+ import { useAutoSendGuard } from '../useAutoSendGuard'
176
190
  import { useSnackbarFunctions } from '@/components/snackbar'
177
191
 
178
192
  const { t } = useI18n()
@@ -270,10 +284,29 @@ function updateOverridePax(v: boolean) {
270
284
  if (dineInSettings) dineInSettings.overridePax = v
271
285
  }
272
286
 
273
- function updateAutoCloseBill(v: boolean) {
274
- const dineInSettings = ensureOrderDineInSettings()
275
- if (dineInSettings) dineInSettings.autoCloseBill = v
276
- }
287
+ const dineInAutoAccept = computed(() => orderSetting.value?.channelAutoAccept?.dineIn ?? null)
288
+
289
+ // Select + auto-close coupling live in one shared implementation; this view only
290
+ // says how to PERSIST a change. `null` stays unset so the backend keeps
291
+ // inheriting the channel's auto-accept — writing `false` would pin it.
292
+ const {
293
+ autoSendKitchenValue,
294
+ selectItems: autoSendSelectItems,
295
+ autoCloseSublabel,
296
+ autoCloseSwitchKey,
297
+ updateAutoSendKitchen,
298
+ updateAutoCloseBill
299
+ } = useAutoSendGuard({
300
+ channel: () => dineInAutoAccept.value,
301
+ autoSendKitchen: () => orderSetting.value?.dineInSettings?.autoSendKitchen,
302
+ autoCloseBill: () => orderSetting.value?.dineInSettings?.autoCloseBill,
303
+ apply: (patch) => {
304
+ const settings = ensureOrderDineInSettings()
305
+ if (!settings) return
306
+ if ('autoSendKitchen' in patch) settings.autoSendKitchen = patch.autoSendKitchen ?? null
307
+ if ('autoCloseBill' in patch) settings.autoCloseBill = patch.autoCloseBill ?? false
308
+ }
309
+ })
277
310
 
278
311
  function updatePrintReceiptAfterClose(v: boolean) {
279
312
  const dineInSettings = ensureOrderDineInSettings()
@@ -7,6 +7,7 @@
7
7
  <PickUpSettingDialogContent
8
8
  :pickup="pickupSetting"
9
9
  :order-pickup-settings="orderPickupSettings"
10
+ :channel-auto-accept="orderSetting?.channelAutoAccept?.pickup ?? null"
10
11
  :accept-pickup="acceptPickup"
11
12
  @update-pickup="updatePickupSetting"
12
13
  @update-order-settings="updateOrderPickupSettings"
@@ -19,12 +19,24 @@
19
19
  label-placement="right"
20
20
  @update:model-value="updateIgnoreStock"
21
21
  />
22
+ <div class="flex flex-col gap-2 mt-5">
23
+ <span class="fm-typo-en-body-md-600">{{ t('order.autoSendKitchen') }}</span>
24
+ <span class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
25
+ {{ t('order.autoSendKitchenChannelSublabel') }}
26
+ </span>
27
+ <FmSelect
28
+ :model-value="autoSendKitchenValue"
29
+ :items="autoSendSelectItems"
30
+ @update:model-value="updateAutoSendKitchen"
31
+ />
32
+ </div>
22
33
  <FmSwitch
23
34
  class="mt-5"
35
+ :key="autoCloseSwitchKey"
24
36
  :model-value="orderPickupSettings.autoCloseBill ?? false"
25
37
  value="autoCloseBill"
26
38
  :label="t('order.autoCloseBill')"
27
- :sublabel="t('order.autoCloseBillSublabel')"
39
+ :sublabel="autoCloseSublabel"
28
40
  label-placement="right"
29
41
  @update:model-value="updateAutoCloseBill"
30
42
  />
@@ -89,7 +101,8 @@ import {
89
101
  FdoPickupPoint,
90
102
  FdoRestaurantPickup
91
103
  } from '@feedmepos/core/entity'
92
- import type { OrderPickupSettingsDto } from '@entity'
104
+ import type { OrderPickupSettingsDto, OrderSettingChannelAutoAccept } from '@entity'
105
+ import { useAutoSendGuard } from '../useAutoSendGuard'
93
106
 
94
107
  import { computed, ref, watch, type PropType } from 'vue'
95
108
  import PickUpPointDialog from './PickUpPointDialog.vue'
@@ -119,6 +132,13 @@ const props = defineProps({
119
132
  type: Object as PropType<OrderPickupSettingsDto>,
120
133
  default: () => ({})
121
134
  },
135
+ // Read-only projection of `setting.pickup`'s auto-accept pair, injected on the
136
+ // order-settings read. null = the backend could not resolve it; the auto-close
137
+ // guard then fails OPEN rather than disabling the toggle on a guess.
138
+ channelAutoAccept: {
139
+ type: Object as PropType<OrderSettingChannelAutoAccept | null>,
140
+ default: null
141
+ },
122
142
  // Read-only pickup online status (restaurant-level `acceptPickup`); shown as a
123
143
  // labeled row at the top of this section, not editable here.
124
144
  acceptPickup: {
@@ -133,9 +153,22 @@ const getClassForOnline = (enabled: boolean) => {
133
153
  : 'bg-fm-color-neutral-gray-100 text-fm-color-typo-neutral fm-typo-en-body-md-600 border fm-corner-radius-lg w-max p-4'
134
154
  }
135
155
 
136
- function updateAutoCloseBill(autoCloseBill: boolean) {
137
- emits('update-order-settings', { ...props.orderPickupSettings, autoCloseBill })
138
- }
156
+ // Select + auto-close coupling live in one shared implementation; this view only
157
+ // says how to PERSIST a change. `null` stays unset so the backend keeps
158
+ // inheriting `setting.pickup`'s auto-accept — writing `false` would pin it.
159
+ const {
160
+ autoSendKitchenValue,
161
+ selectItems: autoSendSelectItems,
162
+ autoCloseSublabel,
163
+ autoCloseSwitchKey,
164
+ updateAutoSendKitchen,
165
+ updateAutoCloseBill
166
+ } = useAutoSendGuard({
167
+ channel: () => props.channelAutoAccept,
168
+ autoSendKitchen: () => props.orderPickupSettings.autoSendKitchen,
169
+ autoCloseBill: () => props.orderPickupSettings.autoCloseBill,
170
+ apply: (patch) => emits('update-order-settings', { ...props.orderPickupSettings, ...patch })
171
+ })
139
172
 
140
173
  function updatePrintReceiptAfterClose(printReceiptAfterClose: boolean) {
141
174
  emits('update-order-settings', { ...props.orderPickupSettings, printReceiptAfterClose })
@@ -4,7 +4,6 @@ import { OrderSettingApi } from '@/api/order-setting'
4
4
  import { restaurantApi, type RestaurantSettingDoc } from '@/api/restaurant-setting'
5
5
  import { useLoading } from '@/composables/loading'
6
6
  import {
7
- type FdoOrderSettings,
8
7
  type FdoOrderQueueSettings,
9
8
  defaultQueueSetting,
10
9
  OrderSettingsDto,
@@ -16,13 +15,16 @@ import { useSnackbarFunctions } from '@/components/snackbar'
16
15
  import { useDialog } from '@feedmepos/ui-library'
17
16
  import PresetRemarksSheet from './PresetRemarksSheet.vue'
18
17
  import { nextCodeSettingBand } from './code-setting-band'
18
+ import { autoSendFromSelectValue, autoSendSelectValue } from '../auto-send-kitchen'
19
19
 
20
20
  const { t } = useI18n()
21
21
  const { showSuccess, showError } = useSnackbarFunctions()
22
22
  const { currentRestaurant } = useCoreStore()
23
23
  const { isLoading, startAsyncCallWithErr, startLoading } = useLoading()
24
24
 
25
- const orderSettingDefault = ref<FdoOrderSettings | undefined>(undefined)
25
+ // The DTO, not the stored document. `OrderSettingUpdateDto` omits the injected
26
+ // read-only `channelAutoAccept`, so it can never be echoed back into Mongo.
27
+ const orderSettingDefault = ref<OrderSettingsDto | undefined>(undefined)
26
28
  const queueSettings = ref<FdoOrderQueueSettings>({ ...defaultQueueSetting })
27
29
  const restaurantSetting = ref<RestaurantSettingDoc | undefined>(undefined)
28
30
  const presetRemarks = ref<string[]>([])
@@ -444,14 +446,9 @@ async function saveAllowOverrideQueueCode(value: boolean) {
444
446
  <div class="flex flex-col gap-2">
445
447
  <span class="fm-typo-en-body-md-600">{{ t('order.autoSendKitchen') }}</span>
446
448
  <FmSelect
447
- :model-value="
448
- queueSettings.autoSendKitchen === null
449
- ? 'null'
450
- : String(queueSettings.autoSendKitchen)
451
- "
449
+ :model-value="autoSendSelectValue(queueSettings.autoSendKitchen)"
452
450
  @update:model-value="
453
- (v: string) =>
454
- updateQueueSetting('autoSendKitchen', v === 'null' ? null : v === 'true')
451
+ (v: string) => updateQueueSetting('autoSendKitchen', autoSendFromSelectValue(v))
455
452
  "
456
453
  :items="[
457
454
  { label: t('order.followDineIn'), value: 'null' },
@@ -0,0 +1,142 @@
1
+ import { computed, ref } from 'vue'
2
+ import type { OrderSettingChannelAutoAccept } from '@entity'
3
+ import { useDialog } from '@feedmepos/ui-library'
4
+ import { useI18n } from '@feedmepos/mf-common'
5
+ import {
6
+ autoCloseSublabelKey,
7
+ autoSendFromSelectValue,
8
+ autoSendSelectValue,
9
+ requiresAutoSendConfirmation
10
+ } from './auto-send-kitchen'
11
+
12
+ /**
13
+ * The Auto Send Kitchen select and its coupling to Auto close bill, as ONE
14
+ * implementation.
15
+ *
16
+ * This used to be copy-pasted into every channel view, and the copies drifted:
17
+ * review caught one path bumping the switch-remount key and another not, which
18
+ * is precisely the class of bug the remount exists to prevent.
19
+ *
20
+ * Used by the three views that own BOTH controls: dine-in, pickup and in-house
21
+ * delivery. Queue deliberately does not — it has no auto-close toggle of its own
22
+ * (queue orders take `autoCloseBill` from `dineInSettings`), so there is no
23
+ * event to hang the dialog on, and wiring it would mean this page writing to
24
+ * another page's settings. Kiosk owns both flags and SHOULD use this, but its
25
+ * settings block is still behind `v-if="false"` — wire it here when that block
26
+ * is unhidden rather than growing a fourth copy.
27
+ *
28
+ * This is guidance only. The backend enforces "a bill is only closed
29
+ * automatically if the kitchen was notified" at the point of resolution, so
30
+ * nothing here is load-bearing: a merchant who never opens this page, an old
31
+ * portal bundle, or a direct API write is covered regardless.
32
+ *
33
+ * Views differ only in HOW they persist, so that is the one thing they pass in:
34
+ * `apply` receives a partial patch and the view maps it onto its own state
35
+ * (a reactive settings object, or an emit).
36
+ */
37
+ export function useAutoSendGuard(opts: {
38
+ /** The channel's auto-accept pair, or null when it could not be read. */
39
+ channel: () => OrderSettingChannelAutoAccept | null | undefined
40
+ autoSendKitchen: () => boolean | null | undefined
41
+ autoCloseBill: () => boolean | null | undefined
42
+ /** Dine-in's value, for the channels that inherit it (queue, kiosk). */
43
+ inheritFrom?: () => boolean | null | undefined
44
+ apply: (patch: { autoSendKitchen?: boolean | null; autoCloseBill?: boolean }) => void
45
+ }) {
46
+ const { t } = useI18n()
47
+ const Dialog = useDialog()
48
+
49
+ const inheritFrom = () => opts.inheritFrom?.() ?? null
50
+
51
+ /**
52
+ * Remounts the auto-close switch so it re-reads the STORED value. FmSwitch's
53
+ * internal state after a click we decline to apply is not something we
54
+ * control, and a switch left visually on while nothing was saved is the worst
55
+ * outcome here.
56
+ */
57
+ const autoCloseSwitchKey = ref(0)
58
+
59
+ const autoSendKitchenValue = computed(() => autoSendSelectValue(opts.autoSendKitchen()))
60
+
61
+ const autoCloseSublabel = computed(() =>
62
+ t(
63
+ autoCloseSublabelKey(
64
+ opts.autoSendKitchen(),
65
+ opts.channel(),
66
+ opts.autoCloseBill(),
67
+ inheritFrom()
68
+ )
69
+ )
70
+ )
71
+
72
+ // Static labels: the unset option says what it INHERITS from, not what that
73
+ // currently evaluates to. Resolving it here meant the label depended on the
74
+ // async settings GET, which is why the select needed a remount key to repaint
75
+ // — and the resolved suffix was the thing that made a simple control look
76
+ // complicated. The backend is what guarantees the outcome now.
77
+ const selectItems = computed(() => [
78
+ { label: t('order.followAutoAccept'), value: 'null' },
79
+ { label: t('order.enabled'), value: 'true' },
80
+ { label: t('order.disabled'), value: 'false' }
81
+ ])
82
+
83
+ function updateAutoSendKitchen(v: string) {
84
+ const autoSendKitchen = autoSendFromSelectValue(v)
85
+ // Reverse direction: turning kitchen send off while auto-close is ON would
86
+ // leave auto-close inert, so it visibly follows off. No dialog — this
87
+ // direction only REMOVES behaviour, it never starts printing tickets.
88
+ //
89
+ // Gated on auto-close actually being on. Without that check this wrote an
90
+ // explicit `autoCloseBill: false` over an UNSET value on every such change,
91
+ // turning "inherit" into "pinned off" — the same hazard the kiosk mapper
92
+ // avoids by sending `?? undefined` — and remounted the switch to show a
93
+ // change that had not happened.
94
+ const clearsAutoClose =
95
+ opts.autoCloseBill() === true &&
96
+ requiresAutoSendConfirmation(autoSendKitchen, opts.channel(), inheritFrom())
97
+ if (clearsAutoClose) autoCloseSwitchKey.value++
98
+ opts.apply(
99
+ clearsAutoClose ? { autoSendKitchen, autoCloseBill: false } : { autoSendKitchen }
100
+ )
101
+ }
102
+
103
+ function updateAutoCloseBill(autoCloseBill: boolean) {
104
+ // Turning auto-close ON while the kitchen would not be notified is the one
105
+ // case worth interrupting: the setting would do nothing, and confirming is
106
+ // the one-click fix. The backend suppresses the close either way, so this
107
+ // is guidance rather than the guard it used to be.
108
+ if (
109
+ autoCloseBill &&
110
+ requiresAutoSendConfirmation(opts.autoSendKitchen(), opts.channel(), inheritFrom())
111
+ ) {
112
+ autoCloseSwitchKey.value++
113
+ Dialog.open({
114
+ title: t('order.autoCloseBillEnableKitchenTitle'),
115
+ message: t('order.autoCloseBillEnableKitchenMessage'),
116
+ dismissAway: false,
117
+ primaryActions: {
118
+ text: t('order.autoCloseBillEnableKitchenConfirm'),
119
+ variant: 'primary',
120
+ close: true
121
+ },
122
+ secondaryActions: { text: t('order.cancel'), variant: 'tertiary', close: true }
123
+ }).onPrimary(() => {
124
+ // Pin `true` rather than leave it inherited: otherwise turning
125
+ // auto-accept off later would silently make auto-close inert again.
126
+ opts.apply({ autoSendKitchen: true, autoCloseBill: true })
127
+ })
128
+ return
129
+ }
130
+
131
+ opts.apply({ autoCloseBill })
132
+ }
133
+
134
+ return {
135
+ autoSendKitchenValue,
136
+ selectItems,
137
+ autoCloseSublabel,
138
+ autoCloseSwitchKey,
139
+ updateAutoSendKitchen,
140
+ updateAutoCloseBill
141
+ }
142
+ }
package/tsconfig.app.json CHANGED
@@ -8,7 +8,16 @@
8
8
  "../../common/**/*",
9
9
  "src/**/*.json"
10
10
  ],
11
- "exclude": ["src/**/__tests__/*", "src/**/*.spec.ts"],
11
+ "exclude": [
12
+ "src/**/__tests__/*",
13
+ "src/**/*.spec.ts",
14
+ // `include` pulls in all of `../../common` for the shared helpers, which
15
+ // also sweeps up the jest compatibility suites that live there. This is a
16
+ // Vue app: its `types` array is ["googlemaps"], so jest globals are not
17
+ // loaded and every `describe` / `it` / `expect` in those files is an error.
18
+ // They are backend tests and are type-checked by the backend gate.
19
+ "../../common/**/*.test.ts"
20
+ ],
12
21
  "compilerOptions": {
13
22
  "rootDir": "./../..",
14
23
  "composite": true,