@feedmepos/mf-order-setting 0.0.85 → 0.0.86

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 (84) hide show
  1. package/dist/{DisplaySystemView-hmlT9gsb.js → DisplaySystemView-DjFBEWoq.js} +1 -1
  2. package/dist/{KioskDevicesView-fW6M9Hao.js → KioskDevicesView-RKMdLuB3.js} +1 -1
  3. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-GrsnYo80.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-BrG6Au5z.js} +3 -3
  4. package/dist/{KioskSettingView-BdEPftHH.js → KioskSettingView-CzelEig6.js} +3 -3
  5. package/dist/{KioskView-Bwk9S0tc.js → KioskView-Bi5Kc4cS.js} +35 -30
  6. package/dist/OrderSettingsView-BfxrjGAi.js +68958 -0
  7. package/dist/{app-BnrruYco.js → app-Bg-n-Eug.js} +8316 -7952
  8. package/dist/app.js +1 -1
  9. package/dist/common/index.d.ts +6 -7
  10. package/dist/frontend/mf-order/src/app.d.ts +24 -8
  11. package/dist/frontend/mf-order/src/main.d.ts +24 -8
  12. package/dist/frontend/mf-order/src/modules/order-setting/kiosk/interface.d.ts +2 -0
  13. package/dist/frontend/mf-order/src/stores/order-setting/index.d.ts +18 -99
  14. package/dist/frontend/mf-order/src/views/order-settings/delivery/delivery.d.ts +1 -0
  15. package/dist/frontend/mf-order/src/views/order-settings/delivery/inhouse/InHouseDelivery.vue.d.ts +3 -0
  16. package/dist/frontend/mf-order/src/views/order-settings/delivery/integrated-delivery/IntegratedDelivery.vue.d.ts +12 -1
  17. package/dist/frontend/mf-order/src/views/order-settings/delivery/integrated-delivery/IntegrationIdentityDetails.vue.d.ts +12 -0
  18. package/dist/frontend/mf-order/src/views/order-settings/pickup/PickUpSettingDialogContent.vue.d.ts +3 -0
  19. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  20. package/dist/{index-LyvBqKnv.js → index-0eYg6XEb.js} +1 -1
  21. package/dist/{index-D5wABuUx.js → index-C7Ug4fCi.js} +1 -1
  22. package/dist/{index-D7IWjZdy.js → index-g_rocKrp.js} +2 -2
  23. package/dist/order.dto-MKeJAj6d.js +39215 -0
  24. package/dist/package/entity/alert/investigation.dto.d.ts +3 -0
  25. package/dist/package/entity/alert/suggestion.dto.d.ts +3 -0
  26. package/dist/package/entity/delivery/delivery.dto.d.ts +1 -11
  27. package/dist/package/entity/incoming-order/incoming-order-to-bill.dto.d.ts +89 -51
  28. package/dist/package/entity/index.d.ts +0 -7
  29. package/dist/package/entity/kiosk/kiosk.dto.d.ts +6 -0
  30. package/dist/package/entity/kiosk/marketing/marketing.dto.d.ts +26 -0
  31. package/dist/package/entity/member/member.dto.d.ts +12 -12
  32. package/dist/package/entity/order/dine-in/dine-in.do.d.ts +9 -159
  33. package/dist/package/entity/order/dine-in/dine-in.dto.d.ts +3 -17
  34. package/dist/package/entity/order/effects/effect.dto.d.ts +32 -0
  35. package/dist/package/entity/order/menu/menu.dto.d.ts +6 -144
  36. package/dist/package/entity/order/order.do.d.ts +109 -269
  37. package/dist/package/entity/order/order.dto.d.ts +408 -782
  38. package/dist/package/entity/order/order.enum.d.ts +1 -1
  39. package/dist/package/entity/order/timeslot/timeslot.dto.d.ts +4 -4
  40. package/dist/package/entity/order-platform/foodpanda/foodpanda-order.dto.d.ts +1 -11
  41. package/dist/package/entity/order-platform/grabfood/grabfood-order.do.d.ts +1 -11
  42. package/dist/package/entity/order-platform/order-platform.dto.d.ts +278 -234
  43. package/dist/package/entity/order-platform/shopeefood/shopeefood-order.do.d.ts +1 -11
  44. package/dist/package/entity/order-platform/tng/tng-menu.do.d.ts +6 -6
  45. package/dist/package/entity/order-setting/delivery/inhouse-delivery.dto.d.ts +3 -0
  46. package/dist/package/entity/order-setting/dine-in/dine-in.dto.d.ts +65 -0
  47. package/dist/package/entity/order-setting/kiosk/kiosk.dto.d.ts +1004 -0
  48. package/dist/package/entity/order-setting/order-setting.do.d.ts +5 -337
  49. package/dist/package/entity/order-setting/order-setting.dto.d.ts +879 -678
  50. package/dist/package/entity/order-setting/pickup/pickup.dto.d.ts +3 -0
  51. package/dist/package/entity/order-setting/sequence/sequence.do.d.ts +0 -41
  52. package/dist/package/entity/order-setting/sequence/sequence.dto.d.ts +0 -43
  53. package/dist/package/entity/order-setting/sms/sms.do.d.ts +3 -3
  54. package/dist/package/entity/order-setting/sms/sms.dto.d.ts +3 -3
  55. package/dist/package/entity/queue/queue.do.d.ts +2 -2
  56. package/dist/package/entity/queue/queue.dto.d.ts +49 -65
  57. package/dist/package/entity/restaurant/restaurant.dto.d.ts +11 -0
  58. package/dist/package/entity/websocket/websocket.dto.d.ts +33 -161
  59. package/dist/package/entity/websocket/websocket.enum.d.ts +1 -1
  60. package/dist/style.css +1 -1
  61. package/package.json +2 -2
  62. package/src/locales/en-US.json +6 -2
  63. package/src/locales/ja-JP.json +6 -2
  64. package/src/locales/th-TH.json +6 -2
  65. package/src/locales/zh-CN.json +6 -2
  66. package/src/modules/order-setting/kiosk/interface.ts +12 -0
  67. package/src/stores/order-setting/mapper.spec.ts +36 -0
  68. package/src/stores/order-setting/mapper.ts +5 -0
  69. package/src/views/order-settings/delivery/DeliverySetting.vue +5 -5
  70. package/src/views/order-settings/delivery/delivery.spec.ts +34 -0
  71. package/src/views/order-settings/delivery/delivery.ts +14 -1
  72. package/src/views/order-settings/delivery/integrated-delivery/IntegratedDelivery.vue +10 -1
  73. package/src/views/order-settings/delivery/integrated-delivery/IntegrationIdentityDetails.vue +121 -22
  74. package/src/views/order-settings/delivery/integrated-delivery/LinemanSetting.vue +161 -10
  75. package/dist/OrderSettingsView-DVXfVdXj.js +0 -94831
  76. package/dist/common/booking/index.d.ts +0 -38
  77. package/dist/order.dto-D7HGiueX.js +0 -43514
  78. package/dist/package/entity/booking/booking.do.d.ts +0 -17197
  79. package/dist/package/entity/booking/booking.dto.d.ts +0 -305
  80. package/dist/package/entity/booking/booking.enum.d.ts +0 -7
  81. package/dist/package/entity/order/dine-in/reservation.do.d.ts +0 -75
  82. package/dist/package/entity/order/dine-in/reservation.dto.d.ts +0 -15
  83. package/dist/package/entity/order-setting/reservation/reservation.do.d.ts +0 -662
  84. package/dist/package/entity/order-setting/reservation/reservation.dto.d.ts +0 -527
