@feedmepos/mf-order-setting 0.0.92 → 0.0.94

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 (65) hide show
  1. package/dist/DisplaySystemView-Doh1JUoe.js +8891 -0
  2. package/dist/{KioskDevicesView-d2WLB0-w.js → KioskDevicesView-DvUq9eFa.js} +1 -1
  3. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-rs-hNWUW.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-Ce-CHsOK.js} +3 -3
  4. package/dist/{KioskSettingView-DFE3_u5c.js → KioskSettingView-Ba1sLolg.js} +3 -3
  5. package/dist/{KioskView-C3Pi3lWW.js → KioskView-CeKSWYbI.js} +5 -5
  6. package/dist/{OrderSettingsView-Dxg9RjSd.js → OrderSettingsView-CdxkPS_V.js} +22200 -22918
  7. package/dist/{app-BFlQtcsd.js → app-BQ0W0_dt.js} +2536 -1818
  8. package/dist/app.js +1 -1
  9. package/dist/featureFlag-C4gAe_F4.js +846 -0
  10. package/dist/frontend/mf-order/src/api/brandkit/index.d.ts +3 -0
  11. package/dist/frontend/mf-order/src/api/native-app/index.d.ts +2 -0
  12. package/dist/frontend/mf-order/src/app.d.ts +716 -0
  13. package/dist/frontend/mf-order/src/composables/featureFlag.d.ts +7 -0
  14. package/dist/frontend/mf-order/src/composables/useBusinessFeatureFlag.d.ts +4 -0
  15. package/dist/frontend/mf-order/src/composables/useLegacyOdsAccess.d.ts +4 -0
  16. package/dist/frontend/mf-order/src/composables/useMiniProgramPermission.d.ts +5 -0
  17. package/dist/frontend/mf-order/src/main.d.ts +716 -0
  18. package/dist/frontend/mf-order/src/modules/display-system/legacy-ods/interface.d.ts +80 -0
  19. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorOverrideDialog.vue.d.ts +26 -0
  20. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorSelection.vue.d.ts +15 -0
  21. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue.d.ts +24 -0
  22. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue.d.ts +4 -0
  23. package/dist/frontend/mf-order/src/views/display-system/view-selection.d.ts +18 -0
  24. package/dist/frontend/mf-order/src/views/order-settings/components/push-availability.d.ts +13 -0
  25. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  26. package/dist/{index-ByiTrmdq.js → index-BFJ4YEfL.js} +2 -2
  27. package/dist/{index-CaO7aaaT.js → index-CGiJXVU3.js} +1 -1
  28. package/dist/{index-s2kWGMtz.js → index-DSB009Br.js} +1 -1
  29. package/dist/{order.dto-C829lA69.js → order.dto-CUp8zhQC.js} +18 -18
  30. package/dist/package/entity/order-platform/tng/tng-menu.do.d.ts +74 -74
  31. package/dist/style.css +1 -1
  32. package/env.d.ts +2 -0
  33. package/package.json +2 -1
  34. package/src/api/brandkit/index.ts +24 -0
  35. package/src/api/native-app/index.ts +26 -0
  36. package/src/api/native-app/native-app.spec.ts +42 -0
  37. package/src/composables/featureFlag.ts +22 -0
  38. package/src/composables/useBusinessFeatureFlag.ts +44 -0
  39. package/src/composables/useLegacyOdsAccess.ts +35 -0
  40. package/src/composables/useMiniProgramPermission.ts +19 -0
  41. package/src/composables/useQrOrderingEnabled.ts +1 -21
  42. package/src/locales/en-US.json +179 -0
  43. package/src/locales/ja-JP.json +179 -0
  44. package/src/locales/th-TH.json +179 -0
  45. package/src/locales/zh-CN.json +179 -0
  46. package/src/modules/display-system/legacy-ods/interface.ts +108 -0
  47. package/src/router/routes.ts +1 -1
  48. package/src/views/display-system/DisplaySystemView.vue +82 -11
  49. package/src/views/display-system/legacy-ods/ColorOverrideDialog.vue +223 -0
  50. package/src/views/display-system/legacy-ods/ColorSelection.vue +67 -0
  51. package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.spec.ts +185 -0
  52. package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue +1677 -0
  53. package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.spec.ts +124 -0
  54. package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue +297 -0
  55. package/src/views/display-system/legacy-ods/brandkit.spec.ts +67 -0
  56. package/src/views/display-system/view-selection.spec.ts +93 -0
  57. package/src/views/display-system/view-selection.ts +54 -0
  58. package/src/views/order-settings/components/NotifyChannelEditor.spec.ts +184 -0
  59. package/src/views/order-settings/components/NotifyChannelEditor.vue +84 -5
  60. package/src/views/order-settings/components/push-availability.spec.ts +59 -0
  61. package/src/views/order-settings/components/push-availability.ts +78 -0
  62. package/src/views/order-settings/queue/QueueSetting.vue +7 -0
  63. package/src/views/order-settings/sms/SmsSetting.vue +11 -0
  64. package/dist/DisplaySystemView-DZS7EVfk.js +0 -6564
  65. package/dist/_plugin-vue_export-helper-CHgC5LLL.js +0 -9
