@feedmepos/mf-order-setting 0.0.90 → 0.0.91

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 (46) hide show
  1. package/dist/DisplaySystemView-n7Kqdw-h.js +6564 -0
  2. package/dist/{KioskDevicesView-WuOTJ9Fi.js → KioskDevicesView-Bz6seroM.js} +1 -1
  3. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-DdHfATyN.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-I39sE8Pe.js} +3 -3
  4. package/dist/{KioskSettingView-Bsja2OqT.js → KioskSettingView-Cajjh4Zm.js} +3 -3
  5. package/dist/{KioskView-BH5qGTM_.js → KioskView-BPSbsMi5.js} +93 -89
  6. package/dist/{OrderSettingsView-D6V1ztiS.js → OrderSettingsView-jPxZ3JDM.js} +5 -5
  7. package/dist/{app-DsMk7W2h.js → app-DIhv5ssA.js} +266 -254
  8. package/dist/app.js +1 -1
  9. package/dist/frontend/mf-order/src/app.d.ts +18 -6
  10. package/dist/frontend/mf-order/src/main.d.ts +18 -6
  11. package/dist/frontend/mf-order/src/modules/display-system/interface.d.ts +6 -3
  12. package/dist/frontend/mf-order/src/modules/kiosk/bindDeviceError.d.ts +20 -0
  13. package/dist/frontend/mf-order/src/views/display-system/CanvasArea.vue.d.ts +48 -0
  14. package/dist/frontend/mf-order/src/views/display-system/DisplayLayoutCanvas.vue.d.ts +17 -14
  15. package/dist/frontend/mf-order/src/views/display-system/DisplayModulePickerDialog.vue.d.ts +2 -0
  16. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  17. package/dist/{index-DGDiI0fr.js → index-Clp_YRJ2.js} +1 -1
  18. package/dist/{index-BUDM0Rqn.js → index-CmqNrkNl.js} +1 -1
  19. package/dist/{index-C2n1MV_1.js → index-vaSLX6r3.js} +2 -2
  20. package/dist/{order.dto-BRDWWgMV.js → order.dto-DV1qL3Q_.js} +21 -1
  21. package/dist/package/entity/alert/investigation.dto.d.ts +26 -0
  22. package/dist/package/entity/alert/lark-callback.dto.d.ts +19 -0
  23. package/dist/package/entity/alert/record-resolution.dto.d.ts +35 -0
  24. package/dist/package/entity/index.d.ts +1 -0
  25. package/dist/package/entity/kiosk/kiosk.do.d.ts +105 -0
  26. package/dist/package/entity/kiosk/kiosk.dto.d.ts +76 -0
  27. package/dist/style.css +1 -1
  28. package/package.json +3 -1
  29. package/src/locales/en-US.json +11 -4
  30. package/src/locales/zh-CN.json +9 -4
  31. package/src/modules/display-system/interface.spec.ts +90 -5
  32. package/src/modules/display-system/interface.ts +31 -21
  33. package/src/modules/kiosk/bindDeviceError.spec.ts +42 -0
  34. package/src/modules/kiosk/bindDeviceError.ts +40 -0
  35. package/src/views/display-system/CanvasArea.vue +355 -0
  36. package/src/views/display-system/DisplayLayoutCanvas.spec.ts +57 -0
  37. package/src/views/display-system/DisplayLayoutCanvas.vue +173 -298
  38. package/src/views/display-system/DisplayModulePickerDialog.spec.ts +71 -0
  39. package/src/views/display-system/DisplayModulePickerDialog.vue +107 -0
  40. package/src/views/display-system/DisplayTemplateEditor.spec.ts +151 -0
  41. package/src/views/display-system/DisplayTemplateEditor.vue +233 -193
  42. package/src/views/kiosk/KioskView.vue +2 -4
  43. package/vitest.config.ts +2 -0
  44. package/dist/DisplaySystemView-tosCa7Ej.js +0 -6485
  45. package/dist/frontend/mf-order/src/views/display-system/DisplayModulePalette.vue.d.ts +0 -13
  46. package/src/views/display-system/DisplayModulePalette.vue +0 -140