@@ -130,8 +130,12 @@
130
130
  "integrationReferenceId": "รหัสอ้างอิงการเชื่อมต่อ",
131
131
  "platformStoreId": "รหัสร้านค้าบนแพลตฟอร์ม",
132
132
  "copy": "คัดลอก",
133
- "integrationIdCopied": "คัดลอกรหัสการเชื่อมต่อแล้ว",
134
- "integrationIdCopyFailed": "ไม่สามารถคัดลอกรหัสการเชื่อมต่อได้",
133
+ "copiedToClipboard": "คัดลอก{label}แล้ว",
134
+ "copyToClipboardFailed": "ไม่สามารถคัดลอก{label}ได้",
135
+ "restaurantId": "รหัสร้านอาหาร",
136
+ "supportSummary": "ข้อมูลสำหรับฝ่ายสนับสนุน",
137
+ "lastAcceptedSyncRequestId": "รหัสคำขอซิงค์ล่าสุดที่ได้รับการตอบรับ",
138
+ "linemanSyncDiagnosticsGuidance": "Lineman จะยืนยันผลการซิงค์เมนูแบบอะซิงโครนัส รหัสนี้เป็นของคำขอซิงค์ครั้งล่าสุดที่ Lineman ตอบรับ หากการซิงค์ครั้งล่าสุดถูกปฏิเสธทันที ข้อความแสดงข้อผิดพลาดด้านล่างจะเป็นของครั้งนั้น ขณะที่รหัสยังคงอ้างถึงครั้งก่อนที่ได้รับการตอบรับ กรุณาคัดลอกทั้งสองอย่างแนบไปกับเรื่องที่แจ้งฝ่ายสนับสนุน",
135
139
  "primaryStore": "ร้านค้าหลัก",
