@feedmepos/mf-order-setting 0.0.72-prod → 0.0.73-dev

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 (167) hide show
  1. package/.env +1 -1
  2. package/README.md +7 -23
  3. package/dist/{KioskDevicesView-DIiEVc0i.js → KioskDevicesView-Dztw-Q35.js} +1 -1
  4. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-BD0Olm4_.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-Ol419NZh.js} +3 -3
  5. package/dist/{KioskSettingView-Csm9he9M.js → KioskSettingView-BUHmoxyO.js} +183 -202
  6. package/dist/{KioskView-Ho2ts0j6.js → KioskView-B8j77zMA.js} +4 -4
  7. package/dist/OrderSettingsView-C5PK8CKZ.js +60009 -0
  8. package/dist/{app-xl2InG-H.js → app-C2_s0fkK.js} +5066 -4942
  9. package/dist/app.js +1 -1
  10. package/dist/{dayjs.min-BVqlMLNE.js → dayjs.min-B3nOmXcs.js} +1 -1
  11. package/dist/frontend/mf-order/src/api/menu/index.d.ts +1 -1
  12. package/dist/frontend/mf-order/src/api/remoteOrder/index.d.ts +10 -3
  13. package/dist/frontend/mf-order/src/app.d.ts +44 -8
  14. package/dist/frontend/mf-order/src/components/GoogleMap.vue.d.ts +1 -1
  15. package/dist/frontend/mf-order/src/composables/useQrOrderingEnabled.d.ts +2 -0
  16. package/dist/frontend/mf-order/src/helpers/menu.d.ts +1 -1
  17. package/dist/frontend/mf-order/src/main.d.ts +44 -8
  18. package/dist/frontend/mf-order/src/stores/kiosk/index.d.ts +21 -21
  19. package/dist/frontend/mf-order/src/stores/menu/menu.d.ts +906 -1044
  20. package/dist/frontend/mf-order/src/stores/order-setting/index.d.ts +36 -3
  21. package/dist/frontend/mf-order/src/stores/restaurant/index.d.ts +5 -2
  22. package/dist/frontend/mf-order/src/views/order-settings/delivery/DeliverySetting.vue.d.ts +2 -2
  23. package/dist/frontend/mf-order/src/views/order-settings/delivery/components/TaxInput.vue.d.ts +3 -8
  24. package/dist/frontend/mf-order/src/views/order-settings/delivery/delivery.d.ts +3 -1
  25. package/dist/frontend/mf-order/src/views/order-settings/delivery/delivery.data.d.ts +5 -1
  26. package/dist/frontend/mf-order/src/views/order-settings/delivery/inhouse/InHouseDelivery.vue.d.ts +21 -0
  27. package/dist/frontend/mf-order/src/views/order-settings/delivery/integrated-delivery/ExternalSetting.vue.d.ts +2 -2
  28. package/dist/frontend/mf-order/src/views/order-settings/delivery/integrated-delivery/GrabfoodSetting.vue.d.ts +1 -1
  29. package/dist/frontend/mf-order/src/views/order-settings/delivery/integrated-delivery/IntegratedDelivery.vue.d.ts +28 -2
  30. package/dist/frontend/mf-order/src/views/order-settings/delivery/integrated-delivery/LinemanSetting.vue.d.ts +46 -0
  31. package/dist/frontend/mf-order/src/views/order-settings/delivery/integrated-delivery/TngSetting.vue.d.ts +57 -0
  32. package/dist/frontend/mf-order/src/views/order-settings/pickup/PickUpSettingDialogContent.vue.d.ts +23 -1
  33. package/dist/frontend/mf-order/src/views/order-settings/pickup/Preorder.vue.d.ts +27 -0
  34. package/dist/frontend/mf-order/src/views/order-settings/servicecharge/RateInput.vue.d.ts +8 -0
  35. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  36. package/dist/index-6PQJ07gD.js +665 -0
  37. package/dist/{index-B4qdL9W3.js → index-B4XxBqiV.js} +1 -1
  38. package/dist/lineman-settings.dto-DcYJFu0U.js +60394 -0
  39. package/dist/package/entity/_type-assert.d.ts +7 -0
  40. package/dist/package/entity/booking/booking.dto.d.ts +24 -24
  41. package/dist/package/entity/delivery/delivery.dto.d.ts +8 -8
  42. package/dist/package/entity/delivery/delivery.enum.d.ts +12 -0
  43. package/dist/package/entity/delivery/gateway/grab.dto.d.ts +2 -2
  44. package/dist/package/entity/delivery/gateway/kosmo.dto.d.ts +16 -16
  45. package/dist/package/entity/delivery/gateway/pandago.dto.d.ts +4 -4
  46. package/dist/package/entity/incoming-order/incoming-order-to-bill.dto.d.ts +282 -4591
  47. package/dist/package/entity/incoming-order/incoming-order.do.d.ts +32 -24
  48. package/dist/package/entity/incoming-order/incoming-order.dto.d.ts +23 -11719
  49. package/dist/package/entity/incoming-order/incoming-order.enum.d.ts +2 -2
  50. package/dist/package/entity/index.d.ts +12 -0
  51. package/dist/package/entity/kiosk/kiosk.do.d.ts +140 -140
  52. package/dist/package/entity/kiosk/kiosk.dto.d.ts +168 -168
  53. package/dist/package/entity/kiosk/marketing/marketing.dto.d.ts +63 -7
  54. package/dist/package/entity/marketing/marketing.dto.d.ts +4 -4
  55. package/dist/package/entity/member/member.dto.d.ts +6 -0
  56. package/dist/package/entity/order/dine-in/dine-in.do.d.ts +8 -8
  57. package/dist/package/entity/order/effects/effect.dto.d.ts +68 -16
  58. package/dist/package/entity/order/menu/menu.dto.d.ts +45 -8
  59. package/dist/package/entity/order/order-item/order-item.dto.d.ts +358 -3853
  60. package/dist/package/entity/order/order.do.d.ts +239 -42
  61. package/dist/package/entity/order/order.dto.d.ts +6497 -2236
  62. package/dist/package/entity/order/payment/payment.dto.d.ts +52 -12
  63. package/dist/package/entity/order-platform/base-integration.do.d.ts +8 -0
  64. package/dist/package/entity/order-platform/deliveroo/deliveroo-dto.d.ts +2 -2
  65. package/dist/package/entity/order-platform/deliveroo/deliveroo-order.do.d.ts +524 -524
  66. package/dist/package/entity/order-platform/deliveroo/deliveroo-setting.do.d.ts +2 -2
  67. package/dist/package/entity/order-platform/deliveroo/deliveroo.menu.dto.d.ts +58 -58
  68. package/dist/package/entity/order-platform/external/menu/external-master-menu.do.d.ts +212 -284
  69. package/dist/package/entity/order-platform/external/menu/external-menu.do.d.ts +331 -589
  70. package/dist/package/entity/order-platform/external/menu/external-menu.dto.d.ts +1 -1
  71. package/dist/package/entity/order-platform/external/order/external-order.do.d.ts +12 -12
  72. package/dist/package/entity/order-platform/external/order/external-order.dto.d.ts +275 -275
  73. package/dist/package/entity/order-platform/external/setting/external-setting.do.d.ts +3 -3
  74. package/dist/package/entity/order-platform/external/setting/external-setting.dto.d.ts +3 -3
  75. package/dist/package/entity/order-platform/foodpanda/foodpanda-menu.dto.d.ts +284 -284
  76. package/dist/package/entity/order-platform/foodpanda/foodpanda-order.do.d.ts +40 -40
  77. package/dist/package/entity/order-platform/foodpanda/foodpanda-order.dto.d.ts +16 -16
  78. package/dist/package/entity/order-platform/foodpanda/foodpanda-settings.do.d.ts +2 -2
  79. package/dist/package/entity/order-platform/foodpanda/foodpanda-settings.dto.d.ts +2 -2
  80. package/dist/package/entity/order-platform/gofood/gofood-order.do.d.ts +18 -18
  81. package/dist/package/entity/order-platform/gofood/gofood-settings.do.d.ts +2 -2
  82. package/dist/package/entity/order-platform/gofood/gofood-settings.dto.d.ts +2 -2
  83. package/dist/package/entity/order-platform/gofood/gofood.dto.d.ts +32 -32
  84. package/dist/package/entity/order-platform/grabfood/grabfood-edit-order.do.d.ts +12 -12
  85. package/dist/package/entity/order-platform/grabfood/grabfood-omni.do.d.ts +134 -115
  86. package/dist/package/entity/order-platform/grabfood/grabfood-order.do.d.ts +187 -165
  87. package/dist/package/entity/order-platform/grabfood/grabfood-settings.do.d.ts +8 -2
  88. package/dist/package/entity/order-platform/grabfood/grabfood-webhook.dto.d.ts +49 -42
  89. package/dist/package/entity/order-platform/grabfood/grabfood.dto.d.ts +67 -54
  90. package/dist/package/entity/order-platform/grabfood/grabfood.enum.d.ts +1 -1
  91. package/dist/package/entity/order-platform/lineman/lineman-settings.do.d.ts +37 -0
  92. package/dist/package/entity/order-platform/lineman/lineman-settings.dto.d.ts +40 -0
  93. package/dist/package/entity/order-platform/menu.dto.d.ts +716 -13887
  94. package/dist/package/entity/order-platform/order-platform.dto.d.ts +136 -2
  95. package/dist/package/entity/order-platform/order-platform.enum.d.ts +1 -1
  96. package/dist/package/entity/order-platform/shopeefood/shopeefood-order.do.d.ts +176 -176
  97. package/dist/package/entity/order-platform/shopeefood/shopeefood-order.dto.d.ts +62 -62
  98. package/dist/package/entity/order-platform/shopeefood/shopeefood-settings.do.d.ts +8 -2
  99. package/dist/package/entity/order-platform/shopeefood/shopeefood-settings.dto.d.ts +8 -2
  100. package/dist/package/entity/order-platform/tng/tng-menu.do.d.ts +1061 -0
  101. package/dist/package/entity/order-platform/tng/tng-order.do.d.ts +1915 -0
  102. package/dist/package/entity/order-platform/tng/tng-settings.do.d.ts +379 -0
  103. package/dist/package/entity/order-platform/tng/tng-settings.dto.d.ts +301 -0
  104. package/dist/package/entity/order-platform/tng/tng.dto.d.ts +1162 -0
  105. package/dist/package/entity/order-platform/tng/tng.enum.d.ts +29 -0
  106. package/dist/package/entity/order-setting/delivery/inhouse-delivery.do.d.ts +12 -0
  107. package/dist/package/entity/order-setting/delivery/inhouse-delivery.dto.d.ts +12 -0
  108. package/dist/package/entity/order-setting/dine-in/dine-in.do.d.ts +11 -0
  109. package/dist/package/entity/order-setting/dine-in/dine-in.dto.d.ts +9 -0
  110. package/dist/package/entity/order-setting/kiosk/kiosk.do.d.ts +2 -2
  111. package/dist/package/entity/order-setting/kiosk/kiosk.dto.d.ts +2 -2
  112. package/dist/package/entity/order-setting/order-setting.do.d.ts +55 -4
  113. package/dist/package/entity/order-setting/order-setting.dto.d.ts +110 -8
  114. package/dist/package/entity/order-setting/pickup/pickup.do.d.ts +12 -0
  115. package/dist/package/entity/order-setting/pickup/pickup.dto.d.ts +12 -0
  116. package/dist/package/entity/payment/transaction.d.ts +80 -8
  117. package/dist/package/entity/printer/printer.do.d.ts +14 -14
  118. package/dist/package/entity/pubsub/ably/ably.enum.d.ts +1 -1
  119. package/dist/package/entity/queue/queue.do.d.ts +236 -1901
  120. package/dist/package/entity/queue/queue.dto.d.ts +743 -445
  121. package/dist/package/entity/restaurant/restaurant.dto.d.ts +27 -4
  122. package/dist/package/entity/websocket/websocket.dto.d.ts +10 -10
  123. package/dist/style.css +1 -0
  124. package/golar.config.ts +20 -0
  125. package/package.json +28 -22
  126. package/src/Entry.vue +2 -10
  127. package/src/api/menu/index.ts +1 -1
  128. package/src/api/remoteOrder/index.ts +176 -110
  129. package/src/composables/useQrOrderingEnabled.ts +66 -0
  130. package/src/helpers/menu.ts +1 -1
  131. package/src/locales/en-US.json +421 -412
  132. package/src/locales/ja-JP.json +1769 -1760
  133. package/src/locales/th-TH.json +409 -400
  134. package/src/locales/zh-CN.json +417 -408
  135. package/src/main.ts +4 -44
  136. package/src/router/index.ts +2 -2
  137. package/src/stores/menu/menu.ts +48 -27
  138. package/src/stores/restaurant/index.ts +24 -9
  139. package/src/utils/firebase-storage.ts +3 -13
  140. package/src/views/kiosk/settings/KioskSettingView.vue +3 -2
  141. package/src/views/order-settings/OrderSettingsView.vue +31 -11
  142. package/src/views/order-settings/delivery/DeliverySetting.vue +223 -11
  143. package/src/views/order-settings/delivery/components/TaxInput.vue +1 -1
  144. package/src/views/order-settings/delivery/delivery.data.ts +31 -10
  145. package/src/views/order-settings/delivery/delivery.ts +60 -28
  146. package/src/views/order-settings/delivery/inhouse/InHouseDelivery.vue +40 -1
  147. package/src/views/order-settings/delivery/integrated-delivery/DeliverooSetting.vue +1 -1
  148. package/src/views/order-settings/delivery/integrated-delivery/GrabfoodSetting.vue +15 -7
  149. package/src/views/order-settings/delivery/integrated-delivery/IntegratedDelivery.vue +83 -27
  150. package/src/views/order-settings/delivery/integrated-delivery/LinemanSetting.vue +79 -0
  151. package/src/views/order-settings/delivery/integrated-delivery/ShopeefoodSetting.vue +2 -0
  152. package/src/views/order-settings/delivery/integrated-delivery/TngSetting.vue +749 -0
  153. package/src/views/order-settings/dine-in/DineInSetting.vue +54 -23
  154. package/src/views/order-settings/general/GeneralSetting.vue +15 -6
  155. package/src/views/order-settings/pickup/PickUpSettingDialog.vue +31 -3
  156. package/src/views/order-settings/pickup/PickUpSettingDialogContent.vue +34 -0
  157. package/src/views/order-settings/pickup/Preorder.vue +13 -12
  158. package/src/views/order-settings/queue/QueueSetting.vue +30 -1
  159. package/src/views/order-settings/servicecharge/RateInput.vue +9 -7
  160. package/vite.config.ts +16 -16
  161. package/dist/OrderSettingsView-C0G8kP3v.js +0 -32222
  162. package/dist/frontend/mf-order/src/api/auth/index.d.ts +0 -7
  163. package/dist/frontend/mf-order/src/stores/iframe/index.d.ts +0 -14
  164. package/dist/index-kCI8JdNP.js +0 -628
  165. package/dist/queue-transfer.dto-BLyCakS5.js +0 -45618
  166. package/src/api/auth/index.ts +0 -20
  167. package/src/stores/iframe/index.ts +0 -23