@@ -0,0 +1,1677 @@
1
+ <script setup lang="ts">
2
+ import { computed, onUnmounted, ref, watch } from 'vue'
3
+ import { RestaurantSelector, useCoreStore, useI18n } from '@feedmepos/mf-common'
4
+ import { MediaPickerModal, type MediaSelection } from '@feedmepos/media-picker'
5
+ import ColorOverrideDialog from './ColorOverrideDialog.vue'
6
+ import { useLegacyOdsAccess } from '@/composables/useLegacyOdsAccess'
7
+ import {
8
+ type BaseBrandKit,
9
+ type BrandKit,
10
+ type OdsConfigEntry,
11
+ type OdsDisplayConfig,
12
+ type OdsFooterMode,
13
+ type OdsLayoutConfig,
14
+ type OrderGroupingMode,
15
+ type OrderStatusBoardLayout,
16
+ type RecursivePartial,
17
+ type StatusSectionTemplateName
18
+ } from '@/modules/display-system/legacy-ods/interface'
19
+
20
+ interface ButtonGroupItemWithValue<T = string> {
21
+ label: string
22
+ value: T
23
+ icon?: string | null
24
+ disabled?: boolean
25
+ }
26
+
27
+ const props = defineProps<{
28
+ modelValue: boolean
29
+ entry: OdsConfigEntry
30
+ editingIndex: number
31
+ allEntries: OdsConfigEntry[]
32
+ brandkit: BrandKit
33
+ colorFields: { key: string; title: string; description: string }[]
34
+ saving?: boolean
35
+ }>()
36
+
37
+ const emit = defineEmits<{
38
+ (e: 'update:modelValue', v: boolean): void
39
+ (e: 'save', entry: OdsConfigEntry): void
40
+ (e: 'cancel'): void
41
+ }>()
42
+
43
+ const Core = useCoreStore()
44
+ const { t } = useI18n()
45
+
46
+ const { canAccessOdsTemplateSettings, isAdvancedCustomizationEnabled } = useLegacyOdsAccess()
47
+
48
+ const show = computed({
49
+ get: () => props.modelValue,
50
+ set: (v) => emit('update:modelValue', v)
51
+ })
52
+
53
+ type OdsPresetKey = 'preset1' | 'preset2'
54
+
55
+ const draft = ref<OdsConfigEntry>({ isDefault: false })
56
+
57
+ const cloneEntry = (): OdsConfigEntry => JSON.parse(JSON.stringify(props.entry))
58
+
59
+ const syncDraft = () => {
60
+ const entry = cloneEntry()
61
+ if (!entry.display) entry.display = createDefaultOdsDisplay()
62
+ if (!entry.layout) {
63
+ entry.layout = JSON.parse(JSON.stringify(ODS_PRESETS[0].layout))
64
+ }
65
+ draft.value = entry
66
+ }
67
+
68
+ watch(
69
+ () => props.modelValue,
70
+ (v) => {
71
+ if (v) syncDraft()
72
+ }
73
+ )
74
+
75
+ const anotherDefaultExists = computed(() =>
76
+ props.allEntries.some((e, i) => e.isDefault && i !== props.editingIndex)
77
+ )
78
+
79
+ const selectedRestaurantIds = computed({
80
+ get: () => draft.value.restaurantIds ?? [],
81
+ set: (v: string[] | null | undefined) => {
82
+ draft.value.restaurantIds = v ?? []
83
+ }
84
+ })
85
+
86
+ const DEFAULT_CURRENT_ORDER = {
87
+ enabled: true
88
+ } satisfies NonNullable<OdsDisplayConfig['currentOrder']>
89
+
90
+ const DEFAULT_ODS_DISPLAY: OdsDisplayConfig = {
91
+ groupBy: 'type',
92
+ statusLayout: 'horizontal',
93
+ statusSlidingPeriod: 10000,
94
+ currentOrder: { ...DEFAULT_CURRENT_ORDER },
95
+ showProgressBar: false,
96
+ statusColumnCount: 1,
97
+ showDeliveryIcons: true,
98
+ showOrderTypeTabs: false,
99
+ showOrderCount: false,
100
+ showEstimatedTime: false,
101
+ hideCompletedOrders: false,
102
+ statusTemplate: 'default'
103
+ }
104
+
105
+ const createDefaultOdsDisplay = (): OdsDisplayConfig => ({
106
+ ...DEFAULT_ODS_DISPLAY,
107
+ currentOrder: { ...DEFAULT_CURRENT_ORDER }
108
+ })
109
+
110
+ const DEFAULT_ODS_LAYOUT: OdsLayoutConfig = {
111
+ background: {
112
+ url: '/images/feedme-bg.png',
113
+ size: 'cover',
114
+ position: 'center'
115
+ },
116
+ sidePanel: null,
117
+ boardWidthPercent: 100,
118
+ boardAlign: 'right',
119
+ footerMode: 'feedme'
120
+ }
121
+
122
+ const ensureDisplay = () => {
123
+ if (!draft.value.display) {
124
+ draft.value.display = createDefaultOdsDisplay()
125
+ }
126
+ }
127
+
128
+ const resolveDisplay = (display?: OdsDisplayConfig): OdsDisplayConfig => ({
129
+ ...createDefaultOdsDisplay(),
130
+ ...display,
131
+ currentOrder: {
132
+ ...DEFAULT_CURRENT_ORDER,
133
+ ...display?.currentOrder
134
+ }
135
+ })
136
+
137
+ const odsDisplay = computed<OdsDisplayConfig>(() => resolveDisplay(draft.value.display))
138
+
139
+ const updateDisplay = <K extends keyof OdsDisplayConfig>(key: K, value: OdsDisplayConfig[K]) => {
140
+ if (!canAccessOdsTemplateSettings.value) return
141
+ ensureDisplay()
142
+ draft.value.display![key] = value
143
+ }
144
+
145
+ const updateCurrentOrderEnabled = (enabled: boolean) => {
146
+ if (!canAccessOdsTemplateSettings.value) return
147
+ ensureDisplay()
148
+ draft.value.display!.currentOrder = {
149
+ ...(draft.value.display!.currentOrder ?? {}),
150
+ enabled
151
+ }
152
+ }
153
+
154
+ const updateHideCompletedOrders = (hide: boolean) => {
155
+ ensureDisplay()
156
+ draft.value.display!.hideCompletedOrders = hide
157
+ }
158
+
159
+ const resetDisplay = () => {
160
+ if (!canAccessOdsTemplateSettings.value) return
161
+ draft.value.display = {
162
+ ...createDefaultOdsDisplay(),
163
+ hideCompletedOrders: odsDisplay.value.hideCompletedOrders
164
+ }
165
+ }
166
+
167
+ const ensureLayout = () => {
168
+ if (!draft.value.layout) {
169
+ draft.value.layout = {}
170
+ }
171
+ }
172
+
173
+ const resolveLayout = (layout?: OdsLayoutConfig): OdsLayoutConfig => {
174
+ if (!layout) return DEFAULT_ODS_LAYOUT
175
+ return {
176
+ ...DEFAULT_ODS_LAYOUT,
177
+ ...layout,
178
+ background: layout.background === undefined ? DEFAULT_ODS_LAYOUT.background : layout.background,
179
+ sidePanel: layout.sidePanel === undefined ? DEFAULT_ODS_LAYOUT.sidePanel : layout.sidePanel
180
+ }
181
+ }
182
+
183
+ const odsLayout = computed<OdsLayoutConfig>(() => resolveLayout(draft.value.layout))
184
+
185
+ const updateLayoutField = <K extends keyof OdsLayoutConfig>(key: K, value: OdsLayoutConfig[K]) => {
186
+ if (!canAccessOdsTemplateSettings.value) return
187
+ ensureLayout()
188
+ draft.value.layout![key] = value
189
+ }
190
+
191
+ const resetLayout = () => {
192
+ if (!canAccessOdsTemplateSettings.value) return
193
+ draft.value.layout = undefined
194
+ syncBackgroundSource()
195
+ syncSidePanelSource()
196
+ }
197
+
198
+ type OdsPreset = {
199
+ key: OdsPresetKey
200
+ labelKey: string
201
+ layout: OdsLayoutConfig
202
+ display: OdsDisplayConfig
203
+ }
204
+
205
+ const ODS_PRESETS: OdsPreset[] = [
206
+ {
207
+ key: 'preset1',
208
+ labelKey: 'order.displaySystem.legacyOds.preset1',
209
+ layout: {
210
+ background: {
211
+ url: '/images/feedme-bg.png',
212
+ size: 'cover',
213
+ position: 'center'
214
+ },
215
+ sidePanel: null,
216
+ boardWidthPercent: 100,
217
+ boardAlign: 'right'
218
+ },
219
+ display: {
220
+ groupBy: 'type',
221
+ statusLayout: 'horizontal',
222
+ statusSlidingPeriod: 10000,
223
+ showProgressBar: false,
224
+ statusColumnCount: 1,
225
+ showDeliveryIcons: true,
226
+ showOrderTypeTabs: false,
227
+ showOrderCount: false,
228
+ showEstimatedTime: false,
229
+ hideCompletedOrders: false,
230
+ statusTemplate: 'default',
231
+ currentOrder: {
232
+ enabled: true
233
+ }
234
+ }
235
+ },
236
+ {
237
+ key: 'preset2',
238
+ labelKey: 'order.displaySystem.legacyOds.preset2',
239
+ layout: {
240
+ background: null,
241
+ sidePanel: {
242
+ url: '/images/feedme-side.png',
243
+ position: 'left',
244
+ widthPercent: 33
245
+ },
246
+ boardWidthPercent: 100,
247
+ boardAlign: 'right'
248
+ },
249
+ display: {
250
+ groupBy: 'status',
251
+ statusLayout: 'horizontal',
252
+ statusSlidingPeriod: 10000,
253
+ showProgressBar: true,
254
+ statusColumnCount: 1,
255
+ showDeliveryIcons: true,
256
+ showOrderTypeTabs: false,
257
+ showOrderCount: false,
258
+ showEstimatedTime: false,
259
+ hideCompletedOrders: false,
260
+ statusTemplate: 'default',
261
+ currentOrder: {
262
+ enabled: true
263
+ }
264
+ }
265
+ }
266
+ ]
267
+
268
+ const presetItems = computed<ButtonGroupItemWithValue<OdsPresetKey>[]>(() =>
269
+ ODS_PRESETS.map((preset) => ({
270
+ label: t(preset.labelKey),
271
+ value: preset.key
272
+ }))
273
+ )
274
+
275
+ const stableStringify = (value: unknown): string => {
276
+ if (Array.isArray(value)) {
277
+ return `[${value.map((item) => stableStringify(item)).join(',')}]`
278
+ }
279
+ if (value && typeof value === 'object') {
280
+ return `{${Object.keys(value)
281
+ .sort()
282
+ .map(
283
+ (key) =>
284
+ `${JSON.stringify(key)}:${stableStringify((value as Record<string, unknown>)[key])}`
285
+ )
286
+ .join(',')}}`
287
+ }
288
+ return JSON.stringify(value) ?? 'undefined'
289
+ }
290
+
291
+ const presetDisplayKey = (display: OdsDisplayConfig) =>
292
+ stableStringify({ ...display, hideCompletedOrders: undefined })
293
+
294
+ const activePresetKey = computed<OdsPresetKey | null>(
295
+ () =>
296
+ ODS_PRESETS.find(
297
+ (preset) =>
298
+ presetDisplayKey(resolveDisplay(preset.display)) === presetDisplayKey(odsDisplay.value) &&
299
+ stableStringify(resolveLayout(preset.layout)) === stableStringify(odsLayout.value)
300
+ )?.key ?? null
301
+ )
302
+
303
+ const applyPreset = (presetKey: OdsPresetKey) => {
304
+ const preset = ODS_PRESETS.find((item) => item.key === presetKey)
305
+ if (!preset) return
306
+
307
+ const hideCompletedOrders = odsDisplay.value.hideCompletedOrders
308
+ draft.value.layout = JSON.parse(JSON.stringify(preset.layout))
309
+ draft.value.display = {
310
+ ...JSON.parse(JSON.stringify(preset.display)),
311
+ hideCompletedOrders
312
+ }
313
+ syncBackgroundSource()
314
+ syncSidePanelSource()
315
+ }
316
+
317
+ type BackgroundSource = 'disable' | 'default' | 'left' | 'custom'
318
+
319
+ const ODS_BACKGROUND_DEFAULT_URL = '/images/feedme-bg.png'
320
+ const ODS_BACKGROUND_LEFT_URL = '/images/feedme-bg-left-image.png'
321
+
322
+ const backgroundSource = ref<BackgroundSource>('default')
323
+
324
+ const getDefaultBackground = (url: string = ODS_BACKGROUND_DEFAULT_URL) => ({
325
+ url,
326
+ size: 'cover',
327
+ position: 'center'
328
+ })
329
+
330
+ const syncBackgroundSource = () => {
331
+ const bg = draft.value.layout?.background
332
+ if (bg === null) {
333
+ backgroundSource.value = 'disable'
334
+ } else if (!bg || bg.url === ODS_BACKGROUND_DEFAULT_URL) {
335
+ backgroundSource.value = 'default'
336
+ } else if (bg.url === ODS_BACKGROUND_LEFT_URL) {
337
+ backgroundSource.value = 'left'
338
+ } else {
339
+ backgroundSource.value = 'custom'
340
+ }
341
+ }
342
+
343
+ const setBackgroundSource = (source: BackgroundSource) => {
344
+ if (!canAccessOdsTemplateSettings.value) return
345
+ ensureLayout()
346
+ if (source === 'disable') {
347
+ draft.value.layout!.background = null
348
+ backgroundSource.value = 'disable'
349
+ return
350
+ }
351
+ if (source === 'custom') {
352
+ const current = draft.value.layout!.background
353
+ if (
354
+ current &&
355
+ current !== null &&
356
+ current.url !== ODS_BACKGROUND_DEFAULT_URL &&
357
+ current.url !== ODS_BACKGROUND_LEFT_URL
358
+ ) {
359
+ draft.value.layout!.background = current
360
+ } else {
361
+ draft.value.layout!.background = {
362
+ url: '',
363
+ size: current?.size ?? 'cover',
364
+ position: current?.position ?? 'center'
365
+ }
366
+ }
367
+ backgroundSource.value = 'custom'
368
+ return
369
+ }
370
+ const presetUrl = source === 'left' ? ODS_BACKGROUND_LEFT_URL : ODS_BACKGROUND_DEFAULT_URL
371
+ const current = draft.value.layout!.background
372
+ draft.value.layout!.background = {
373
+ ...(current && current !== null ? current : getDefaultBackground(presetUrl)),
374
+ url: presetUrl
375
+ }
376
+ backgroundSource.value = source
377
+ }
378
+
379
+ const hasCustomBackground = computed(() => {
380
+ const url = draft.value.layout?.background?.url
381
+ return !!url && url !== ODS_BACKGROUND_DEFAULT_URL && url !== ODS_BACKGROUND_LEFT_URL
382
+ })
383
+
384
+ type SidePanelSource = 'disable' | 'default' | 'custom'
385
+
386
+ const sidePanelSource = ref<SidePanelSource>('default')
387
+
388
+ const ODS_SIDE_PANEL_DEFAULT_URL = '/images/feedme-side.png'
389
+
390
+ const getDefaultSidePanel = () => ({
391
+ url: ODS_SIDE_PANEL_DEFAULT_URL,
392
+ position: 'left' as const,
393
+ widthPercent: 33
394
+ })
395
+
396
+ const syncSidePanelSource = () => {
397
+ const sp = draft.value.layout?.sidePanel
398
+ if (!sp) {
399
+ sidePanelSource.value = 'disable'
400
+ } else if (sp.url === ODS_SIDE_PANEL_DEFAULT_URL) {
401
+ sidePanelSource.value = 'default'
402
+ } else {
403
+ sidePanelSource.value = 'custom'
404
+ }
405
+ }
406
+
407
+ const setSidePanelSource = (source: SidePanelSource) => {
408
+ if (!canAccessOdsTemplateSettings.value) return
409
+ ensureLayout()
410
+ if (source === 'disable') {
411
+ draft.value.layout!.sidePanel = null
412
+ sidePanelSource.value = 'disable'
413
+ return
414
+ }
415
+ if (source === 'default') {
416
+ const current = draft.value.layout!.sidePanel
417
+ draft.value.layout!.sidePanel = {
418
+ ...(current && current !== null ? current : getDefaultSidePanel()),
419
+ url: ODS_SIDE_PANEL_DEFAULT_URL
420
+ }
421
+ sidePanelSource.value = 'default'
422
+ return
423
+ }
424
+
425
+ const current = draft.value.layout!.sidePanel
426
+ if (current && current !== null && current.url !== ODS_SIDE_PANEL_DEFAULT_URL) {
427
+ draft.value.layout!.sidePanel = current
428
+ } else {
429
+ draft.value.layout!.sidePanel = {
430
+ url: '',
431
+ position: current?.position ?? 'left',
432
+ widthPercent: current?.widthPercent ?? 33
433
+ }
434
+ }
435
+ sidePanelSource.value = 'custom'
436
+ }
437
+
438
+ const hasCustomSidePanel = computed(() => {
439
+ const url = draft.value.layout?.sidePanel?.url
440
+ return !!url && url !== ODS_SIDE_PANEL_DEFAULT_URL
441
+ })
442
+
443
+ type MediaTarget = 'background' | 'sidePanel'
444
+ const mediaPickerOpen = ref(false)
445
+ const mediaTarget = ref<MediaTarget>('background')
446
+
447
+ const pickImage = (target: MediaTarget) => {
448
+ if (!canAccessOdsTemplateSettings.value) return
449
+ mediaTarget.value = target
450
+ mediaPickerOpen.value = true
451
+ }
452
+
453
+ const selectImage = (selection: MediaSelection) => {
454
+ mediaPickerOpen.value = false
455
+ if (!canAccessOdsTemplateSettings.value) return
456
+ ensureLayout()
457
+ if (mediaTarget.value === 'background') {
458
+ draft.value.layout!.background = {
459
+ url: selection.url,
460
+ size: draft.value.layout!.background?.size ?? 'cover',
461
+ position: draft.value.layout!.background?.position ?? 'center'
462
+ }
463
+ backgroundSource.value = 'custom'
464
+ } else {
465
+ draft.value.layout!.sidePanel = {
466
+ url: selection.url,
467
+ position: draft.value.layout!.sidePanel?.position ?? 'left',
468
+ widthPercent: draft.value.layout!.sidePanel?.widthPercent ?? 30,
469
+ objectFit: draft.value.layout!.sidePanel?.objectFit ?? 'cover'
470
+ }
471
+ sidePanelSource.value = 'custom'
472
+ }
473
+ }
474
+
475
+ const overrideDialogOpen = ref(false)
476
+ const baseBrandkit = computed(() => ({ colors: props.brandkit.colors }) as BaseBrandKit)
477
+
478
+ const getFromPath = (obj: any, path: string) =>
479
+ path.split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), obj)
480
+
481
+ const setAtPath = (obj: any, path: string, value: unknown) => {
482
+ const keys = path.split('.')
483
+ let current = obj
484
+ keys.forEach((key, idx) => {
485
+ if (idx === keys.length - 1) {
486
+ current[key] = value
487
+ return
488
+ }
489
+ current[key] = current[key] ?? {}
490
+ current = current[key]
491
+ })
492
+ }
493
+
494
+ const filterColorOverride = (override: RecursivePartial<BaseBrandKit> | undefined) => {
495
+ if (!override) return undefined
496
+
497
+ const allowedOverride: RecursivePartial<BaseBrandKit> = {}
498
+ props.colorFields.forEach((field) => {
499
+ const value = getFromPath(override, field.key)
500
+ if (value !== undefined) {
501
+ setAtPath(allowedOverride, field.key, value)
502
+ }
503
+ })
504
+
505
+ return Object.keys(allowedOverride).length ? allowedOverride : undefined
506
+ }
507
+
508
+ const overrideCount = computed(() => {
509
+ const existing = draft.value.brandkitOverride ?? {}
510
+ return props.colorFields.reduce((acc, field) => {
511
+ return acc + (getFromPath(existing, field.key) !== undefined ? 1 : 0)
512
+ }, 0)
513
+ })
514
+
515
+ const onSaveOverride = (override: RecursivePartial<BaseBrandKit> | undefined) => {
516
+ if (!canAccessOdsTemplateSettings.value) return
517
+ const allowedOverride = filterColorOverride(override)
518
+ if (allowedOverride) {
519
+ draft.value.brandkitOverride = allowedOverride
520
+ } else {
521
+ delete draft.value.brandkitOverride
522
+ }
523
+ }
524
+
525
+ const groupByItems = computed<ButtonGroupItemWithValue<OrderGroupingMode>[]>(() => [
526
+ { label: t('order.displaySystem.legacyOds.groupByStatus'), value: 'status' },
527
+ { label: t('order.displaySystem.legacyOds.groupByType'), value: 'type' }
528
+ ])
529
+
530
+ const statusLayoutItems = computed<ButtonGroupItemWithValue<OrderStatusBoardLayout>[]>(() => [
531
+ { label: t('order.displaySystem.legacyOds.layoutHorizontal'), value: 'horizontal' },
532
+ { label: t('order.displaySystem.legacyOds.layoutVertical'), value: 'vertical' }
533
+ ])
534
+
535
+ const statusColumnItems: ButtonGroupItemWithValue<number>[] = [
536
+ { label: '1', value: 1 },
537
+ { label: '2', value: 2 },
538
+ { label: '3', value: 3 }
539
+ ]
540
+
541
+ const boardAlignItems = computed<ButtonGroupItemWithValue[]>(() => [
542
+ { label: t('order.displaySystem.legacyOds.alignLeft'), value: 'left' },
543
+ { label: t('order.displaySystem.legacyOds.alignCenter'), value: 'center' },
544
+ { label: t('order.displaySystem.legacyOds.alignRight'), value: 'right' }
545
+ ])
546
+
547
+ const sidePanelPositionItems = computed<ButtonGroupItemWithValue[]>(() => [
548
+ { label: t('order.displaySystem.legacyOds.alignLeft'), value: 'left' },
549
+ { label: t('order.displaySystem.legacyOds.alignRight'), value: 'right' }
550
+ ])
551
+
552
+ const statusTemplateItems = computed<ButtonGroupItemWithValue<StatusSectionTemplateName>[]>(() => [
553
+ { label: t('order.displaySystem.legacyOds.templateDefault'), value: 'default' },
554
+ { label: t('order.displaySystem.legacyOds.templateBorderedCard'), value: 'bordered-card' },
555
+ { label: t('order.displaySystem.legacyOds.templateFloatingTab'), value: 'floating-tab' },
556
+ { label: t('order.displaySystem.legacyOds.templateCenteredPill'), value: 'centered-pill' }
557
+ ])
558
+
559
+ const footerModeItems = computed<ButtonGroupItemWithValue<OdsFooterMode>[]>(() => [
560
+ { label: t('order.displaySystem.legacyOds.footerModeFeedme'), value: 'feedme' },
561
+ { label: t('order.displaySystem.legacyOds.footerModeSettingsIcon'), value: 'settingsIcon' }
562
+ ])
563
+
564
+ const ODS_PREVIEW_WIDTH = 1920
565
+ const ODS_PREVIEW_HEIGHT = 1080
566
+
567
+ const previewViewportRef = ref<HTMLElement | null>(null)
568
+ const previewScale = ref(1)
569
+ let resizeObserver: ResizeObserver | null = null
570
+
571
+ const odsPreviewBaseUrl = computed(() => 'https://v2-dev.feedme-ods.pages.dev/')
572
+
573
+ const resolveAssetPreviewUrl = (url: string) => {
574
+ if (!url.startsWith('/')) return url
575
+ if (!odsPreviewBaseUrl.value) return url
576
+ try {
577
+ return new URL(url, odsPreviewBaseUrl.value).toString()
578
+ } catch {
579
+ return url
580
+ }
581
+ }
582
+
583
+ const encodeUrlSafeBase64 = (value: string) => {
584
+ const bytes = new TextEncoder().encode(value)
585
+ const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
586
+ return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
587
+ }
588
+
589
+ const previewBrandkitOverride = computed<RecursivePartial<BaseBrandKit> | undefined>(() => {
590
+ const override = draft.value.brandkitOverride
591
+ if (!override) return undefined
592
+
593
+ const previewOverride: RecursivePartial<BaseBrandKit> = {}
594
+ props.colorFields.forEach((field) => {
595
+ const overrideValue = getFromPath(override, field.key)
596
+ if (overrideValue === undefined) return
597
+ if (overrideValue === getFromPath(props.brandkit, field.key)) return
598
+ setAtPath(previewOverride, field.key, overrideValue)
599
+ })
600
+
601
+ return Object.keys(previewOverride).length ? previewOverride : undefined
602
+ })
603
+
604
+ const previewBrandKit = computed<BrandKit>(() => ({
605
+ ...props.brandkit,
606
+ ods: [
607
+ {
608
+ isDefault: true,
609
+ layout: draft.value.layout,
610
+ display: draft.value.display,
611
+ brandkitOverride: previewBrandkitOverride.value
612
+ }
613
+ ]
614
+ }))
615
+
616
+ const previewUrl = computed(() => {
617
+ if (!odsPreviewBaseUrl.value) return ''
618
+ try {
619
+ const url = new URL(odsPreviewBaseUrl.value, window.location.origin)
620
+ url.searchParams.set('preview', '1')
621
+ url.searchParams.set('brandKit', encodeUrlSafeBase64(JSON.stringify(previewBrandKit.value)))
622
+ return url.toString()
623
+ } catch {
624
+ return ''
625
+ }
626
+ })
627
+
628
+ const openPreviewInNewTab = () => {
629
+ if (previewUrl.value) {
630
+ window.open(previewUrl.value, '_blank', 'noopener,noreferrer')
631
+ }
632
+ }
633
+
634
+ const updatePreviewScale = () => {
635
+ const vp = previewViewportRef.value
636
+ if (!vp) {
637
+ previewScale.value = 1
638
+ return
639
+ }
640
+ const wScale = vp.clientWidth / ODS_PREVIEW_WIDTH
641
+ const hScale = vp.clientHeight / ODS_PREVIEW_HEIGHT
642
+ previewScale.value = Math.min(wScale, hScale, 1)
643
+ }
644
+
645
+ watch(
646
+ previewViewportRef,
647
+ (vp) => {
648
+ resizeObserver?.disconnect()
649
+ resizeObserver = null
650
+ if (vp) {
651
+ resizeObserver = new ResizeObserver(() => updatePreviewScale())
652
+ resizeObserver.observe(vp)
653
+ updatePreviewScale()
654
+ }
655
+ },
656
+ { flush: 'post' }
657
+ )
658
+
659
+ watch(
660
+ () => props.modelValue,
661
+ (v) => {
662
+ if (v) {
663
+ syncBackgroundSource()
664
+ syncSidePanelSource()
665
+ }
666
+ }
667
+ )
668
+
669
+ onUnmounted(() => resizeObserver?.disconnect())
670
+
671
+ const onClose = () => {
672
+ resizeObserver?.disconnect()
673
+ resizeObserver = null
674
+ emit('cancel')
675
+ show.value = false
676
+ }
677
+
678
+ const onSave = () => {
679
+ const entry = JSON.parse(JSON.stringify(draft.value)) as OdsConfigEntry
680
+ if (!entry.display) entry.display = createDefaultOdsDisplay()
681
+ if (!entry.layout) {
682
+ entry.layout = JSON.parse(JSON.stringify(ODS_PRESETS[0].layout))
683
+ }
684
+ const allowedOverride = filterColorOverride(entry.brandkitOverride)
685
+ if (allowedOverride) {
686
+ entry.brandkitOverride = allowedOverride
687
+ } else {
688
+ delete entry.brandkitOverride
689
+ }
690
+ emit('save', entry)
691
+ }
692
+ </script>
693
+
694
+ <template>
695
+ <Teleport to="body">
696
+ <Transition name="fade">
697
+ <div
698
+ v-if="show"
699
+ class="fixed inset-0 flex flex-col w-screen h-dvh bg-fm-color-neutral-white"
700
+ style="z-index: 50"
701
+ tabindex="-1"
702
+ @keyup.esc="onClose"
703
+ >
704
+ <div
705
+ class="flex items-center px-24 pt-16 pb-8 border-b border-fm-color-neutral-gray-100 space-x-4"
706
+ >
707
+ <p class="fm-typo-en-title-sm-600 mr-auto">
708
+ {{ t('order.displaySystem.legacyOds.odsConfiguration')
709
+ }}{{ draft.label ? ` - ${draft.label}` : '' }}
710
+ </p>
711
+ <FmButton
712
+ :label="t('order.displaySystem.legacyOds.cancel')"
713
+ variant="tertiary"
714
+ @click="onClose"
715
+ />
716
+ <FmButton
717
+ :label="t('order.displaySystem.legacyOds.confirm')"
718
+ variant="primary"
719
+ :disabled="saving"
720
+ @click="onSave"
721
+ />
722
+ </div>
723
+
724
+ <div class="flex flex-col flex-1 overflow-scroll w-full px-24 py-24">
725
+ <div class="flex flex-col gap-24 max-w-[1600px] w-full mx-auto">
726
+ <div class="rounded-lg border border-fm-color-neutral-gray-100 p-16 space-y-12">
727
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-12">
728
+ <FmTextField
729
+ v-model="draft.label"
730
+ :label="t('order.displaySystem.legacyOds.label')"
731
+ placeholder="e.g. Default, Flagship, Express"
732
+ />
733
+ <div class="flex items-center gap-12 pt-24">
734
+ <FmSwitch
735
+ v-model="draft.isDefault"
736
+ :label="t('order.displaySystem.legacyOds.defaultConfiguration')"
737
+ />
738
+ </div>
739
+ </div>
740
+ <div
741
+ v-if="draft.isDefault && anotherDefaultExists"
742
+ class="flex items-start gap-8 rounded-lg bg-fm-color-warning-50 border border-fm-color-warning-200 px-12 py-8"
743
+ >
744
+ <FmIcon
745
+ name="warning"
746
+ size="sm"
747
+ class="text-fm-color-warning-500 flex-shrink-0 mt-2"
748
+ />
749
+ <span class="fm-typo-en-body-sm-400 text-fm-color-warning-700">
750
+ {{ t('order.displaySystem.legacyOds.defaultWarning') }}
751
+ </span>
752
+ </div>
753
+ <div v-if="!draft.isDefault">
754
+ <RestaurantSelector
755
+ v-model="selectedRestaurantIds"
756
+ multiple
757
+ :managable-only="false"
758
+ />
759
+ </div>
760
+ <div v-else class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
761
+ {{ t('order.displaySystem.legacyOds.restaurantInfo') }}
762
+ </div>
763
+ </div>
764
+
765
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-24 items-start">
766
+ <div class="space-y-24">
767
+ <div class="rounded-lg overflow-hidden border border-fm-color-neutral-gray-100">
768
+ <div
769
+ class="py-16 px-24 bg-fm-color-neutral-gray-50 border-b border-fm-color-neutral-gray-100"
770
+ >
771
+ <div class="flex items-center gap-12 mb-8">
772
+ <div
773
+ class="inline-flex items-center justify-center w-10 h-10 bg-fm-color-neutral-gray-100 rounded-full"
774
+ >
775
+ <FmIcon
776
+ name="dashboard_customize"
777
+ class="text-fm-color-typo-secondary"
778
+ size="md"
779
+ outline
780
+ />
781
+ </div>
782
+ <div class="fm-typo-en-body-lg-600 text-fm-color-typo-primary">
783
+ {{ t('order.displaySystem.legacyOds.selectPreset') }}
784
+ </div>
785
+ </div>
786
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
787
+ {{ t('order.displaySystem.legacyOds.selectPresetDescription') }}
788
+ </div>
789
+ </div>
790
+
791
+ <div class="px-24 py-12">
792
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
793
+ <FmButtonGroup
794
+ :items="presetItems"
795
+ :model-value="presetItems.find((i) => i.value === activePresetKey) ?? null"
796
+ @update:model-value="(v: any) => applyPreset(v.value)"
797
+ />
798
+ </div>
799
+ </div>
800
+ </div>
801
+
802
+ <div class="rounded-lg overflow-hidden border border-fm-color-neutral-gray-100">
803
+ <div
804
+ class="py-16 px-24 bg-fm-color-neutral-gray-50 border-b border-fm-color-neutral-gray-100"
805
+ >
806
+ <div class="flex items-center gap-12 mb-8">
807
+ <div
808
+ class="inline-flex items-center justify-center w-10 h-10 bg-fm-color-neutral-gray-100 rounded-full"
809
+ >
810
+ <FmIcon
811
+ name="visibility_off"
812
+ class="text-fm-color-typo-secondary"
813
+ size="md"
814
+ outline
815
+ />
816
+ </div>
817
+ <div class="fm-typo-en-body-lg-600 text-fm-color-typo-primary">
818
+ {{ t('order.displaySystem.legacyOds.hideCollectedOrders') }}
819
+ </div>
820
+ </div>
821
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
822
+ {{ t('order.displaySystem.legacyOds.hideCollectedOrdersDescription') }}
823
+ </div>
824
+ </div>
825
+
826
+ <div class="px-24 py-12">
827
+ <div
828
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
829
+ >
830
+ <FmSwitch
831
+ :label="t('order.displaySystem.legacyOds.hideCollectedOrders')"
832
+ :model-value="odsDisplay.hideCompletedOrders ?? false"
833
+ @update:model-value="(v: boolean) => updateHideCompletedOrders(v)"
834
+ />
835
+ </div>
836
+ </div>
837
+ </div>
838
+
839
+ <div
840
+ v-if="canAccessOdsTemplateSettings"
841
+ class="rounded-lg overflow-hidden border border-fm-color-neutral-gray-100"
842
+ >
843
+ <div
844
+ class="py-16 px-24 bg-fm-color-secondary-50 border-b border-fm-color-neutral-gray-100"
845
+ >
846
+ <div class="flex items-center gap-12 mb-8">
847
+ <div
848
+ class="inline-flex items-center justify-center w-10 h-10 bg-fm-color-secondary-100 rounded-full"
849
+ >
850
+ <FmIcon
851
+ name="monitor"
852
+ class="text-fm-color-secondary-500"
853
+ size="md"
854
+ outline
855
+ />
856
+ </div>
857
+ <div class="fm-typo-en-body-lg-600 text-fm-color-typo-primary">
858
+ {{ t('order.displaySystem.legacyOds.displaySettings') }}
859
+ </div>
860
+ </div>
861
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
862
+ {{ t('order.displaySystem.legacyOds.displaySettingsDescription') }}
863
+ </div>
864
+ </div>
865
+
866
+ <div class="px-24 py-12">
867
+ <div class="flex items-center justify-end gap-8">
868
+ <FmButton
869
+ icon="autorenew"
870
+ :label="t('order.displaySystem.legacyOds.resetToDefault')"
871
+ variant="plain"
872
+ :disabled="!canAccessOdsTemplateSettings"
873
+ @click="resetDisplay"
874
+ />
875
+ </div>
876
+ </div>
877
+
878
+ <div class="px-24 py-12">
879
+ <div class="space-y-12">
880
+ <div>
881
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
882
+ {{ t('order.displaySystem.legacyOds.colorOverride') }}
883
+ </div>
884
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
885
+ {{ t('order.displaySystem.legacyOds.colorOverrideConfigDescription') }}
886
+ </div>
887
+ </div>
888
+ <div
889
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
890
+ >
891
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
892
+ {{
893
+ t('order.displaySystem.legacyOds.colorsOverridden', {
894
+ count: overrideCount
895
+ })
896
+ }}
897
+ </div>
898
+ <FmButton
899
+ variant="secondary"
900
+ icon="palette"
901
+ :label="t('order.displaySystem.legacyOds.configure')"
902
+ :disabled="!canAccessOdsTemplateSettings"
903
+ @click="canAccessOdsTemplateSettings && (overrideDialogOpen = true)"
904
+ />
905
+ </div>
906
+ </div>
907
+ </div>
908
+
909
+ <div class="px-24 py-12">
910
+ <div class="space-y-12">
911
+ <div>
912
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
913
+ {{ t('order.displaySystem.legacyOds.groupOrdersBy') }}
914
+ </div>
915
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
916
+ {{ t('order.displaySystem.legacyOds.groupOrdersByDescription') }}
917
+ </div>
918
+ </div>
919
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
920
+ <FmButtonGroup
921
+ :items="groupByItems"
922
+ :model-value="
923
+ groupByItems.find((i) => i.value === odsDisplay.groupBy) ??
924
+ groupByItems[0]
925
+ "
926
+ :disabled="!canAccessOdsTemplateSettings"
927
+ @update:model-value="(v: any) => updateDisplay('groupBy', v.value)"
928
+ />
929
+ </div>
930
+ </div>
931
+ </div>
932
+
933
+ <div class="px-24 py-12">
934
+ <div class="space-y-12">
935
+ <div>
936
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
937
+ {{ t('order.displaySystem.legacyOds.showCurrentOrder') }}
938
+ </div>
939
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
940
+ {{ t('order.displaySystem.legacyOds.showCurrentOrderDescription') }}
941
+ </div>
942
+ </div>
943
+ <div
944
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
945
+ >
946
+ <FmSwitch
947
+ :label="t('order.displaySystem.legacyOds.showCurrentOrder')"
948
+ :model-value="odsDisplay.currentOrder?.enabled ?? true"
949
+ :disabled="!canAccessOdsTemplateSettings"
950
+ @update:model-value="(v: boolean) => updateCurrentOrderEnabled(v)"
951
+ />
952
+ </div>
953
+ </div>
954
+ </div>
955
+
956
+ <div v-if="odsDisplay.groupBy === 'status'" class="px-24 py-12">
957
+ <div class="space-y-12">
958
+ <div>
959
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
960
+ {{ t('order.displaySystem.legacyOds.statusBoardLayout') }}
961
+ </div>
962
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
963
+ {{ t('order.displaySystem.legacyOds.statusBoardLayoutDescription') }}
964
+ </div>
965
+ </div>
966
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
967
+ <FmButtonGroup
968
+ :items="statusLayoutItems"
969
+ :model-value="
970
+ statusLayoutItems.find((i) => i.value === odsDisplay.statusLayout) ??
971
+ statusLayoutItems[0]
972
+ "
973
+ :disabled="!canAccessOdsTemplateSettings"
974
+ @update:model-value="(v: any) => updateDisplay('statusLayout', v.value)"
975
+ />
976
+ </div>
977
+ </div>
978
+ </div>
979
+
980
+ <div
981
+ v-if="odsDisplay.groupBy === 'status' && odsDisplay.statusLayout !== 'vertical'"
982
+ class="px-24 py-12"
983
+ >
984
+ <div class="space-y-12">
985
+ <div>
986
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
987
+ {{ t('order.displaySystem.legacyOds.statusColumnCount') }}
988
+ </div>
989
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
990
+ {{ t('order.displaySystem.legacyOds.statusColumnCountDescription') }}
991
+ </div>
992
+ </div>
993
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
994
+ <FmButtonGroup
995
+ :items="statusColumnItems"
996
+ :model-value="
997
+ statusColumnItems.find(
998
+ (i) => i.value === odsDisplay.statusColumnCount
999
+ ) ?? statusColumnItems[1]
1000
+ "
1001
+ :disabled="!canAccessOdsTemplateSettings"
1002
+ @update:model-value="
1003
+ (v: any) => updateDisplay('statusColumnCount', v.value)
1004
+ "
1005
+ />
1006
+ </div>
1007
+ </div>
1008
+ </div>
1009
+
1010
+ <div v-if="odsDisplay.groupBy === 'status'" class="px-24 py-12">
1011
+ <div class="space-y-12">
1012
+ <div>
1013
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1014
+ {{ t('order.displaySystem.legacyOds.slidingInterval') }}
1015
+ </div>
1016
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1017
+ {{ t('order.displaySystem.legacyOds.slidingIntervalDescription') }}
1018
+ </div>
1019
+ </div>
1020
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
1021
+ <FmStepperField
1022
+ :model-value="odsDisplay.statusSlidingPeriod / 1000"
1023
+ label="Sliding interval (seconds)"
1024
+ :disabled="!canAccessOdsTemplateSettings"
1025
+ :min="1"
1026
+ :max="60"
1027
+ :step="1"
1028
+ :append-text="'s'"
1029
+ :max-fraction-digits="0"
1030
+ @update:model-value="
1031
+ (v: any) =>
1032
+ updateDisplay(
1033
+ 'statusSlidingPeriod',
1034
+ Math.max(1000, Math.min(60000, (Number(v) || 10) * 1000))
1035
+ )
1036
+ "
1037
+ />
1038
+ </div>
1039
+ </div>
1040
+ </div>
1041
+
1042
+ <div v-if="odsDisplay.groupBy === 'status'" class="px-24 py-12">
1043
+ <div class="space-y-12">
1044
+ <div>
1045
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1046
+ {{ t('order.displaySystem.legacyOds.showDeliveryIcons') }}
1047
+ </div>
1048
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1049
+ {{ t('order.displaySystem.legacyOds.showDeliveryIconsDescription') }}
1050
+ </div>
1051
+ </div>
1052
+ <div
1053
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
1054
+ >
1055
+ <FmSwitch
1056
+ :label="t('order.displaySystem.legacyOds.showDeliveryIcons')"
1057
+ :model-value="odsDisplay.showDeliveryIcons"
1058
+ :disabled="!canAccessOdsTemplateSettings"
1059
+ @update:model-value="
1060
+ (v: boolean) => updateDisplay('showDeliveryIcons', v)
1061
+ "
1062
+ />
1063
+ </div>
1064
+ </div>
1065
+ </div>
1066
+
1067
+ <div v-if="odsDisplay.groupBy === 'status'" class="px-24 py-12">
1068
+ <div class="space-y-12">
1069
+ <div>
1070
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1071
+ {{ t('order.displaySystem.legacyOds.showOrderCount') }}
1072
+ </div>
1073
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1074
+ {{ t('order.displaySystem.legacyOds.showOrderCountDescription') }}
1075
+ </div>
1076
+ </div>
1077
+ <div
1078
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
1079
+ >
1080
+ <FmSwitch
1081
+ :label="t('order.displaySystem.legacyOds.showOrderCount')"
1082
+ :model-value="odsDisplay.showOrderCount"
1083
+ :disabled="!canAccessOdsTemplateSettings"
1084
+ @update:model-value="(v: boolean) => updateDisplay('showOrderCount', v)"
1085
+ />
1086
+ </div>
1087
+ </div>
1088
+ </div>
1089
+
1090
+ <div v-if="odsDisplay.groupBy === 'status'" class="px-24 py-12">
1091
+ <div class="space-y-12">
1092
+ <div>
1093
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1094
+ {{ t('order.displaySystem.legacyOds.showEstimatedTime') }}
1095
+ </div>
1096
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1097
+ {{ t('order.displaySystem.legacyOds.showEstimatedTimeDescription') }}
1098
+ </div>
1099
+ </div>
1100
+ <div
1101
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
1102
+ >
1103
+ <FmSwitch
1104
+ :label="t('order.displaySystem.legacyOds.showEstimatedTime')"
1105
+ :model-value="odsDisplay.showEstimatedTime"
1106
+ :disabled="!canAccessOdsTemplateSettings"
1107
+ @update:model-value="
1108
+ (v: boolean) => updateDisplay('showEstimatedTime', v)
1109
+ "
1110
+ />
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+
1115
+ <div v-if="odsDisplay.groupBy === 'type'" class="px-24 py-12">
1116
+ <div class="space-y-12">
1117
+ <div>
1118
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1119
+ {{ t('order.displaySystem.legacyOds.showOrderTypeTabs') }}
1120
+ </div>
1121
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1122
+ {{ t('order.displaySystem.legacyOds.showOrderTypeTabsDescription') }}
1123
+ </div>
1124
+ </div>
1125
+ <div
1126
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
1127
+ >
1128
+ <FmSwitch
1129
+ :label="t('order.displaySystem.legacyOds.showOrderTypeTabs')"
1130
+ :model-value="odsDisplay.showOrderTypeTabs"
1131
+ :disabled="!canAccessOdsTemplateSettings"
1132
+ @update:model-value="
1133
+ (v: boolean) => updateDisplay('showOrderTypeTabs', v)
1134
+ "
1135
+ />
1136
+ </div>
1137
+ </div>
1138
+ </div>
1139
+
1140
+ <div v-if="odsDisplay.groupBy !== 'status'" class="px-24 py-12">
1141
+ <div class="space-y-12">
1142
+ <div>
1143
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1144
+ {{ t('order.displaySystem.legacyOds.showProgressBar') }}
1145
+ </div>
1146
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1147
+ {{ t('order.displaySystem.legacyOds.showProgressBarDescription') }}
1148
+ </div>
1149
+ </div>
1150
+ <div
1151
+ class="bg-fm-color-neutral-gray-50 rounded-lg p-12 flex items-center justify-between"
1152
+ >
1153
+ <FmSwitch
1154
+ :label="t('order.displaySystem.legacyOds.showProgressBar')"
1155
+ :model-value="odsDisplay.showProgressBar"
1156
+ :disabled="!canAccessOdsTemplateSettings"
1157
+ @update:model-value="(v: boolean) => updateDisplay('showProgressBar', v)"
1158
+ />
1159
+ </div>
1160
+ </div>
1161
+ </div>
1162
+
1163
+ <div
1164
+ v-if="
1165
+ isAdvancedCustomizationEnabled &&
1166
+ odsDisplay.groupBy === 'status' &&
1167
+ odsDisplay.statusLayout === 'horizontal'
1168
+ "
1169
+ class="px-24 py-12"
1170
+ >
1171
+ <div class="space-y-12">
1172
+ <div>
1173
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1174
+ {{ t('order.displaySystem.legacyOds.statusTemplate') }}
1175
+ </div>
1176
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1177
+ {{ t('order.displaySystem.legacyOds.statusTemplateDescription') }}
1178
+ </div>
1179
+ </div>
1180
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
1181
+ <FmButtonGroup
1182
+ :items="statusTemplateItems"
1183
+ :model-value="
1184
+ statusTemplateItems.find(
1185
+ (i) => i.value === (odsDisplay.statusTemplate ?? 'default')
1186
+ ) ?? null
1187
+ "
1188
+ :disabled="!canAccessOdsTemplateSettings"
1189
+ @update:model-value="(v: any) => updateDisplay('statusTemplate', v.value)"
1190
+ />
1191
+ </div>
1192
+ </div>
1193
+ </div>
1194
+ </div>
1195
+
1196
+ <div
1197
+ v-if="canAccessOdsTemplateSettings"
1198
+ class="rounded-lg overflow-hidden border border-fm-color-neutral-gray-100"
1199
+ >
1200
+ <div
1201
+ class="py-16 px-24 bg-fm-color-primary-50 border-b border-fm-color-neutral-gray-100"
1202
+ >
1203
+ <div class="flex items-center gap-12 mb-8">
1204
+ <div
1205
+ class="inline-flex items-center justify-center w-10 h-10 bg-fm-color-primary-100 rounded-full"
1206
+ >
1207
+ <FmIcon
1208
+ name="dashboard"
1209
+ class="text-fm-color-primary-500"
1210
+ size="md"
1211
+ outline
1212
+ />
1213
+ </div>
1214
+ <div class="fm-typo-en-body-lg-600 text-fm-color-typo-primary">
1215
+ {{ t('order.displaySystem.legacyOds.layoutSettings') }}
1216
+ </div>
1217
+ </div>
1218
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1219
+ {{ t('order.displaySystem.legacyOds.layoutSettingsDescription') }}
1220
+ </div>
1221
+ </div>
1222
+
1223
+ <div class="px-24 py-12">
1224
+ <div class="flex items-center justify-end gap-8">
1225
+ <FmButton
1226
+ icon="autorenew"
1227
+ :label="t('order.displaySystem.legacyOds.resetToDefault')"
1228
+ variant="plain"
1229
+ :disabled="!canAccessOdsTemplateSettings"
1230
+ @click="resetLayout"
1231
+ />
1232
+ </div>
1233
+ </div>
1234
+
1235
+ <div class="px-24 py-12">
1236
+ <div class="space-y-12">
1237
+ <div>
1238
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1239
+ {{ t('order.displaySystem.legacyOds.backgroundImage') }}
1240
+ </div>
1241
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1242
+ {{ t('order.displaySystem.legacyOds.backgroundImageDescription') }}
1243
+ </div>
1244
+ </div>
1245
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12 space-y-12">
1246
+ <div class="flex flex-wrap gap-12">
1247
+ <FmRadio
1248
+ :model-value="backgroundSource"
1249
+ :label="t('order.displaySystem.legacyOds.disable')"
1250
+ value="disable"
1251
+ :disabled="!canAccessOdsTemplateSettings"
1252
+ @update:model-value="() => setBackgroundSource('disable')"
1253
+ />
1254
+ <FmRadio
1255
+ :model-value="backgroundSource"
1256
+ :label="t('order.displaySystem.legacyOds.defaultImage1')"
1257
+ value="default"
1258
+ :disabled="!canAccessOdsTemplateSettings"
1259
+ @update:model-value="() => setBackgroundSource('default')"
1260
+ />
1261
+ <FmRadio
1262
+ :model-value="backgroundSource"
1263
+ :label="t('order.displaySystem.legacyOds.defaultImage2')"
1264
+ value="left"
1265
+ :disabled="!canAccessOdsTemplateSettings"
1266
+ @update:model-value="() => setBackgroundSource('left')"
1267
+ />
1268
+ <FmRadio
1269
+ :model-value="backgroundSource"
1270
+ :label="t('order.displaySystem.legacyOds.custom')"
1271
+ value="custom"
1272
+ :disabled="!canAccessOdsTemplateSettings"
1273
+ @update:model-value="() => setBackgroundSource('custom')"
1274
+ />
1275
+ </div>
1276
+
1277
+ <div
1278
+ v-if="
1279
+ backgroundSource !== 'disable' &&
1280
+ (backgroundSource !== 'custom' || hasCustomBackground)
1281
+ "
1282
+ class="relative"
1283
+ >
1284
+ <div
1285
+ class="w-full h-[120px] border border-fm-color-neutral-gray-200 rounded-lg overflow-hidden bg-fm-color-neutral-white"
1286
+ >
1287
+ <img
1288
+ :src="resolveAssetPreviewUrl(odsLayout.background?.url ?? '')"
1289
+ class="w-full h-full object-contain"
1290
+ />
1291
+ </div>
1292
+ <div
1293
+ v-if="backgroundSource === 'custom'"
1294
+ class="mt-8 flex items-center gap-8"
1295
+ >
1296
+ <FmButton
1297
+ variant="secondary"
1298
+ icon="photo_library"
1299
+ :label="t('order.displaySystem.legacyOds.changeImage')"
1300
+ :disabled="!canAccessOdsTemplateSettings"
1301
+ @click="pickImage('background')"
1302
+ />
1303
+ </div>
1304
+ </div>
1305
+
1306
+ <FmButton
1307
+ v-if="backgroundSource === 'custom' && !hasCustomBackground"
1308
+ variant="secondary"
1309
+ icon="photo_library"
1310
+ :label="t('order.displaySystem.legacyOds.chooseImage')"
1311
+ :disabled="!canAccessOdsTemplateSettings"
1312
+ @click="pickImage('background')"
1313
+ />
1314
+
1315
+ <div
1316
+ v-if="backgroundSource === 'disable'"
1317
+ class="rounded-lg border border-dashed border-fm-color-neutral-gray-200 bg-white px-16 py-16"
1318
+ >
1319
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1320
+ {{ t('order.displaySystem.legacyOds.backgroundDisabled') }}
1321
+ </div>
1322
+ </div>
1323
+ </div>
1324
+ </div>
1325
+ </div>
1326
+
1327
+ <div class="px-24 py-12">
1328
+ <div class="space-y-12">
1329
+ <div>
1330
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1331
+ {{ t('order.displaySystem.legacyOds.sidePanelImage') }}
1332
+ </div>
1333
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1334
+ {{ t('order.displaySystem.legacyOds.sidePanelImageDescription') }}
1335
+ </div>
1336
+ </div>
1337
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12 space-y-12">
1338
+ <div class="flex flex-wrap gap-12">
1339
+ <FmRadio
1340
+ :model-value="sidePanelSource"
1341
+ :label="t('order.displaySystem.legacyOds.disable')"
1342
+ value="disable"
1343
+ :disabled="!canAccessOdsTemplateSettings"
1344
+ @update:model-value="() => setSidePanelSource('disable')"
1345
+ />
1346
+ <FmRadio
1347
+ :model-value="sidePanelSource"
1348
+ :label="t('order.displaySystem.legacyOds.defaultImage')"
1349
+ value="default"
1350
+ :disabled="!canAccessOdsTemplateSettings"
1351
+ @update:model-value="() => setSidePanelSource('default')"
1352
+ />
1353
+ <FmRadio
1354
+ :model-value="sidePanelSource"
1355
+ :label="t('order.displaySystem.legacyOds.customImage')"
1356
+ value="custom"
1357
+ :disabled="!canAccessOdsTemplateSettings"
1358
+ @update:model-value="() => setSidePanelSource('custom')"
1359
+ />
1360
+ </div>
1361
+
1362
+ <div
1363
+ v-if="sidePanelSource === 'disable'"
1364
+ class="rounded-lg border border-dashed border-fm-color-neutral-gray-200 bg-white px-16 py-16"
1365
+ >
1366
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1367
+ {{ t('order.displaySystem.legacyOds.sidePanelDisabled') }}
1368
+ </div>
1369
+ </div>
1370
+
1371
+ <template v-else>
1372
+ <div
1373
+ v-if="sidePanelSource !== 'custom' || hasCustomSidePanel"
1374
+ class="relative"
1375
+ >
1376
+ <div
1377
+ class="w-full h-[120px] border border-fm-color-neutral-gray-200 rounded-lg overflow-hidden bg-fm-color-neutral-white"
1378
+ >
1379
+ <img
1380
+ :src="resolveAssetPreviewUrl(odsLayout.sidePanel?.url ?? '')"
1381
+ class="w-full h-full object-contain"
1382
+ />
1383
+ </div>
1384
+ <div
1385
+ v-if="sidePanelSource === 'custom'"
1386
+ class="mt-8 flex items-center gap-8"
1387
+ >
1388
+ <FmButton
1389
+ variant="secondary"
1390
+ icon="photo_library"
1391
+ :label="t('order.displaySystem.legacyOds.changeImage')"
1392
+ :disabled="!canAccessOdsTemplateSettings"
1393
+ @click="pickImage('sidePanel')"
1394
+ />
1395
+ </div>
1396
+ </div>
1397
+
1398
+ <FmButton
1399
+ v-if="sidePanelSource === 'custom' && !hasCustomSidePanel"
1400
+ variant="secondary"
1401
+ icon="photo_library"
1402
+ :label="t('order.displaySystem.legacyOds.chooseImage')"
1403
+ :disabled="!canAccessOdsTemplateSettings"
1404
+ @click="pickImage('sidePanel')"
1405
+ />
1406
+
1407
+ <template v-if="draft.layout?.sidePanel">
1408
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
1409
+ <div>
1410
+ <div class="fm-typo-en-body-sm-600 text-fm-color-typo-primary mb-4">
1411
+ {{ t('order.displaySystem.legacyOds.position') }}
1412
+ </div>
1413
+ <FmButtonGroup
1414
+ :items="sidePanelPositionItems"
1415
+ :model-value="
1416
+ sidePanelPositionItems.find(
1417
+ (i) => i.value === odsLayout.sidePanel?.position
1418
+ ) ?? sidePanelPositionItems[0]
1419
+ "
1420
+ :disabled="!canAccessOdsTemplateSettings"
1421
+ @update:model-value="
1422
+ (v: any) => {
1423
+ if (!canAccessOdsTemplateSettings) return
1424
+ ensureLayout()
1425
+ if (draft.layout!.sidePanel) {
1426
+ draft.layout!.sidePanel.position = v.value as
1427
+ 'left' | 'right'
1428
+ }
1429
+ }
1430
+ "
1431
+ />
1432
+ </div>
1433
+ <div>
1434
+ <div class="fm-typo-en-body-sm-600 text-fm-color-typo-primary mb-4">
1435
+ {{ t('order.displaySystem.legacyOds.widthPercent') }}
1436
+ </div>
1437
+ <FmTextField
1438
+ type="number"
1439
+ :model-value="String(odsLayout.sidePanel?.widthPercent ?? 30)"
1440
+ placeholder="30"
1441
+ :disabled="!canAccessOdsTemplateSettings"
1442
+ @update:model-value="
1443
+ (v: any) => {
1444
+ if (!canAccessOdsTemplateSettings) return
1445
+ ensureLayout()
1446
+ if (draft.layout!.sidePanel) {
1447
+ draft.layout!.sidePanel.widthPercent = Math.max(
1448
+ 1,
1449
+ Math.min(80, Number(v) || 30)
1450
+ )
1451
+ }
1452
+ }
1453
+ "
1454
+ />
1455
+ </div>
1456
+ </div>
1457
+ </template>
1458
+ </template>
1459
+ </div>
1460
+ </div>
1461
+ </div>
1462
+
1463
+ <div class="px-24 py-12">
1464
+ <div class="space-y-12">
1465
+ <div>
1466
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1467
+ {{ t('order.displaySystem.legacyOds.boardWidth') }}
1468
+ </div>
1469
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1470
+ {{ t('order.displaySystem.legacyOds.boardWidthDescription') }}
1471
+ </div>
1472
+ </div>
1473
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
1474
+ <FmStepperField
1475
+ :model-value="odsLayout.boardWidthPercent ?? 100"
1476
+ label="Board width percent"
1477
+ :disabled="!canAccessOdsTemplateSettings"
1478
+ :min="10"
1479
+ :max="100"
1480
+ :step="5"
1481
+ :append-text="'%'"
1482
+ :max-fraction-digits="0"
1483
+ @update:model-value="
1484
+ (v: any) =>
1485
+ updateLayoutField(
1486
+ 'boardWidthPercent',
1487
+ Math.max(10, Math.min(100, Number(v) || 100))
1488
+ )
1489
+ "
1490
+ />
1491
+ </div>
1492
+ </div>
1493
+ </div>
1494
+
1495
+ <div v-if="(odsLayout.boardWidthPercent ?? 100) < 100" class="px-24 py-12">
1496
+ <div class="space-y-12">
1497
+ <div>
1498
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1499
+ {{ t('order.displaySystem.legacyOds.boardAlignment') }}
1500
+ </div>
1501
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1502
+ {{ t('order.displaySystem.legacyOds.boardAlignmentDescription') }}
1503
+ </div>
1504
+ </div>
1505
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
1506
+ <FmButtonGroup
1507
+ :items="boardAlignItems"
1508
+ :model-value="
1509
+ boardAlignItems.find(
1510
+ (i) => i.value === (odsLayout.boardAlign ?? 'left')
1511
+ ) ?? boardAlignItems[0]
1512
+ "
1513
+ :disabled="!canAccessOdsTemplateSettings"
1514
+ @update:model-value="
1515
+ (v: any) =>
1516
+ updateLayoutField(
1517
+ 'boardAlign',
1518
+ v.value as 'left' | 'right' | 'center'
1519
+ )
1520
+ "
1521
+ />
1522
+ </div>
1523
+ </div>
1524
+ </div>
1525
+
1526
+ <div v-if="isAdvancedCustomizationEnabled" class="px-24 py-12">
1527
+ <div class="space-y-12">
1528
+ <div>
1529
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-4">
1530
+ {{ t('order.displaySystem.legacyOds.footerMode') }}
1531
+ </div>
1532
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1533
+ {{ t('order.displaySystem.legacyOds.footerModeDescription') }}
1534
+ </div>
1535
+ </div>
1536
+ <div class="bg-fm-color-neutral-gray-50 rounded-lg p-12">
1537
+ <FmButtonGroup
1538
+ :items="footerModeItems"
1539
+ :model-value="
1540
+ footerModeItems.find(
1541
+ (i) => i.value === (odsLayout.footerMode ?? 'feedme')
1542
+ ) ?? footerModeItems[0]
1543
+ "
1544
+ :disabled="!canAccessOdsTemplateSettings"
1545
+ @update:model-value="
1546
+ (v: any) => updateLayoutField('footerMode', v.value as OdsFooterMode)
1547
+ "
1548
+ />
1549
+ </div>
1550
+ </div>
1551
+ </div>
1552
+ </div>
1553
+ </div>
1554
+
1555
+ <div class="min-w-0 lg:sticky lg:top-24">
1556
+ <div
1557
+ class="overflow-hidden rounded-lg border border-fm-color-neutral-gray-100 bg-white"
1558
+ >
1559
+ <div
1560
+ class="border-b border-fm-color-neutral-gray-100 bg-fm-color-neutral-gray-50 px-24 py-16"
1561
+ >
1562
+ <div class="flex flex-col gap-12 lg:flex-row lg:items-start lg:justify-between">
1563
+ <div>
1564
+ <div class="fm-typo-en-body-lg-600 text-fm-color-typo-primary">
1565
+ {{ t('order.displaySystem.legacyOds.odsPreview') }}
1566
+ </div>
1567
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1568
+ {{ t('order.displaySystem.legacyOds.previewDescription') }}
1569
+ </div>
1570
+ </div>
1571
+ <div class="flex flex-wrap items-center gap-8">
1572
+ <FmButton
1573
+ variant="secondary"
1574
+ :label="t('order.displaySystem.legacyOds.openInNewTab')"
1575
+ :disabled="!previewUrl"
1576
+ @click="openPreviewInNewTab"
1577
+ />
1578
+ </div>
1579
+ </div>
1580
+ </div>
1581
+
1582
+ <div class="bg-fm-color-neutral-gray-50 p-16">
1583
+ <div
1584
+ v-if="previewUrl"
1585
+ ref="previewViewportRef"
1586
+ class="flex h-[300px] w-full items-center justify-center overflow-auto md:h-[400px] lg:h-[480px]"
1587
+ >
1588
+ <div
1589
+ class="relative flex-shrink-0 overflow-hidden rounded-[12px] border border-black/10 bg-white shadow-[0_8px_24px_rgba(15,23,42,0.16)]"
1590
+ :style="{
1591
+ width: `${ODS_PREVIEW_WIDTH * previewScale}px`,
1592
+ height: `${ODS_PREVIEW_HEIGHT * previewScale}px`
1593
+ }"
1594
+ >
1595
+ <div
1596
+ :style="{
1597
+ width: `${ODS_PREVIEW_WIDTH}px`,
1598
+ height: `${ODS_PREVIEW_HEIGHT}px`,
1599
+ transform: `scale(${previewScale})`,
1600
+ transformOrigin: 'top left'
1601
+ }"
1602
+ >
1603
+ <iframe
1604
+ :src="previewUrl"
1605
+ title="ODS Preview"
1606
+ :width="ODS_PREVIEW_WIDTH"
1607
+ :height="ODS_PREVIEW_HEIGHT"
1608
+ class="block border-0"
1609
+ />
1610
+ </div>
1611
+ </div>
1612
+ </div>
1613
+
1614
+ <div
1615
+ v-else
1616
+ class="flex h-[200px] items-center justify-center rounded-[16px] border border-dashed border-fm-color-neutral-gray-200 bg-white px-24 text-center"
1617
+ >
1618
+ <div class="space-y-8">
1619
+ <div class="fm-typo-en-body-md-600 text-fm-color-typo-primary">
1620
+ {{ t('order.displaySystem.legacyOds.previewUnavailable') }}
1621
+ </div>
1622
+ <div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
1623
+ Set a valid <code>VITE_ODS_PREVIEW_URL</code> to enable the embedded ODS
1624
+ preview.
1625
+ </div>
1626
+ </div>
1627
+ </div>
1628
+ </div>
1629
+ </div>
1630
+ </div>
1631
+ </div>
1632
+ </div>
1633
+ </div>
1634
+ </div>
1635
+ </Transition>
1636
+ <MediaPickerModal
1637
+ v-model:open="mediaPickerOpen"
1638
+ :z-index="70"
1639
+ :business-id="Core.currentBusiness.value?._id ?? ''"
1640
+ :kinds="['image']"
1641
+ @select="selectImage"
1642
+ />
1643
+ </Teleport>
1644
+
1645
+ <ColorOverrideDialog
1646
+ v-model="overrideDialogOpen"
1647
+ :title="t('order.displaySystem.legacyOds.colorOverrideTitle')"
1648
+ :description="t('order.displaySystem.legacyOds.colorOverrideDescription')"
1649
+ :color-fields="colorFields"
1650
+ :base-brandkit="baseBrandkit"
1651
+ :value="draft.brandkitOverride"
1652
+ @save="onSaveOverride"
1653
+ />
1654
+ </template>
1655
+
1656
+ <style scoped>
1657
+ .fade-enter-from {
1658
+ opacity: 0;
1659
+ }
1660
+
1661
+ .fade-enter-to {
1662
+ opacity: 1;
1663
+ }
1664
+
1665
+ .fade-leave-from {
1666
+ opacity: 1;
1667
+ }
1668
+
1669
+ .fade-leave-to {
1670
+ opacity: 0;
1671
+ }
1672
+
1673
+ .fade-enter-active,
1674
+ .fade-leave-active {
1675
+ transition: opacity 0.5s ease;
1676
+ }
1677
+ </style>