136
140
  "removeStore": "ลบ",
137
141
  "removeStoreConfirm": "ลบร้าน '{merchantID}' ใช่หรือไม่?",
@@ -130,8 +130,12 @@
130
130
  "integrationReferenceId": "集成参考 ID",
131
131
  "platformStoreId": "平台门店 ID",
132
132
  "copy": "复制",
133
- "integrationIdCopied": "集成 ID 已复制",
134
- "integrationIdCopyFailed": "无法复制集成 ID",
133
+ "copiedToClipboard": "已复制{label}",
134
+ "copyToClipboardFailed": "无法复制{label}",
135
+ "restaurantId": "餐厅 ID",
136
+ "supportSummary": "支持信息",
137
+ "lastAcceptedSyncRequestId": "最近一次受理的同步请求 ID",
138
+ "linemanSyncDiagnosticsGuidance": "Lineman 会以异步方式确认菜单同步结果。此 ID 属于 Lineman 最近一次受理的同步请求。如果最新一次尝试被直接拒绝,下方的错误信息属于该次尝试,而此 ID 仍指向此前受理的那一次。请将两者一并复制到支持工单中。",
135
139
  "primaryStore": "主门店",
136
140
  "removeStore": "删除",
137
141
  "removeStoreConfirm": "确定删除门店「{merchantID}」吗?",
@@ -94,6 +94,12 @@ export interface MfKioskOrderSetting {
94
94
  autoSendKitchen: boolean | null,
95
95
  autoCloseBill: boolean | null,
96
96
  printReceiptAfterClose: boolean | null,
97
+ // Whether POS may reject a kiosk order (KDI-5089). No form control edits it
98
+ // — ops set it — but it MUST be carried here anyway: the mapper rebuilds
99
+ // kioskSettings field by field and the backend $sets the whole
100
+ // subdocument, so a field the mapper drops is deleted on the next save of
101
+ // an unrelated kiosk field. That is how prod MY lost it on 2026-09-22.
102
+ allowReject: boolean | null,
97
103
  // Locales the kiosk may offer the customer. null or [] = no restriction,
98
104
  // i.e. the kiosk shows every language it ships. Anything else is the exact
99
105
  // allowed set.
@@ -122,6 +128,12 @@ export interface MfKioskOrderSettingForm {
122
128
  autoSendKitchen: boolean | null,
123
129
  autoCloseBill: boolean | null,
124
130
  printReceiptAfterClose: boolean | null,
131
+ // Whether POS may reject a kiosk order (KDI-5089). No form control edits it
132
+ // — ops set it — but it MUST be carried here anyway: the mapper rebuilds
133
+ // kioskSettings field by field and the backend $sets the whole
134
+ // subdocument, so a field the mapper drops is deleted on the next save of
135
+ // an unrelated kiosk field. That is how prod MY lost it on 2026-09-22.
136
+ allowReject: boolean | null,
125
137
  // Locales the kiosk may offer the customer. null or [] = no restriction,
126
138
  // i.e. the kiosk shows every language it ships. Anything else is the exact
127
139
  // allowed set.
@@ -68,3 +68,39 @@ describe('kiosk cover carousel round-trip', () => {
68
68
  expect(saved(form).coverCarousel).toBeNull()
69
69
  })
70
70
  })