@@ -0,0 +1,71 @@
1
+ // @vitest-environment happy-dom
2
+ import { expect, it, vi } from 'vitest'
3
+ import { mount } from '@vue/test-utils'
4
+ import { nextTick, ref } from 'vue'
5
+ import { FeedMeUI } from '@feedmepos/ui-library'
6
+ import { defaultDisplayTemplateDraft } from '@/modules/display-system/interface'
7
+
8
+ vi.mock('@feedmepos/media-picker', () => ({ MediaPickerModal: { template: '<div />' } }))
9
+ vi.mock('@feedmepos/mf-common', () => ({
10
+ useI18n: () => ({ t: (key: string) => key }),
11
+ useCoreStore: () => ({
12
+ currentBusiness: ref({}),
13
+ currentCountry: ref('MY'),
14
+ namespace: ref(''),
15
+ restaurants: ref([])
16
+ })
17
+ }))
18
+ vi.mock('@/api/brandkit', () => ({ getRestaurantDisplayBrandKit: vi.fn() }))
19
+ vi.mock('./DisplayPreview.vue', () => ({ default: { template: '<iframe />' } }))
20
+ vi.mock('./DisplayInspector.vue', () => ({ default: { template: '<div />' } }))
21
+ import Editor from './DisplayTemplateEditor.vue'
22
+ import Picker from './DisplayModulePickerDialog.vue'
23
+
24
+ it('unmounts every replacement picker and fires onClose with ui-library 1.10.13', async () => {
25
+ const unmounted = vi.fn()
26
+ const closed = vi.fn()
27
+ const originalMixins = Picker.mixins
28
+ Picker.mixins = [...(originalMixins ?? []), { unmounted }]
29
+ const w = mount(Editor, {
30
+ props: { modelValue: false, template: defaultDisplayTemplateDraft('ads') as never },
31
+ global: { plugins: [FeedMeUI], stubs: { Teleport: true } }
32
+ })
33
+ const dialog = (
34
+ w.vm as unknown as {
35
+ $dialog: {
36
+ currentDialog: { dialogCount: number }
37
+ open: (options: unknown) => { onClose: (cb: () => void) => unknown }
38
+ close: () => void
39
+ }
40
+ }
41
+ ).$dialog
42
+ const originalOpen = dialog.open.bind(dialog)
43
+ const spy = vi.spyOn(dialog, 'open').mockImplementation((options) => {
44
+ const result = originalOpen(options)
45
+ result.onClose(closed)
46
+ return result
47
+ })
48
+ try {
49
+ await w.setProps({ modelValue: true })
50
+ for (let count = 1; count <= 3; count += 1) {
51
+ await w.find('.canvas-chip-swap').trigger('click')
52
+ await nextTick()
53
+ const pick = document.querySelector<HTMLButtonElement>('.module-picker-card')!
54
+ expect(pick).toBeTruthy()
55
+ pick.click()
56
+ await nextTick()
57
+ expect(unmounted).toHaveBeenCalledTimes(count)
58
+ expect(closed).toHaveBeenCalledTimes(count * 2 - 1)
59
+ expect(document.querySelector('.module-picker')).toBeNull()
60
+ expect(dialog.currentDialog.dialogCount).toBe(1)
61
+ dialog.close()
62
+ await nextTick()
63
+ expect(dialog.currentDialog.dialogCount).toBe(0)
64
+ }
65
+ } finally {
66
+ while (dialog.currentDialog.dialogCount > 0) dialog.close()
67
+ spy.mockRestore()
68
+ w.unmount()
69
+ Picker.mixins = originalMixins
70
+ }
71
+ })
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useI18n } from '@feedmepos/mf-common'
4
+ import { useDialogChild } from '@feedmepos/ui-library'
5
+ import { DISPLAY_BLOCK_DEFINITIONS } from '@entity'
6
+
7
+ const { t } = useI18n()
8
+ const { emitData, emitAction } = useDialogChild<string>()
9
+
10
+ const GROUP_ORDER = ['pickup', 'queue', 'media', 'info'] as const
11
+
12
+ const groups = computed(() =>
13
+ GROUP_ORDER.map((group) => ({
14
+ group,
15
+ label: t(`order.displaySystem.editor.ui.group${group.charAt(0).toUpperCase()}${group.slice(1)}`),
16
+ items: DISPLAY_BLOCK_DEFINITIONS.filter((definition) => definition.editor.group === group)
17
+ })).filter((entry) => entry.items.length)
18
+ )
19
+
20
+ function pick(type: string): void {
21
+ emitData(type)
22
+ emitAction('primary')
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="module-picker">
28
+ <p class="module-picker-hint">{{ t('order.displaySystem.editor.ui.pickModuleHint') }}</p>
29
+ <section v-for="entry in groups" :key="entry.group" class="module-picker-group">
30
+ <h4 class="module-picker-group-label">{{ entry.label }}</h4>
31
+ <div class="module-picker-grid">
32
+ <button
33
+ v-for="definition in entry.items"
34
+ :key="definition.type"
35
+ type="button"
36
+ class="module-picker-card"
37
+ @click="pick(definition.type)"
38
+ >
39
+ <span class="module-picker-icon"><FmIcon :name="definition.editor.icon" size="sm" /></span>
40
+ <span class="module-picker-label">{{ t(definition.editor.labelKey) }}</span>
41
+ </button>
42
+ </div>
43
+ </section>
44
+ </div>
45
+ </template>
46
+
47
+ <style scoped>
48
+ .module-picker {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: 18px;
52
+ }
53
+ .module-picker-hint {
54
+ margin: 0;
55
+ color: #6b7280;
56
+ font-size: 12px;
57
+ }
58
+ .module-picker-group {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 8px;
62
+ }
63
+ .module-picker-group-label {
64
+ margin: 0;
65
+ color: #9ca3af;
66
+ font-size: 10px;
67
+ font-weight: 700;
68
+ letter-spacing: 0.06em;
69
+ text-transform: uppercase;
70
+ }
71
+ .module-picker-grid {
72
+ display: grid;
73
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
74
+ gap: 8px;
75
+ }
76
+ .module-picker-card {
77
+ display: flex;
78
+ flex-direction: column;
79
+ align-items: center;
80
+ gap: 8px;
81
+ border: 1px solid #e5e7eb;
82
+ border-radius: 10px;
83
+ padding: 14px 10px;
84
+ background: white;
85
+ color: #374151;
86
+ font-size: 13px;
87
+ text-align: center;
88
+ cursor: pointer;
89
+ }
90
+ .module-picker-card:hover {
91
+ border-color: #f97316;
92
+ background: #fffbf5;
93
+ }
94
+ .module-picker-icon {
95
+ display: grid;
96
+ height: 36px;
97
+ width: 36px;
98
+ place-items: center;
99
+ border-radius: 8px;
100
+ background: #f3f4f6;
101
+ color: #4b5563;
102
+ }
103
+ .module-picker-label {
104
+ line-height: 1.3;
105
+ overflow-wrap: anywhere;
106
+ }
107
+ </style>
@@ -0,0 +1,151 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { mount, type VueWrapper } from '@vue/test-utils'
4
+ import { nextTick, ref } from 'vue'
5
+ import { FdtoCreateDisplayTemplate } from '@entity'
6
+ import {
7
+ defaultDisplayTemplateDraft,
8
+ displayTemplatePayload,
9
+ walkZones,
10
+ type DisplaySplitDraft
11
+ } from '@/modules/display-system/interface'
12
+
13
+ const dialog = vi.hoisted(() => ({ open: vi.fn(), close: vi.fn() }))
14
+ vi.mock('@feedmepos/ui-library', () => ({ useDialog: () => dialog }))
15
+ vi.mock('@feedmepos/media-picker', () => ({ MediaPickerModal: { template: '<div />' } }))
16
+ vi.mock('@feedmepos/mf-common', () => ({
17
+ useI18n: () => ({ t: (key: string) => key }),
18
+ useCoreStore: () => ({
19
+ currentBusiness: ref({}),
20
+ currentCountry: ref('MY'),
21
+ namespace: ref(''),
22
+ restaurants: ref([])
23
+ })
24
+ }))
25
+ vi.mock('@/api/brandkit', () => ({ getRestaurantDisplayBrandKit: vi.fn() }))
26
+ vi.mock('./DisplayPreview.vue', () => ({ default: { template: '<iframe />' } }))
27
+ vi.mock('./DisplayInspector.vue', () => ({ default: { template: '<div />' } }))
28
+ import Editor from './DisplayTemplateEditor.vue'
29
+ import Canvas from './DisplayLayoutCanvas.vue'
30
+
31
+ const wrappers: VueWrapper[] = []
32
+ afterEach(() => {
33
+ wrappers.forEach((w) => w.unmount())
34
+ wrappers.length = 0
35
+ vi.clearAllMocks()
36
+ })
37
+ async function editor() {
38
+ const draft = defaultDisplayTemplateDraft('ads')
39
+ draft.label = 'Board and ads'
40
+ walkZones(draft.root).forEach((zone) => {
41
+ if (zone.block?.type === 'media.image')
42
+ zone.block.props.url = 'https://image.feedme.cc/promo.png'
43
+ if (zone.block?.type === 'media.carousel')
44
+ zone.block.props.items = [
45
+ { kind: 'IMAGE', url: 'https://image.feedme.cc/ad.png', objectFit: 'COVER' }
46
+ ]
47
+ })
48
+ const w = mount(Editor, {
49
+ props: { modelValue: false, template: draft as never },
50
+ global: {
51
+ stubs: {
52
+ Teleport: true,
53
+ FmTooltip: { template: '<div><slot /></div>' },
54
+ FmButton: {
55
+ props: ['label', 'disabled'],
56
+ template: '<button :disabled="disabled">{{ label }}</button>'
57
+ },
58
+ FmIcon: true,
59
+ FmTextField: true,
60
+ FmStepperField: true,
61
+ FmSelect: true,
62
+ FmSwitch: true
63
+ }
64
+ }
65
+ })
66
+ wrappers.push(w)
67
+ await w.setProps({ modelValue: true })
68
+ return w
69
+ }
70
+ function button(w: VueWrapper, suffix: string) {
71
+ return w.findAll('button').find((b) => b.text().endsWith(suffix))!
72
+ }
73
+ function root(w: VueWrapper) {
74
+ return w.findComponent(Canvas).props('root') as DisplaySplitDraft
75
+ }
76
+
77
+ describe('DisplayTemplateEditor review regressions', () => {
78
+ it('offers a top-level add action for Board + ads and stops at the cap', async () => {
79
+ dialog.open.mockReturnValue({ onPrimary: vi.fn() })
80
+ const w = await editor()
81
+ for (let count = 2; count < 6; count += 1) {
82
+ const add = button(w, '.addArea')
83
+ expect(add).toBeDefined()
84
+ await add.trigger('click')
85
+ expect(root(w).children).toHaveLength(count + 1)
86
+ }
87
+ expect(button(w, '.addArea').attributes('disabled')).toBeDefined()
88
+ expect(
89
+ walkZones(root(w))
90
+ .slice(0, 3)
91
+ .map((zone) => zone.block?.type)
92
+ ).toEqual(['pickup.categoryBoard', 'media.image', 'media.carousel'])
93
+ })
94
+
95
+ it('toggles nested directions and emits a Save payload accepted by the API', async () => {
96
+ const w = await editor()
97
+ const baseline = FdtoCreateDisplayTemplate.safeParse(
98
+ displayTemplatePayload({ ...defaultDisplayTemplateDraft(), label: 'Valid', root: root(w) })
99
+ )
100
+ expect(baseline.success, JSON.stringify(baseline)).toBe(true)
101
+ await button(w, '.directionRow').trigger('click')
102
+ expect((root(w).children[1]!.node as DisplaySplitDraft).direction).toBe('ROW')
103
+ await button(w, '.save').trigger('click')
104
+ const saved = w.emitted('save')![0]![0] as ReturnType<typeof defaultDisplayTemplateDraft>
105
+ expect(FdtoCreateDisplayTemplate.safeParse(displayTemplatePayload(saved)).success).toBe(true)
106
+ })
107
+
108
+ it('confirms populated removal, leaves it on cancel, and removes empty areas immediately', async () => {
109
+ let confirm: (() => void) | undefined
110
+ dialog.open.mockReturnValue({
111
+ onPrimary: (cb: () => void) => {
112
+ confirm = cb
113
+ }
114
+ })
115
+ const w = await editor()
116
+ await w.find('.canvas-area-remove').trigger('click')
117
+ expect(walkZones(root(w))).toHaveLength(3)
118
+ expect(dialog.open).toHaveBeenCalledWith(
119
+ expect.objectContaining({ title: expect.stringContaining('removeAreaConfirmTitle') })
120
+ )
121
+ // Cancelling never invokes the primary callback.
122
+ expect(walkZones(root(w))[0]!.block?.type).toBe('pickup.categoryBoard')
123
+ confirm!()
124
+ await nextTick()
125
+ expect(walkZones(root(w))).toHaveLength(2)
126
+ const zone = walkZones(root(w))[1]!
127
+ delete zone.block
128
+ await nextTick()
129
+ dialog.open.mockClear()
130
+ await w.findAll('.canvas-area-remove')[1]!.trigger('click')
131
+ expect(dialog.open).not.toHaveBeenCalled()
132
+ expect(w.findComponent(Canvas).props('root').kind).toBe('ZONE')
133
+ })
134
+
135
+ it('closes the picker before opening replacement confirmation', async () => {
136
+ let pick: ((type: string) => void) | undefined
137
+ dialog.open.mockReturnValue({
138
+ onPrimary: (cb: (type: string) => void) => {
139
+ pick = cb
140
+ }
141
+ })
142
+ const w = await editor()
143
+ await w.find('.canvas-chip-swap').trigger('click')
144
+ pick!('info.text')
145
+ await nextTick()
146
+ expect(dialog.close).toHaveBeenCalledTimes(1)
147
+ expect(dialog.close.mock.invocationCallOrder[0]).toBeLessThan(
148
+ dialog.open.mock.invocationCallOrder[1]!
149
+ )
150
+ })
151
+ })