@@ -0,0 +1,749 @@
1
+ <script setup lang="ts">
2
+ import { computed, type PropType, reactive, ref } from 'vue'
3
+ import IntegratedDelivery from './IntegratedDelivery.vue'
4
+ import RateInput from '../../servicecharge/RateInput.vue'
5
+ import { clone } from '@/helpers/object'
6
+ import {
7
+ FdoTngSettings,
8
+ TNG_FULFILLMENT_KEYS,
9
+ type TNG_FULFILLMENT_KEY,
10
+ type TNG_PAYMENT_METHOD
11
+ } from '@entity'
12
+ import { useI18n, useCoreStore } from '@feedmepos/mf-common'
13
+ import { FdoLinkedDelivery } from '@feedmepos/core/entity'
14
+ import { useLoading } from '@/composables/loading'
15
+ import { remoteOrderApi } from '@/api/remoteOrder'
16
+ import { useSnackbarFunctions } from '@/components/snackbar'
17
+ import type { DeliveryCompany } from '../delivery'
18
+ import type { IntegratedDeliverySettingData } from './IntegratedDelivery.vue'
19
+ import type { CategorizedItem } from '@/helpers/menu'
20
+ import type { AdjustmentAmount } from '@/helpers/number'
21
+
22
+ export interface TngDeliveryData {
23
+ manual: FdoLinkedDelivery | null
24
+ integrated: FdoTngSettings
25
+ }
26
+
27
+ type FulfillmentType = TNG_FULFILLMENT_KEY
28
+ type ServiceChargeDraft = { items: string[] | null | undefined; rate: AdjustmentAmount }
29
+ type FulfillmentDraft = {
30
+ catalogId: string | null
31
+ paymentMethods: string[]
32
+ serviceCharge: ServiceChargeDraft[]
33
+ }
34
+ type SelectedCategories = Record<string, { isAllSelected: boolean; isSomeSelected: boolean }>
35
+
36
+ const props = defineProps({
37
+ initialValue: {
38
+ type: Object as PropType<TngDeliveryData>,
39
+ required: true
40
+ },
41
+ company: {
42
+ type: Object as PropType<DeliveryCompany>,
43
+ required: true
44
+ },
45
+ canUseAutoDeliveryIntegration: {
46
+ type: [String, Boolean],
47
+ required: true
48
+ },
49
+ categorizedItems: {
50
+ type: Array as PropType<CategorizedItem[]>,
51
+ required: true
52
+ }
53
+ })
54
+ const emits = defineEmits<{
55
+ (e: 'update:model-value', payload: { manual: any; auto: any }): void
56
+ (e: 'validation-result', message: string | null): void
57
+ }>()
58
+
59
+ const { t } = useI18n()
60
+ const { currentRestaurant } = useCoreStore()
61
+ const { startAsyncCallWithErr } = useLoading()
62
+ const { showError, showSuccess } = useSnackbarFunctions()
63
+
64
+ const data = reactive(clone(props.initialValue))
65
+
66
+ const paymentMethodOptions = [
67
+ { label: 'TNGD', value: 'TNGD' },
68
+ { label: 'Cashier', value: 'CASHIER' }
69
+ ]
70
+ const defaultPaymentMethod = 'TNGD'
71
+ const defaultPaymentMethods = [defaultPaymentMethod]
72
+
73
+ const paymentMethodWarnings = reactive<Record<FulfillmentType, string>>({
74
+ dineIn: '',
75
+ pickup: '',
76
+ reservation: ''
77
+ })
78
+ const fulfillmentModeUiKeys = reactive<Record<FulfillmentType, number>>({
79
+ dineIn: 0,
80
+ pickup: 0,
81
+ reservation: 0
82
+ })
83
+ const paymentMethodUiKeys = reactive<Record<FulfillmentType, number>>({
84
+ dineIn: 0,
85
+ pickup: 0,
86
+ reservation: 0
87
+ })
88
+
89
+ const fulfillmentLabels: Record<FulfillmentType, string> = {
90
+ dineIn: 'Dine In',
91
+ pickup: 'Pick Up',
92
+ reservation: 'Reservation'
93
+ }
94
+
95
+ const fulfillmentSections: Array<{ key: FulfillmentType; label: string }> =
96
+ TNG_FULFILLMENT_KEYS.map((key) => ({ key, label: fulfillmentLabels[key] }))
97
+
98
+ const fulfillmentEnabled = reactive<Record<FulfillmentType, boolean>>({
99
+ dineIn: data.integrated.dineIn != null,
100
+ pickup: data.integrated.pickup != null,
101
+ reservation: data.integrated.reservation != null
102
+ })
103
+
104
+ const modelValue = computed<IntegratedDeliverySettingData>(() => {
105
+ const auto = data.integrated
106
+ return {
107
+ manual: data.manual,
108
+ auto: {
109
+ enable: auto.enable,
110
+ integrationId: auto.partnerMerchantId,
111
+ catalogId: null,
112
+ autoAccept: auto.autoAccept,
113
+ autoCloseBill: auto.autoCloseBill || false,
114
+ printReceiptAfterClose: auto.printReceiptAfterClose ?? true,
115
+ canRemark: auto.canRemark ?? true,
116
+ commissionRate: 0,
117
+ status: auto.status,
118
+ // updatedAt: auto.updatedAt,
119
+ // lastSynchronize: auto.lastSynchronize,
120
+ menuSyncError: auto.menuSyncError?.message ?? null
121
+ }
122
+ }
123
+ })
124
+
125
+ function toFulfillmentDraft(config: any): FulfillmentDraft | null {
126
+ if (!config) return null
127
+ return {
128
+ catalogId: config.catalogId ?? null,
129
+ paymentMethods: [...(config.paymentMethods ?? [])],
130
+ serviceCharge: Array.isArray(config.serviceCharge)
131
+ ? config.serviceCharge.map((sc: any) => ({
132
+ items: sc.items ?? null,
133
+ rate: sc.rate ?? { amount: 0, precision: 4 }
134
+ }))
135
+ : []
136
+ }
137
+ }
138
+
139
+ const fulfillmentDraftCache = reactive<Record<FulfillmentType, FulfillmentDraft | null>>({
140
+ dineIn: toFulfillmentDraft(data.integrated.dineIn),
141
+ pickup: toFulfillmentDraft(data.integrated.pickup),
142
+ reservation: toFulfillmentDraft(data.integrated.reservation)
143
+ })
144
+
145
+ function emitUpdatedTngSettings() {
146
+ emits('update:model-value', { manual: data.manual, auto: data.integrated })
147
+ }
148
+
149
+ function clearValidationState(type?: FulfillmentType) {
150
+ emits('validation-result', null)
151
+ if (type) paymentMethodWarnings[type] = ''
152
+ }
153
+
154
+ function resolvePaymentMethods(type: FulfillmentType): string[] {
155
+ const cached = fulfillmentDraftCache[type]?.paymentMethods
156
+ if (cached?.length) return [...cached]
157
+
158
+ const current = data.integrated[type]?.paymentMethods
159
+ if (current?.length) return [...current]
160
+
161
+ return [...defaultPaymentMethods]
162
+ }
163
+
164
+ function updateModelValue({ manual, auto }: IntegratedDeliverySettingData) {
165
+ const {
166
+ integrationId,
167
+ autoAccept,
168
+ enable,
169
+ autoCloseBill,
170
+ printReceiptAfterClose,
171
+ canRemark,
172
+ status
173
+ } = auto
174
+
175
+ data.manual = manual
176
+ data.integrated = {
177
+ ...data.integrated,
178
+ enable,
179
+ autoAccept: autoAccept ?? data.integrated.autoAccept ?? true,
180
+ autoCloseBill: autoCloseBill ?? false,
181
+ printReceiptAfterClose: printReceiptAfterClose ?? true,
182
+ canRemark: canRemark ?? true,
183
+ partnerMerchantId: integrationId,
184
+ status: status ?? data.integrated.status
185
+ }
186
+
187
+ emitUpdatedTngSettings()
188
+ }
189
+
190
+ function onToggleFulfillment(type: FulfillmentType, enabled: boolean) {
191
+ const enabledModesCount = Object.values(fulfillmentEnabled).filter(Boolean).length
192
+
193
+ if (!enabled && enabledModesCount <= 1) {
194
+ const msg = 'At least one fulfillment mode is required'
195
+ showError(msg)
196
+ emits('validation-result', msg)
197
+ fulfillmentModeUiKeys[type] += 1
198
+ emitUpdatedTngSettings()
199
+ return
200
+ }
201
+
202
+ clearValidationState(type)
203
+ fulfillmentEnabled[type] = enabled
204
+
205
+ if (enabled) {
206
+ const cachedConfig = fulfillmentDraftCache[type]
207
+ const currentConfig = data.integrated[type]
208
+
209
+ data.integrated = {
210
+ ...data.integrated,
211
+ [type]: {
212
+ catalogId: cachedConfig?.catalogId ?? currentConfig?.catalogId ?? null,
213
+ paymentMethods: resolvePaymentMethods(type),
214
+ serviceCharge: cachedConfig?.serviceCharge ??
215
+ currentConfig?.serviceCharge ?? [{ items: null, rate: { amount: 0, precision: 4 } }]
216
+ }
217
+ }
218
+ emitUpdatedTngSettings()
219
+ return
220
+ }
221
+
222
+ fulfillmentDraftCache[type] = toFulfillmentDraft(data.integrated[type])
223
+
224
+ data.integrated = { ...data.integrated, [type]: null }
225
+ emitUpdatedTngSettings()
226
+ }
227
+
228
+ function updateFulfillmentCatalog(type: FulfillmentType, catalogId: string | null) {
229
+ fulfillmentEnabled[type] = true
230
+ data.integrated = { ...data.integrated, [type]: { ...data.integrated[type], catalogId } }
231
+ emitUpdatedTngSettings()
232
+ }
233
+
234
+ function toggleFulfillmentPaymentMethod(type: FulfillmentType, method: string, checked: boolean) {
235
+ clearValidationState(type)
236
+ const current = data.integrated[type]?.paymentMethods ?? []
237
+ if (checked) {
238
+ data.integrated = {
239
+ ...data.integrated,
240
+ [type]: {
241
+ ...data.integrated[type],
242
+ paymentMethods: Array.from(new Set([...current, method]))
243
+ }
244
+ }
245
+ } else {
246
+ if (current.length <= 1) {
247
+ const msg = 'At least one payment method is required'
248
+ paymentMethodWarnings[type] = msg
249
+ showError(msg)
250
+ emits('validation-result', msg)
251
+
252
+ // Keep last option checked and force checkbox re-render for visual consistency.
253
+ data.integrated = {
254
+ ...data.integrated,
255
+ [type]: { ...data.integrated[type], paymentMethods: [...current] }
256
+ }
257
+ paymentMethodUiKeys[type] += 1
258
+ emitUpdatedTngSettings()
259
+ return
260
+ }
261
+
262
+ data.integrated = {
263
+ ...data.integrated,
264
+ [type]: { ...data.integrated[type], paymentMethods: current.filter((m) => m !== method) }
265
+ }
266
+ }
267
+ emitUpdatedTngSettings()
268
+ }
269
+
270
+ async function syncMenu() {
271
+ await startAsyncCallWithErr(async () => {
272
+ await remoteOrderApi.integratedDelivery.syncTngMenu(currentRestaurant.value?._id ?? '')
273
+ data.integrated.lastSynchronize = new Date().toISOString()
274
+ showSuccess(t('order.syncMenuSuccess'))
275
+ })
276
+ }
277
+
278
+ function updateFulfillmentServiceChargeRate(
279
+ type: FulfillmentType,
280
+ index: number,
281
+ rate: AdjustmentAmount
282
+ ) {
283
+ const scList = [...(data.integrated[type]?.serviceCharge ?? [])]
284
+ scList[index] = { ...scList[index], rate }
285
+ data.integrated = {
286
+ ...data.integrated,
287
+ [type]: { ...data.integrated[type], serviceCharge: scList }
288
+ }
289
+ emitUpdatedTngSettings()
290
+ }
291
+
292
+ function addServiceCharge(type: FulfillmentType) {
293
+ const scList = [...(data.integrated[type]?.serviceCharge ?? [])]
294
+ scList.push({ items: [], rate: { amount: 0, precision: 4 } })
295
+ data.integrated = {
296
+ ...data.integrated,
297
+ [type]: { ...data.integrated[type], serviceCharge: scList }
298
+ }
299
+ emitUpdatedTngSettings()
300
+ }
301
+
302
+ function removeServiceCharge(type: FulfillmentType, index: number) {
303
+ const scList = (data.integrated[type]?.serviceCharge ?? []).filter((_, i) => i !== index)
304
+ data.integrated = {
305
+ ...data.integrated,
306
+ [type]: { ...data.integrated[type], serviceCharge: scList }
307
+ }
308
+ emitUpdatedTngSettings()
309
+ }
310
+
311
+ function canAddServiceCharge(type: FulfillmentType): boolean {
312
+ const scList = data.integrated[type]?.serviceCharge ?? []
313
+ // Any SC covers all items → no room for more
314
+ if (scList.some((sc) => !sc.items)) return false
315
+ // All explicit items combined already cover every available item
316
+ const takenIds = new Set(scList.flatMap((sc) => sc.items ?? []))
317
+ const totalItems = props.categorizedItems.reduce((acc, cat) => acc + cat.items.length, 0)
318
+ return takenIds.size < totalItems
319
+ }
320
+
321
+ // service charge
322
+ const sideSheetItem = ref<boolean>(false)
323
+ const searchValueItem = ref<string>('')
324
+ const selectedItemIds = ref<string[] | null | undefined>(null)
325
+ const serviceChargeFulfillmentMode = ref<FulfillmentType | null>(null)
326
+ const serviceChargeEditIndex = ref<number | null>(null)
327
+ const categoryCollapseState = ref<Record<string, boolean>>({})
328
+
329
+ const excludedItemIds = computed<Set<string>>(() => {
330
+ if (!serviceChargeFulfillmentMode.value || serviceChargeEditIndex.value === null) return new Set()
331
+ const type = serviceChargeFulfillmentMode.value
332
+ const scList = data.integrated[type]?.serviceCharge ?? []
333
+ const otherScs = scList.filter((_, i) => i !== serviceChargeEditIndex.value)
334
+
335
+ // If any other SC has items: null (all items), every item is excluded
336
+ if (otherScs.some((sc) => !sc.items)) {
337
+ return new Set(props.categorizedItems.flatMap((cat) => cat.items.map((item) => item._id)))
338
+ }
339
+ return new Set(otherScs.flatMap((sc) => sc.items ?? []))
340
+ })
341
+
342
+ function isScItemSelectDisabled(type: FulfillmentType, index: number): boolean {
343
+ const scList = data.integrated[type]?.serviceCharge ?? []
344
+ const otherScs = scList.filter((_, i) => i !== index)
345
+ // Any other SC claims all items → nothing left for this one
346
+ if (otherScs.some((sc) => !sc.items)) return true
347
+ // All items are explicitly claimed by other SCs
348
+ const takenIds = new Set(otherScs.flatMap((sc) => sc.items ?? []))
349
+ const totalItems = props.categorizedItems.reduce((acc, cat) => acc + cat.items.length, 0)
350
+ return takenIds.size >= totalItems
351
+ }
352
+
353
+ const filteredCategorizedItems = computed(() =>
354
+ props.categorizedItems
355
+ .map((categorizedItem) => ({
356
+ ...categorizedItem,
357
+ items: categorizedItem.items.filter(
358
+ (subItem) =>
359
+ !excludedItemIds.value.has(subItem._id) &&
360
+ (searchValueItem.value
361
+ ? subItem.name.toLowerCase().includes(searchValueItem.value.toLowerCase())
362
+ : true)
363
+ )
364
+ }))
365
+ .filter((item) => item.items.length > 0)
366
+ )
367
+ const totalCategorizedItemsCount = computed(() =>
368
+ props.categorizedItems.reduce(
369
+ (total, curr) =>
370
+ total + curr.items.filter((item) => !excludedItemIds.value.has(item._id)).length,
371
+ 0
372
+ )
373
+ )
374
+ const categoryCheckState = computed<SelectedCategories>(() => {
375
+ return props.categorizedItems.reduce((pv: SelectedCategories, curr: CategorizedItem) => {
376
+ const availableItems = curr.items.filter((item) => !excludedItemIds.value.has(item._id))
377
+ const selectedItems = availableItems.filter((item) => selectedItemIds.value?.includes(item._id))
378
+ const _isAllItemSelected =
379
+ !selectedItemIds.value || selectedItems.length === availableItems.length
380
+ const _isSomeItemSelected =
381
+ !!selectedItemIds.value &&
382
+ selectedItems.length > 0 &&
383
+ selectedItems.length < availableItems.length
384
+
385
+ pv[curr.id ?? ''] = { isAllSelected: _isAllItemSelected, isSomeSelected: _isSomeItemSelected }
386
+ return pv
387
+ }, {})
388
+ })
389
+
390
+ function toggleServiceChargeSideSheet(type: FulfillmentType, index: number) {
391
+ serviceChargeFulfillmentMode.value = type
392
+ serviceChargeEditIndex.value = index
393
+ const scList = data.integrated[type]?.serviceCharge ?? []
394
+ selectedItemIds.value = scList[index]?.items ?? null
395
+ searchValueItem.value = ''
396
+
397
+ props.categorizedItems.forEach((item) => {
398
+ categoryCollapseState.value[item.id ?? ''] = true
399
+ })
400
+
401
+ sideSheetItem.value = true
402
+ }
403
+ function selectAllCategorizedItems(v: boolean) {
404
+ if (v) {
405
+ if (excludedItemIds.value.size === 0) {
406
+ // No other SCs have items — safe to use null (truly all)
407
+ selectedItemIds.value = null
408
+ } else {
409
+ // Other SCs hold some items — select only what's available explicitly
410
+ selectedItemIds.value = props.categorizedItems
411
+ .flatMap((cat) => cat.items.map((item) => item._id))
412
+ .filter((id) => !excludedItemIds.value.has(id))
413
+ }
414
+ } else {
415
+ selectedItemIds.value = []
416
+ }
417
+ }
418
+ function expandedItemIds(): string[] {
419
+ return (
420
+ selectedItemIds.value ??
421
+ props.categorizedItems.flatMap((categorizedItem) =>
422
+ categorizedItem.items
423
+ .filter((item) => !excludedItemIds.value.has(item._id))
424
+ .map((subItem) => subItem._id)
425
+ )
426
+ )
427
+ }
428
+ function toggleCategorySelection(categoryId: string, isSelected: boolean) {
429
+ const categoryItemIds =
430
+ props.categorizedItems
431
+ .find((categorizedItem) => categorizedItem.id === categoryId)
432
+ ?.items.filter((item) => !excludedItemIds.value.has(item._id))
433
+ .map((subItem) => subItem._id) ?? []
434
+
435
+ if (isSelected) {
436
+ if (selectedItemIds.value === null) return
437
+
438
+ const mergedItemIds = [...new Set([...(selectedItemIds.value ?? []), ...categoryItemIds])]
439
+ selectedItemIds.value =
440
+ mergedItemIds.length === totalCategorizedItemsCount.value && excludedItemIds.value.size === 0
441
+ ? null
442
+ : mergedItemIds
443
+ } else {
444
+ selectedItemIds.value = expandedItemIds().filter((id) => !categoryItemIds.includes(id))
445
+ }
446
+ }
447
+ function toggleCategoryCollapse(categoryId: string) {
448
+ const currentCollapseState = categoryCollapseState.value[categoryId]
449
+ categoryCollapseState.value[categoryId] = !currentCollapseState
450
+ }
451
+ function confirmServiceChargeItems() {
452
+ const type = serviceChargeFulfillmentMode.value
453
+ if (!type || serviceChargeEditIndex.value === null) return
454
+ const scList = [...(data.integrated[type]?.serviceCharge ?? [])]
455
+ scList[serviceChargeEditIndex.value] = {
456
+ ...scList[serviceChargeEditIndex.value],
457
+ items: selectedItemIds.value
458
+ }
459
+ data.integrated = {
460
+ ...data.integrated,
461
+ [type]: { ...data.integrated[type], serviceCharge: scList }
462
+ }
463
+ sideSheetItem.value = false
464
+ emitUpdatedTngSettings()
465
+ }
466
+ function toggleItemSelection(itemId: string, isSelected: boolean) {
467
+ if (isSelected) {
468
+ if (selectedItemIds.value === null) return
469
+ const mergedItemIds = [...new Set([...(selectedItemIds.value ?? []), itemId])]
470
+ selectedItemIds.value =
471
+ mergedItemIds.length === totalCategorizedItemsCount.value && excludedItemIds.value.size === 0
472
+ ? null
473
+ : mergedItemIds
474
+ } else {
475
+ selectedItemIds.value = expandedItemIds().filter((id) => id !== itemId)
476
+ }
477
+ }
478
+ </script>
479
+
480
+ <template>
481
+ <div class="tng-setting-sheet">
482
+ <IntegratedDelivery
483
+ :model-value="modelValue"
484
+ :company="company"
485
+ :can-use-auto-delivery-integration="canUseAutoDeliveryIntegration"
486
+ :show-test-order-actions="false"
487
+ @update:model-value="updateModelValue"
488
+ @sync-menu="syncMenu"
489
+ >
490
+ <template #sync-menu="{ editable, catalogOptions }">
491
+ <div class="flex flex-col mt-7 gap-7">
492
+ <!-- Fulfillment Mode Setting -->
493
+ <div>
494
+ <div class="flex-grow fm-typo-en-title-sm-600 mb-3">
495
+ {{ t('order.fulfillmentModeSetting') }}
496
+ </div>
497
+
498
+ <div
499
+ v-for="section in fulfillmentSections"
500
+ :key="section.key"
501
+ :class="fulfillmentEnabled[section.key] ? 'mb-5' : ''"
502
+ >
503
+ <FmCheckbox
504
+ :key="`${section.key}-${fulfillmentModeUiKeys[section.key]}`"
505
+ :value="section.key"
506
+ :label="section.label"
507
+ :model-value="fulfillmentEnabled[section.key]"
508
+ :disabled="editable"
509
+ @update:model-value="(v: boolean) => onToggleFulfillment(section.key, v)"
510
+ />
511
+ <FmCard
512
+ v-if="fulfillmentEnabled[section.key]"
513
+ variant="outlined"
514
+ class="p-5 flex flex-col gap-3.5"
515
+ >
516
+ <!-- catalog -->
517
+ <div class="flex flex-col gap-2">
518
+ <div class="fm-typo-en-body-lg-600">
519
+ {{ t('order.catalog') }}
520
+ <span class="text-fm-color-secondary-salmon">*</span>
521
+ </div>
522
+ <FmSelect
523
+ :model-value="data.integrated[section.key]?.catalogId ?? null"
524
+ :placeholder="t('order.selectItem')"
525
+ :items="catalogOptions"
526
+ :disabled="editable"
527
+ @update:model-value="
528
+ (v: string | null) => updateFulfillmentCatalog(section.key, v)
529
+ "
530
+ />
531
+ </div>
532
+
533
+ <!-- service charge -->
534
+ <div class="flex flex-col gap-2">
535
+ <!-- title & adding button -->
536
+ <div class="flex items-center justify-between">
537
+ <div class="fm-typo-en-body-lg-600">
538
+ {{ t('order.serviceCharge') + ' (' + t('order.rate') + ')' }}
539
+ </div>
540
+
541
+ <!-- TODO: enable when TNG supports multiple service charges per fulfillment mode -->
542
+ <!-- :disabled="!canAddServiceCharge(section.key)" -->
543
+ <FmButton
544
+ variant="plain"
545
+ :label="t('order.addServiceCharge')"
546
+ size="md"
547
+ :disabled="true"
548
+ @click="() => addServiceCharge(section.key)"
549
+ />
550
+ </div>
551
+
552
+ <!-- lists -->
553
+ <div
554
+ v-for="(sc, scIndex) in data.integrated[section.key]?.serviceCharge ?? []"
555
+ :key="scIndex"
556
+ class="flex gap-1 items-center"
557
+ >
558
+ <div class="flex-1">
559
+ <RateInput
560
+ :label="''"
561
+ :model-value="sc.rate ?? { amount: 0, precision: 4 }"
562
+ @update:model-value="
563
+ (v) => updateFulfillmentServiceChargeRate(section.key, scIndex, v)
564
+ "
565
+ />
566
+ </div>
567
+
568
+ <div class="flex items-center">
569
+ <FmButton
570
+ append-icon="checkbook"
571
+ variant="plain"
572
+ size="md"
573
+ :disabled="isScItemSelectDisabled(section.key, scIndex)"
574
+ @click="() => toggleServiceChargeSideSheet(section.key, scIndex)"
575
+ />
576
+ <FmButton
577
+ append-icon="delete"
578
+ variant="plain"
579
+ size="md"
580
+ :disabled="(data.integrated[section.key]?.serviceCharge ?? []).length <= 1"
581
+ @click="() => removeServiceCharge(section.key, scIndex)"
582
+ />
583
+ </div>
584
+ </div>
585
+ </div>
586
+
587
+ <!-- payment method -->
588
+ <div class="flex flex-col gap-2">
589
+ <div>
590
+ <div class="fm-typo-en-body-lg-600">
591
+ {{ t('order.paymentMethod') }}
592
+ <span class="text-fm-color-secondary-salmon">*</span>
593
+ </div>
594
+ <div
595
+ v-if="paymentMethodWarnings[section.key]"
596
+ class="text-xs"
597
+ style="color: #dc2626"
598
+ >
599
+ {{ paymentMethodWarnings[section.key] }}
600
+ </div>
601
+ </div>
602
+ <div class="flex gap-2 flex-wrap">
603
+ <FmCheckbox
604
+ v-for="option in paymentMethodOptions"
605
+ :key="`${section.key}-${option.value}-${paymentMethodUiKeys[section.key]}`"
606
+ :value="option.value"
607
+ :label="option.label"
608
+ :model-value="
609
+ (data.integrated[section.key]?.paymentMethods ?? []).includes(
610
+ option.value as TNG_PAYMENT_METHOD
611
+ )
612
+ "
613
+ :disabled="editable"
614
+ @update:model-value="
615
+ (v: boolean) => toggleFulfillmentPaymentMethod(section.key, option.value, v)
616
+ "
617
+ />
618
+ </div>
619
+ </div>
620
+ </FmCard>
621
+ </div>
622
+ </div>
623
+
624
+ <!-- Sync Menu Setting -->
625
+ <div>
626
+ <div class="flex-grow fm-typo-en-title-sm-600 mb-3">
627
+ {{ t('order.syncMenuSetting') }}
628
+ </div>
629
+
630
+ <div class="flex items-center gap-3 mb-7">
631
+ <FmCheckbox
632
+ :value="'canRemark'"
633
+ :model-value="data.integrated.canRemark ?? true"
634
+ :label="t('order.allowRemarks') ?? 'Allow Remarks'"
635
+ @update:model-value="
636
+ (v: boolean) => {
637
+ data.integrated.canRemark = v
638
+ emitUpdatedTngSettings()
639
+ }
640
+ "
641
+ />
642
+ </div>
643
+ </div>
644
+ </div>
645
+
646
+ <!-- Service Charge Item Side Sheet -->
647
+ <FmSideSheet
648
+ :header="t('order.selectItem')"
649
+ :maxWidth="600"
650
+ dismiss-away
651
+ v-model="sideSheetItem"
652
+ >
653
+ <div>
654
+ <!-- topper -->
655
+ <div class="flex flex-row my-5 items-center">
656
+ <div class="text-fm-color-typo-secondary">
657
+ {{ selectedItemIds?.length ?? totalCategorizedItemsCount }}
658
+ {{ t('order.productSelected') }}
659
+ </div>
660
+ <FmSearch
661
+ v-model:model-value="searchValueItem"
662
+ class="ml-auto"
663
+ :placeholder="t('order.search')"
664
+ />
665
+ </div>
666
+
667
+ <!-- select all checkbox -->
668
+ <FmCheckbox
669
+ :label="t('order.selectAllItemsIncludeCustomItem')"
670
+ :model-value="!selectedItemIds"
671
+ value=""
672
+ @update:model-value="(v: boolean) => selectAllCategorizedItems(v)"
673
+ />
674
+
675
+ <!-- categorized items -->
676
+ <div
677
+ v-for="(item, index) in filteredCategorizedItems"
678
+ :key="index"
679
+ class="flex flex-col mb-6"
680
+ >
681
+ <FmCard variant="outlined" class="p-5">
682
+ <div class="flex flex-col fm-corner-radius-lg">
683
+ <!-- category checkbox + collapse button -->
684
+ <div
685
+ :class="[
686
+ 'flex flex-row items-center',
687
+ categoryCollapseState[item.id ?? ''] ? 'mb-2' : ''
688
+ ]"
689
+ >
690
+ <div class="flex flex-row">
691
+ <FmCheckbox
692
+ value=""
693
+ :model-value="categoryCheckState[item.id ?? ''].isAllSelected"
694
+ :indeterminate="categoryCheckState[item.id ?? ''].isSomeSelected"
695
+ @update:model-value="toggleCategorySelection(item.id ?? '', $event)"
696
+ />
697
+ <div class="font-bold ml-2">{{ item.name }}</div>
698
+ </div>
699
+ <FmButton
700
+ variant="tertiary"
701
+ class="ml-auto"
702
+ :icon="categoryCollapseState[item.id ?? ''] ? 'expand_less' : 'expand_more'"
703
+ @click="toggleCategoryCollapse(item.id ?? '')"
704
+ />
705
+ </div>
706
+
707
+ <!-- category items -->
708
+ <div class="flex flex-col pl-6" v-if="categoryCollapseState[item.id ?? '']">
709
+ <FmCheckbox
710
+ v-for="(subItem, subIndex) in item.items"
711
+ :key="subIndex"
712
+ :model-value="!selectedItemIds || selectedItemIds.includes(subItem._id)"
713
+ :label="subItem.name"
714
+ :value="subItem._id"
715
+ @update:model-value="(v: boolean) => toggleItemSelection(subItem._id, v)"
716
+ />
717
+ </div>
718
+ </div>
719
+ </FmCard>
720
+ </div>
721
+ </div>
722
+ <template #side-sheet-footer>
723
+ <div class="flex gap-8 items-center justify-start">
724
+ <FmButton
725
+ variant="primary"
726
+ :label="t('order.confirm')"
727
+ size="lg"
728
+ @click="confirmServiceChargeItems"
729
+ />
730
+ <FmButton
731
+ variant="secondary"
732
+ :label="t('order.cancel')"
733
+ size="lg"
734
+ @click="sideSheetItem = false"
735
+ />
736
+ </div>
737
+ </template>
738
+ </FmSideSheet>
739
+ </template>
740
+ </IntegratedDelivery>
741
+ </div>
742
+ </template>
743
+
744
+ <style scoped>
745
+ .tng-setting-sheet :deep(.text-fm-color-neutral-gray-400.fm-typo-cn-body-md-400.mb-5.mt-5) {
746
+ margin-top: 0;
747
+ margin-bottom: 0;
748
+ }
749
+ </style>