71
+
72
+ describe('kiosk allowReject round-trip (KDI-5089)', () => {
73
+ // The regression this guards, found on prod MY 2026-09-22: no form control
74
+ // edits allowReject — ops set it — so the mapper has to carry it purely to
75
+ // hand it back. While it was missing, every portal kiosk save rebuilt
76
+ // kioskSettings without it and the backend's whole-subdocument $set deleted
77
+ // it, which turns the POS reject button back on for kiosk orders.
78
+ it('reads a stored allowReject: false back onto the form', () => {
79
+ expect(toKioskForm({ allowReject: false }).allowReject).toBe(false)
80
+ })
81
+
82
+ it('reads a stored allowReject: true back onto the form', () => {
83
+ expect(toKioskForm({ allowReject: true }).allowReject).toBe(true)
84
+ })
85
+
86
+ // null, not false: unset means "inherit dineInSettings", and collapsing it to
87
+ // a boolean here is what would pin the value and kill that inheritance.
88
+ it('reads an unset allowReject as null, not false', () => {
89
+ expect(toKioskForm({}).allowReject).toBeNull()
90
+ })
91
+
92
+ it('saves the merchant choice back', () => {
93
+ expect(saved({ ...defaultKioskOrderSetting, allowReject: false }).allowReject).toBe(false)
94
+ })
95
+
96
+ it('omits the field when unset, so the backend keeps inheriting', () => {
97
+ expect(saved({ ...defaultKioskOrderSetting, allowReject: null }).allowReject).toBeUndefined()
98
+ })
99
+
100
+ it('survives a full read-then-save round trip of an unrelated edit', () => {
101
+ const form = toKioskForm({ allowReject: false, canTakeaway: true })
102
+ // The merchant toggles something else entirely.
103
+ form.takeaway.enabled = false
104
+ expect(saved(form).allowReject).toBe(false)
105
+ })
106
+ })
@@ -84,6 +84,7 @@ export const defaultKioskOrderSetting: MfKioskOrderSettingForm = {
84
84
  autoSendKitchen: null,
85
85
  autoCloseBill: null,
86
86
  printReceiptAfterClose: null,
87
+ allowReject: null,
87
88
  languages: null
88
89
  }
89
90
 
@@ -154,6 +155,9 @@ const toKiosk = (
154
155
  autoSendKitchen: kioskOrderSettings?.autoSendKitchen ?? null,
155
156
  autoCloseBill: kioskOrderSettings?.autoCloseBill ?? null,
156
157
  printReceiptAfterClose: kioskOrderSettings?.printReceiptAfterClose ?? null,
158
+ // Read back purely so the write side can hand it straight back. Nothing in
159
+ // the form touches it; dropping it here deletes it on the next save.
160
+ allowReject: kioskOrderSettings?.allowReject ?? null,
157
161
  // Stored [] means the same thing as unset ("all languages"), so collapse it
158
162
  // to null here — the form only ever has to reason about null vs a real set.
159
163
  languages: kioskOrderSettings?.languages?.length ? [...kioskOrderSettings.languages] : null
@@ -255,6 +259,7 @@ const toOrderKioskSettingsDto = (
255
259
  autoSendKitchen: kioskSetting.autoSendKitchen ?? undefined,
256
260
  autoCloseBill: kioskSetting.autoCloseBill ?? undefined,
257
261
  printReceiptAfterClose: kioskSetting.printReceiptAfterClose ?? undefined,
262
+ allowReject: kioskSetting.allowReject ?? undefined,
258
263
  // Omit rather than send [] when the merchant restricts nothing, so the
259
264
  // saved document stays free of a field that means "all languages" anyway.
260
265
  languages: kioskSetting.languages?.length ? kioskSetting.languages : undefined,
@@ -216,12 +216,13 @@ import deliveryData from './delivery.data'
216
216
  import {
217
217
  getInvalidExternalMenuCatalogOrderTypes,
218
218
  useDelivery,
219
+ isHiddenDeliveryKey,
220
+ AUTO_INTEGRATION_CLIENT,
219
221
  type DeliveryCompany,
220
222
  type LinkedDeliveryDoc
221
223
  } from './delivery'
222
224
  import { useLoading } from '@/composables/loading'
223
225
  import { remoteOrderApi } from '@/api/remoteOrder'
224
- import { AUTO_INTEGRATION_CLIENT } from '../delivery/delivery'
225
226
  import { useMenuStore } from '@/stores/menu/menu'
226
227
  import ManualIntegratedDeliverySetting from './components/ManualIntegratedDeliverySetting.vue'
227
228
  import { insertOrUpdate, remove, sort } from '@/helpers/iteration'
@@ -850,14 +851,13 @@ const integratedCompanies = computed<IntegratedDeliveryRow[]>(() => {
850
851
  ]
851
852
 
852
853
  return [
853
- // FeedMe's own online-ordering-dependent delivery (In-house + FeedMe Express)
854
- // is part of "FeedMe online ordering", so it stays hidden when QR ordering is
855
- // off (e.g. Go plan). Third-party delivery integrations remain available.
854
+ // In-house delivery is part of "FeedMe online ordering", so it stays hidden when
855
+ // QR ordering is off (e.g. Go plan). Third-party delivery integrations remain available.
856
856
  ...(qrEnabled.value ? feedmeDelivery : []),
857
857
  ...integratedDelivery.filter((delivery) => {
858
858
  return isIntegratedPlatformSupported(delivery.key, currentCountry.value)
859
859
  })
860
- ]
860
+ ].filter((row) => !isHiddenDeliveryKey(row.key))
861
861
  })
862
862
 
863
863
  const loadingKey = computed(() => {
@@ -0,0 +1,34 @@
1
+ import { F_DELIVERY_TYPE } from '@feedmepos/core/entity'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { AUTO_INTEGRATION_CLIENT, isHiddenDeliveryKey } from './delivery'
4
+
5
+ // delivery.ts pulls in @feedmepos/mf-common for useCoreStore, whose dist bundle
6
+ // crashes under vitest; isHiddenDeliveryKey never touches it, so stub it out.
7
+ vi.mock('@feedmepos/mf-common', () => ({ useCoreStore: () => ({}) }))
8
+
9
+ describe('isHiddenDeliveryKey', () => {
10
+ it('hides the TNG integration (pending TNGD delivery/pickup support)', () => {
11
+ expect(isHiddenDeliveryKey(AUTO_INTEGRATION_CLIENT.tng)).toBe(true)
12
+ })
13
+
14
+ it('hides FeedMe Express (service ended 1 Feb 2026)', () => {
15
+ expect(isHiddenDeliveryKey(F_DELIVERY_TYPE.enum.FEEDME)).toBe(true)
16
+ })
17
+
18
+ it('keeps every other delivery row visible', () => {
19
+ const stillShown = [
20
+ AUTO_INTEGRATION_CLIENT.foodpanda,
21
+ AUTO_INTEGRATION_CLIENT.grabfood,
22
+ AUTO_INTEGRATION_CLIENT.lineman,
23
+ AUTO_INTEGRATION_CLIENT.shopeefood,
24
+ AUTO_INTEGRATION_CLIENT.gofood,
25
+ AUTO_INTEGRATION_CLIENT.deliveroo,
26
+ AUTO_INTEGRATION_CLIENT.external,
27
+ F_DELIVERY_TYPE.enum.IN_HOUSE
28
+ ]
29
+
30
+ for (const key of stillShown) {
31
+ expect(isHiddenDeliveryKey(key)).toBe(false)
32
+ }
33
+ })
34
+ })
@@ -1,4 +1,9 @@
1
- import { F_PAYMENT_NAME, FdoLinkedDelivery, F_COUNTRY } from '@feedmepos/core/entity'
1
+ import {
2
+ F_PAYMENT_NAME,
3
+ FdoLinkedDelivery,
4
+ F_COUNTRY,
5
+ F_DELIVERY_TYPE
6
+ } from '@feedmepos/core/entity'
2
7
  import { useCoreStore } from '@feedmepos/mf-common'
3
8
  import { F_ORDER_PLATFORM, type FdoExternalSetting } from '@entity'
4
9
 
@@ -29,6 +34,10 @@ export type ExternalMenuOrderTypeWithRequiredCatalog = Extract<
29
34
  'delivery' | 'pickup'
30
35
  >
31
36
 
37
+ // Pending TNGD delivery/pickup support on the Near Me SPI.
38
+ const TNG_PENDING_DELIVERY_KEYS: string[] = [AUTO_INTEGRATION_CLIENT.tng]
39
+ const DISCONTINUED_DELIVERY_KEYS: string[] = [F_DELIVERY_TYPE.enum.FEEDME]
40
+
32
41
  export function externalMenuOrderTypeRequiresCatalog(
33
42
  platform: FdoExternalSetting['platform'],
34
43
  orderType: ExternalMenuOrderType
@@ -59,6 +68,10 @@ export function getInvalidExternalMenuCatalogOrderTypes(
59
68
  })
60
69
  }
61
70
 
71
+ export function isHiddenDeliveryKey(key: string): boolean {
72
+ return TNG_PENDING_DELIVERY_KEYS.includes(key) || DISCONTINUED_DELIVERY_KEYS.includes(key)
73
+ }
74
+
62
75
  function formatDeliveryCompanyKey(key: string) {
63
76
  return key.toLowerCase().replace('_', '').toLowerCase()
64
77
  }
@@ -54,6 +54,14 @@ const props = defineProps({
54
54
  type: Boolean,
55
55
  default: false
56
56
  },
57
+ // Lineman renders the same error inside the diagnostics card below, where it
58
+ // is copyable. Rendering it here as well would show support the same string
59
+ // twice, so that panel suppresses this tooltip and takes the formatted value
60
+ // through the `integration-details` slot instead.
61
+ hideMenuSyncErrorTooltip: {
62
+ type: Boolean,
63
+ default: false
64
+ },
57
65
  showTestOrderActions: {
58
66
  type: Boolean,
59
67
  default: true
@@ -346,7 +354,7 @@ const formattedMenuSyncError = computed(() => {
346
354
  <slot name="order-setting" :editable="!canEdit" />
347
355
  </div>
348
356
 
349
- <slot name="integration-details" />
357
+ <slot name="integration-details" :menu-sync-error="formattedMenuSyncError" />
350
358
 
351
359
  <!-- Commision rate not used yet -->
352
360
  <!-- <div class="mb-5">
@@ -386,6 +394,7 @@ const formattedMenuSyncError = computed(() => {
386
394
  </div>
387
395
  <FmTooltip
388
396
  v-if="
397
+ !hideMenuSyncErrorTooltip &&
389
398
  (modelValue.auto.status === 'FAILED' || alwaysShowMenuSyncError) &&
390
399
  formattedMenuSyncError !== null
391
400
  "
@@ -3,6 +3,11 @@ import { computed } from 'vue'
3
3
  import { useI18n } from '@feedmepos/mf-common'
4
4
  import { useSnackbarFunctions } from '@/components/snackbar'
5
5
 
6
+ interface SupportSummaryRow {
7
+ label: string
8
+ value?: string | null
9
+ }
10
+
6
11
  const props = defineProps<{
7
12
  integrationId?: string | null
8
13
  partnerId?: string | null
@@ -10,68 +15,135 @@ const props = defineProps<{
10
15
  providerId?: string | null
11
16
  providerLabel?: string
12
17
  storeLabel?: string
18
+ // Drops the id rows and keeps only the support diagnostics below. For
19
+ // Lineman there is no FeedMe-side integration identity worth showing — it
20
+ // reuses the restaurant id — so the rows would only be noise.
21
+ hideIdentity?: boolean
22
+ // Overrides the tooltip next to the title. The default text describes
23
+ // GrabFood/Foodpanda/ShopeeFood multi-store ids; platforms whose FeedMe
24
+ // identity works differently pass their own.
25
+ guidance?: string
26
+ // Support-facing extras: the id the platform gave us for the last async menu
27
+ // sync, and whatever message came back with it. Neither carries its own copy
28
+ // button - both are lines in `supportSummary`, and a per-row button would
29
+ // only offer support a worse paste than the block below.
30
+ referenceId?: string | null
31
+ referenceLabel?: string
32
+ message?: string | null
33
+ messageLabel?: string
34
+ // The backend nulls `message` out when a sync starts and again when it
35
+ // succeeds, so hiding the row leaves support unable to tell "no failure" from
36
+ // "not wired up". Callers that want the state either way render a dash.
37
+ showEmptyMessage?: boolean
38
+ // Rows for the one-press support block. The card cannot build this itself -
39
+ // platform, restaurant and status live on the caller - so it takes the lines
40
+ // already composed and only owns the formatting. Empty values are dropped so
41
+ // a half-populated record still yields a clean paste.
42
+ supportSummary?: SupportSummaryRow[]
13
43
  }>()
14
44
 
15
45
  const { t } = useI18n()
16
46
  const { showSuccess, showError } = useSnackbarFunctions()
17
47
 
18
48
  const visiblePartnerId = computed(() => props.partnerId || null)
49
+ const guidanceText = computed(() => props.guidance || t('order.integrationIdGuidance'))
50
+ const showIdentity = computed(() => !props.hideIdentity)
51
+ const showCard = computed(
52
+ () =>
53
+ (showIdentity.value &&
54
+ Boolean(props.integrationId || props.partnerId || props.providerId || props.storeLabel)) ||
55
+ Boolean(props.referenceId || props.message || props.showEmptyMessage)
56
+ )
57
+
58
+ // The record is destructive - the next sync nulls the error and replaces the
59
+ // request id - so this block is often the only surviving evidence of a failure
60
+ // once support has pasted it into the ticket.
61
+ const supportSummaryText = computed(() =>
62
+ (props.supportSummary ?? [])
63
+ .filter((row) => row.value)
64
+ .map((row) => `${row.label}: ${row.value}`)
65
+ .join('\n')
66
+ )
19
67
 
20
- async function copyIntegrationId() {
21
- if (!props.integrationId) return
68
+ async function copySupportSummary() {
69
+ await copyValue(supportSummaryText.value, t('order.supportSummary'))
70
+ }
71
+
72
+ async function copyValue(value: string | null | undefined, label: string) {
73
+ if (!value) return
22
74
  try {
23
- await navigator.clipboard.writeText(props.integrationId)
24
- showSuccess(t('order.integrationIdCopied'))
75
+ await navigator.clipboard.writeText(value)
76
+ showSuccess(t('order.copiedToClipboard', { label }))
25
77
  } catch {
26
- showError(t('order.integrationIdCopyFailed'))
78
+ showError(t('order.copyToClipboardFailed', { label }))
27
79
  }
28
80
  }
29
81
  </script>
30
82
 
31
83
  <template>
32
84
  <div
33
- v-if="integrationId || partnerId || providerId || storeLabel"
85
+ v-if="showCard"
34
86
  class="integration-details"
35
87
  :class="{ 'integration-details--owned': storeLabel }"
36
88
  >
37
89
  <div class="integration-details__header">
38
90
  <div class="integration-details__title">
39
91
  <div class="fm-typo-en-title-sm-600">{{ t('order.integrationDetails') }}</div>
40
- <FmTooltip :content="t('order.integrationIdGuidance')" :z-index="1000">
41
- <FmIcon
42
- name="info"
43
- size="sm"
44
- class="cursor-help text-fm-color-neutral-gray-400"
45
- />
92
+ <FmTooltip :content="guidanceText" :z-index="1000">
93
+ <FmIcon name="info" size="sm" class="cursor-help text-fm-color-neutral-gray-400" />
46
94
  </FmTooltip>
47
95
  </div>
48
96
  <span v-if="storeLabel" class="integration-details__store-label fm-typo-en-body-sm-600">
49
97
  {{ storeLabel }}
50
98
  </span>
51
99
  </div>
52
- <div class="integration-details__row fm-typo-cn-body-md-400">
100
+ <div v-if="showIdentity" class="integration-details__row fm-typo-cn-body-md-400">
53
101
  <span class="text-fm-color-neutral-gray-400">{{ t('order.feedmeIntegrationId') }}</span>
54
102
  <div class="integration-details__value">
55
103
  <code>{{ integrationId || t('order.integrationIdNotAssigned') }}</code>
56
- <FmButton
57
- v-if="integrationId"
58
- variant="tertiary"
59
- size="md"
60
- :label="t('order.copy')"
61
- @click="copyIntegrationId"
62
- />
104
+ <span v-if="integrationId" class="integration-details__action">
105
+ <FmButton
106
+ variant="tertiary"
107
+ size="md"
108
+ :label="t('order.copy')"
109
+ @click="copyValue(integrationId, t('order.feedmeIntegrationId'))"
110
+ />
111
+ </span>
63
112
  </div>
64
113
  </div>
65
- <div v-if="visiblePartnerId" class="integration-details__row fm-typo-cn-body-md-400">
114
+ <div
115
+ v-if="showIdentity && visiblePartnerId"
116
+ class="integration-details__row fm-typo-cn-body-md-400"
117
+ >
66
118
  <span class="text-fm-color-neutral-gray-400">{{ partnerLabel || t('order.partnerId') }}</span>
67
119
  <code>{{ visiblePartnerId }}</code>
68
120
  </div>
69
- <div v-if="providerId" class="integration-details__row fm-typo-cn-body-md-400">
121
+ <div v-if="showIdentity && providerId" class="integration-details__row fm-typo-cn-body-md-400">
70
122
  <span class="text-fm-color-neutral-gray-400">{{
71
123
  providerLabel || t('order.platformStoreId')
72
124
  }}</span>
73
125
  <code>{{ providerId }}</code>
74
126
  </div>
127
+ <div v-if="referenceId" class="integration-details__row fm-typo-cn-body-md-400">
128
+ <span class="text-fm-color-neutral-gray-400">{{
129
+ referenceLabel || t('order.integrationReferenceId')
130
+ }}</span>
131
+ <code>{{ referenceId }}</code>
132
+ </div>
133
+ <div v-if="message || showEmptyMessage" class="integration-details__row fm-typo-cn-body-md-400">
134
+ <span class="text-fm-color-neutral-gray-400">{{
135
+ messageLabel || t('order.menuSyncError')
136
+ }}</span>
137
+ <code class="integration-details__message">{{ message || '-' }}</code>
138
+ </div>
139
+ <div v-if="supportSummaryText" class="integration-details__summary">
140
+ <FmButton
141
+ variant="secondary"
142
+ size="md"
143
+ :label="t('order.copy')"
144
+ @click="copySupportSummary"
145
+ />
146
+ </div>
75
147
  </div>
76
148
  </template>
77
149
 
@@ -126,6 +198,33 @@ async function copyIntegrationId() {
126
198
  gap: 0.5rem;
127
199
  }
128
200
 
201
+ /* FmButton's root is a flex box with `overflow: hidden` and no
202
+ `flex-shrink: 0`, so an opaque single-token value would squeeze it until its
203
+ own label was clipped. Pin the button at its natural width and make the
204
+ value the only thing that gives. */
205
+ .integration-details__action {
206
+ display: inline-flex;
207
+ flex: 0 0 auto;
208
+ }
209
+
210
+ .integration-details__value > code {
211
+ flex: 1 1 auto;
212
+ }
213
+
214
+ .integration-details__summary {
215
+ display: flex;
216
+ justify-content: flex-end;
217
+ margin-top: 0.25rem;
218
+ }
219
+
220
+ /* A sync message can be a long JSON blob: keep it in its own column like every
221
+ other value and let it scroll rather than stretch the card. */
222
+ .integration-details__message {
223
+ max-height: 9rem;
224
+ overflow-y: auto;
225
+ white-space: pre-wrap;
226
+ }
227
+
129
228
  code {
130
229
  min-width: 0;
131
230
  overflow-wrap: anywhere;