@feedmepos/mf-order-setting 0.0.49 → 0.0.51

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 (64) hide show
  1. package/dist/{KioskDevicesView-Ch_mWJz9.js → KioskDevicesView-TIls1ag1.js} +1 -1
  2. package/dist/KioskDevicesView.vue_vue_type_script_setup_true_lang-B2gjPfOJ.js +501 -0
  3. package/dist/KioskSettingView-UPE-q-Zd.js +573 -0
  4. package/dist/KioskView-B0Jj2sOl.js +477 -0
  5. package/dist/{OrderSettingsView-CRgoLOD2.js → OrderSettingsView-Ca4y2PNF.js} +4 -4
  6. package/dist/{app-CDTAjXj9.js → app-JqOEacuf.js} +75 -19
  7. package/dist/app.js +1 -1
  8. package/dist/{dayjs.min-dI_j30pv.js → dayjs.min-B42nUieJ.js} +1 -1
  9. package/dist/frontend/mf-order/src/app.d.ts +56 -0
  10. package/dist/frontend/mf-order/src/main.d.ts +56 -0
  11. package/dist/frontend/mf-order/src/modules/kiosk/interface.d.ts +1 -0
  12. package/dist/frontend/mf-order/src/modules/order-setting/kiosk/interface.d.ts +2 -0
  13. package/dist/frontend/mf-order/src/stores/kiosk/index.d.ts +3 -0
  14. package/dist/frontend/mf-order/src/stores/order-setting/index.d.ts +6 -0
  15. package/dist/frontend/mf-order/src/views/kiosk/devices/KioskDevicesView.vue.d.ts +6 -3
  16. package/dist/frontend/mf-order/src/views/order-settings/servicecharge/ServiceChargeRule.vue.d.ts +2 -2
  17. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  18. package/dist/{index-B8U5Sawr.js → index-B0teNm7_.js} +2 -2
  19. package/dist/{menu.dto-qVeqpSdz.js → menu.dto-DFTxveX1.js} +6355 -6287
  20. package/dist/package/entity/incoming-order/incoming-order-to-bill.dto.d.ts +119 -12
  21. package/dist/package/entity/incoming-order/incoming-order.dto.d.ts +124 -6
  22. package/dist/package/entity/kiosk/kiosk.do.d.ts +32 -0
  23. package/dist/package/entity/kiosk/kiosk.dto.d.ts +44 -0
  24. package/dist/package/entity/order/order-item/order-item.dto.d.ts +30 -0
  25. package/dist/package/entity/order/order.do.d.ts +8 -0
  26. package/dist/package/entity/order/order.dto.d.ts +118 -0
  27. package/dist/package/entity/order/order.enum.d.ts +2 -0
  28. package/dist/package/entity/order-platform/external/menu/external-master-menu.do.d.ts +20 -0
  29. package/dist/package/entity/order-platform/external/menu/external-menu.do.d.ts +23 -0
  30. package/dist/package/entity/order-platform/external/order/external-order.do.d.ts +62 -0
  31. package/dist/package/entity/order-platform/external/order/external-order.dto.d.ts +178 -0
  32. package/dist/package/entity/order-platform/grabfood/grabfood-menu.do.d.ts +15 -0
  33. package/dist/package/entity/order-platform/grabfood/grabfood-omni.do.d.ts +1834 -15
  34. package/dist/package/entity/order-platform/grabfood/grabfood-order.do.d.ts +13 -13
  35. package/dist/package/entity/order-platform/grabfood/grabfood-settings.do.d.ts +3 -0
  36. package/dist/package/entity/order-platform/grabfood/grabfood-webhook.dto.d.ts +528 -805
  37. package/dist/package/entity/order-platform/grabfood/grabfood.dto.d.ts +8 -5
  38. package/dist/package/entity/order-platform/grabfood/grabfood.enum.d.ts +1 -1
  39. package/dist/package/entity/order-platform/menu.dto.d.ts +34 -0
  40. package/dist/package/entity/order-platform/order-platform.dto.d.ts +0 -20
  41. package/dist/package/entity/order-setting/kiosk/kiosk.do.d.ts +3 -0
  42. package/dist/package/entity/order-setting/kiosk/kiosk.dto.d.ts +3 -0
  43. package/dist/package/entity/order-setting/order-setting.do.d.ts +5 -0
  44. package/dist/package/entity/order-setting/order-setting.dto.d.ts +10 -0
  45. package/dist/package/entity/queue/queue.dto.d.ts +45 -0
  46. package/dist/package/entity/restaurant/restaurant.dto.d.ts +5 -0
  47. package/dist/package/entity/websocket/websocket.dto.d.ts +12 -0
  48. package/package.json +1 -1
  49. package/src/locales/en-US.json +15 -1
  50. package/src/locales/ja-JP.json +23 -9
  51. package/src/locales/th-TH.json +15 -1
  52. package/src/locales/zh-CN.json +15 -1
  53. package/src/modules/kiosk/interface.ts +1 -0
  54. package/src/modules/order-setting/kiosk/interface.ts +15 -13
  55. package/src/stores/kiosk/mapper.ts +1 -0
  56. package/src/stores/order-setting/mapper.ts +16 -13
  57. package/src/views/kiosk/KioskSummary.vue +37 -31
  58. package/src/views/kiosk/KioskView.vue +4 -1
  59. package/src/views/kiosk/devices/KioskDeviceCard.vue +205 -89
  60. package/src/views/kiosk/devices/KioskDevicesView.vue +133 -17
  61. package/src/views/kiosk/settings/KioskSettingView.vue +43 -23
  62. package/dist/KioskDevicesView.vue_vue_type_script_setup_true_lang-DV2HYd8u.js +0 -306
  63. package/dist/KioskSettingView-CaBhf48e.js +0 -553
  64. package/dist/KioskView-BGm-emCw.js +0 -452
@@ -77,7 +77,8 @@ export const defaultKioskOrderSetting: MfKioskOrderSettingForm = {
77
77
  },
78
78
  menuItem: {
79
79
  showAllOnly: false
80
- }
80
+ },
81
+ otaChannel: ''
81
82
  }
82
83
 
83
84
  export const defaultSubmitOrderInstruction: MfKioskSubmitOrderInstruction = {
@@ -131,7 +132,8 @@ const toKiosk = (
131
132
  },
132
133
  menuItem: {
133
134
  showAllOnly: kioskOrderSettings?.menuItem?.showAllOnly ?? false
134
- }
135
+ },
136
+ otaChannel: kioskOrderSettings?.otaChannel ?? ''
135
137
  }
136
138
  }
137
139
 
@@ -221,19 +223,20 @@ const toOrderKioskSettingsDto = (
221
223
  dineIn,
222
224
  menuItem: kioskSetting.menuItem,
223
225
  paymentSetting: createPaymentSettings(kioskSetting.paymentSetting),
226
+ otaChannel: kioskSetting.otaChannel || undefined,
224
227
  takeaway: kioskSetting.takeaway.submitOrderInstruction
225
228
  ? {
226
- submitOrderInstruction: {
227
- payAtCounter: kioskSetting.takeaway.submitOrderInstruction.payAtCounter
228
- ? {
229
- en: kioskSetting.takeaway.submitOrderInstruction.payAtCounter
230
- }
231
- : null,
232
- paid: kioskSetting.takeaway.submitOrderInstruction.paid
233
- ? {
234
- en: kioskSetting.takeaway.submitOrderInstruction.paid
235
- }
236
- : null
229
+ submitOrderInstruction: {
230
+ payAtCounter: kioskSetting.takeaway.submitOrderInstruction.payAtCounter
231
+ ? {
232
+ en: kioskSetting.takeaway.submitOrderInstruction.payAtCounter
233
+ }
234
+ : null,
235
+ paid: kioskSetting.takeaway.submitOrderInstruction.paid
236
+ ? {
237
+ en: kioskSetting.takeaway.submitOrderInstruction.paid
238
+ }
239
+ : null
237
240
  }
238
241
  }
239
242
  : undefined
@@ -2,44 +2,50 @@
2
2
  <div :class="dialogType ? '' : ['p-[1.5rem] border fm-corner-radius-lg flex flex-col gap-5 fixed ml-40 w-[320px]']">
3
3
  <span v-if="!dialogType" class="fm-typo-en-title-sm-600">{{ t('order.kioskSummary') }}</span>
4
4
  <hr v-if="!dialogType" class="my-[0.25rem]" />
5
- <div class="flex flex-col gap-1">
6
- <div class="flex flex-col gap-1">
5
+ <div class="flex flex-col gap-4">
6
+ <!-- Device Summary -->
7
+ <div class="flex flex-col gap-2">
7
8
  <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.kioskDisplay') }}</span>
8
-
9
- <div class="fm-typo-en-body-lg-600">
10
- <div v-if="devices.length > 0">
11
- <li class="block" v-for="device in devices" :key="device.machineId">{{ device.name }}</li>
9
+ <div v-if="devices.length > 0" class="flex flex-col gap-1">
10
+ <div
11
+ v-for="device in devices"
12
+ :key="device.machineId"
13
+ class="fm-typo-en-body-sm-400 flex items-center gap-2"
14
+ >
15
+ <span class="px-1.5 py-0.5 fm-corner-radius-sm bg-fm-color-neutral-gray-100 text-fm-color-typo-secondary fm-typo-en-body-xs-600 uppercase">
16
+ {{ device.deviceAppType === 'TABLET_APP' ? 'T' : 'K' }}
17
+ </span>
18
+ <span v-if="device.slotInfo" class="fm-typo-en-body-sm-600">{{ device.slotInfo }}</span>
19
+ <span class="text-fm-color-typo-secondary truncate flex-1" :title="device.name">{{ device.name }}</span>
12
20
  </div>
13
- <div v-else>-</div>
14
21
  </div>
22
+ <div v-else class="fm-typo-en-body-lg-600">-</div>
23
+ </div>
15
24
 
16
- <div v-if="dineInStatus === t('order.activated')">
17
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.dineIn') }}</span>
18
- <div class="fm-typo-en-body-lg-600 block">
19
- <div>{{ dineInStatus }}</div>
20
- </div>
21
- </div>
25
+ <!-- Dine In Status -->
26
+ <div v-if="dineInStatus === t('order.activated')" class="flex flex-col gap-1">
27
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.dineIn') }}</span>
28
+ <div class="fm-typo-en-body-lg-600">{{ dineInStatus }}</div>
29
+ </div>
22
30
 
23
- <div v-if="dineInStatus === t('order.activated') && dineInType !== t('order.unknown')">
24
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.dineInType') }}</span>
25
- <div class="fm-typo-en-body-lg-600 block">
26
- <div>{{ dineInType }}</div>
27
- </div>
28
- </div>
31
+ <!-- Dine In Type -->
32
+ <div v-if="dineInStatus === t('order.activated') && dineInType !== t('order.unknown')" class="flex flex-col gap-1">
33
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.dineInType') }}</span>
34
+ <div class="fm-typo-en-body-lg-600">{{ dineInType }}</div>
35
+ </div>
29
36
 
30
- <div v-if="takeAwayStatus === t('order.activated')">
31
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.takeaway') }}</span>
32
- <div class="fm-typo-en-body-lg-600 block">
33
- <div>{{ takeAwayStatus }}</div>
34
- </div>
35
- </div>
37
+ <!-- Takeaway Status -->
38
+ <div v-if="takeAwayStatus === t('order.activated')" class="flex flex-col gap-1">
39
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.takeaway') }}</span>
40
+ <div class="fm-typo-en-body-lg-600">{{ takeAwayStatus }}</div>
41
+ </div>
36
42
 
37
- <div v-if="acceptedPaymentStatus || offlinePaymentStatus">
38
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{t('order.acceptedPayment')}}</span>
39
- <div class="fm-typo-en-body-lg-600 block">
40
- <div v-if="offlinePaymentStatus">{{ t('order.offlinePayment') }}</div>
41
- <div>{{ acceptedPaymentStatus }}</div>
42
- </div>
43
+ <!-- Payment Status -->
44
+ <div v-if="acceptedPaymentStatus || offlinePaymentStatus" class="flex flex-col gap-1">
45
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ t('order.acceptedPayment') }}</span>
46
+ <div class="fm-typo-en-body-lg-600">
47
+ <div v-if="offlinePaymentStatus">{{ t('order.offlinePayment') }}</div>
48
+ <div>{{ acceptedPaymentStatus }}</div>
43
49
  </div>
44
50
  </div>
45
51
  </div>
@@ -29,7 +29,7 @@
29
29
  <KioskDevicesView
30
30
  v-if="selectedMenuItem == 'device'"
31
31
  :devices="devices"
32
- :request-otp="requestOtp"
32
+ @request-otp="requestOtp"
33
33
  />
34
34
  <KioskSettingView
35
35
  :key="`KioskSettingView-${currentRestaurantId}`"
@@ -151,6 +151,9 @@ async function requestOtp() {
151
151
  contentComponentProps: {
152
152
  otp: KioskStore.state.otp
153
153
  }
154
+ }).onClose(async () => {
155
+ // Refresh devices after dialog closes (in case a device was bound)
156
+ await KioskStore.getDevices()
154
157
  })
155
158
  }
156
159
 
@@ -1,104 +1,185 @@
1
1
  <template>
2
- <div class="border fm-corner-radius-lg flex justify-between pr-[1rem] w-11/12">
3
- <FmSideSheet
4
- :header="t('order.deviceDetails')"
5
- dismiss-away
6
- class="w-full"
7
- :maxWidth="700"
8
- @on:clickedAway="closeUpdateDevicePin"
9
- >
10
- <template #side-sheet-button>
11
- <FmListItem
12
- class="flex-1"
13
- :label="device.name"
14
- :sublabel="`${t('order.activatedDate')}: ${dayjs(device.activatedAt).format('DD MMM YYYY HH:mm')}`"
2
+ <div class="border fm-corner-radius-lg p-12 hover:bg-fm-color-neutral-gray-50 transition-colors">
3
+ <!-- Header Row: Type Badge + Slot + Actions -->
4
+ <div class="flex items-center justify-between mb-2">
5
+ <div class="flex items-center gap-2">
6
+ <span
7
+ class="px-2 py-1 fm-corner-radius-md fm-typo-en-body-sm-600 uppercase bg-fm-color-neutral-gray-200 text-fm-color-typo-secondary"
15
8
  >
16
- </FmListItem>
17
- </template>
18
- <div v-if="!showPinSetting" class="py-[1rem] flex flex-col gap-6">
19
- <span class="fm-typo-en-title-sm-600">{{ device.name }}</span>
20
- <div class="w-full border p-[1.5rem] fm-corner-radius-lg flex flex-col gap-2">
21
- <span class="fm-typo-en-title-sm-600 mb-3">App Info</span>
22
- <div class="flex flex-col gap-1 mb-2">
23
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">Type</span>
24
- <span class="fm-typo-en-body-lg-600 block">{{ appType }}</span>
25
- </div>
26
- <div class="flex flex-col gap-1 mb-2">
27
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">Version</span>
28
- <span class="fm-typo-en-body-lg-600 block">{{
29
- device.deviceAppVersion ?? 'Below 0.0.42'
30
- }}</span>
31
- </div>
32
- <div class="flex flex-col gap-1 mb-2">
33
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">OTA Version</span>
34
- <span class="fm-typo-en-body-lg-600 block">{{
35
- device.deviceAppOTAVersion ?? 'Not available'
36
- }}</span>
37
- </div>
38
- <div class="flex flex-col gap-1 mb-2">
39
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">PIN Code</span>
40
- <span class="fm-typo-en-body-lg-600 block">{{ isPinUpdate ? 'Yes' : 'No' }}</span>
41
- </div>
42
- <FmButton
43
- class="w-auto"
44
- variant="primary"
45
- :label="device?.pinInfo ? 'Reset Device PIN' : 'Set Device PIN'"
46
- @click="showUpdateDevicePin"
47
- />
48
- </div>
49
- <div
50
- v-for="config in configs.filter((c) => c[1])"
51
- class="w-full border p-[1.5rem] fm-corner-radius-lg flex flex-col gap-2"
9
+ {{ isTabletApp ? 'Tablet' : 'Kiosk' }}
10
+ </span>
11
+ <span
12
+ v-if="isTabletApp && device.slotInfo"
13
+ class="fm-typo-en-body-md-600 text-fm-color-typo-primary"
52
14
  >
53
- <span class="fm-typo-en-title-sm-600" v-if="config[1]">{{ config[0] }}</span>
54
- <div
55
- class="flex flex-col gap-1"
56
- v-if="config[1]"
57
- v-for="configEntry in Object.entries(config[1])"
58
- >
59
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{
60
- configEntry[0]
61
- }}</span>
62
- <span class="fm-typo-en-body-lg-600 block">{{
63
- configEntry[1] == '' ? '-' : configEntry[1]
64
- }}</span>
65
- </div>
66
- </div>
15
+ {{ device.slotInfo }}
16
+ </span>
67
17
  </div>
68
- <div v-else>
69
- <div class="py-[1rem] flex flex-col gap-6 text-center items-center">
70
- <div class="flex flex-col gap-1 mb-2">
71
- <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{ pinText }}</span>
72
- <span class="mt-5 block">
73
- <FmPinField v-if="!isPinEntered" :length="4" @complete="(val) => pinEntered(val)" />
74
- <FmPinField v-if="isPinEntered" :length="4" @complete="(val) => pinEntered(val)" />
75
- </span>
18
+ <div class="flex items-center gap-1">
19
+ <FmSideSheet
20
+ :header="t('order.deviceDetails')"
21
+ dismiss-away
22
+ :maxWidth="700"
23
+ @on:clickedAway="closeUpdateDevicePin"
24
+ >
25
+ <template #side-sheet-button>
26
+ <FmButton variant="tertiary" icon="more_vert" />
27
+ </template>
28
+ <div v-if="!showPinSetting" class="py-[1rem] flex flex-col gap-6">
29
+ <span class="fm-typo-en-title-sm-600">{{ device.name }}</span>
30
+ <div class="w-full border p-[1.5rem] fm-corner-radius-lg flex flex-col gap-2">
31
+ <span class="fm-typo-en-title-sm-600 mb-3">App Info</span>
32
+ <div class="flex flex-col gap-1 mb-2">
33
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">Type</span>
34
+ <span class="fm-typo-en-body-lg-600 block">{{ appType }}</span>
35
+ </div>
36
+ <div class="flex flex-col gap-1 mb-2">
37
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">Machine ID</span>
38
+ <span class="fm-typo-en-body-lg-600 block font-mono text-sm break-all">{{
39
+ device.machineId
40
+ }}</span>
41
+ </div>
42
+ <div class="flex flex-col gap-1 mb-2">
43
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">Version</span>
44
+ <span class="fm-typo-en-body-lg-600 block">{{
45
+ device.deviceAppVersion ?? 'Below 0.0.42'
46
+ }}</span>
47
+ </div>
48
+ <div class="flex flex-col gap-1 mb-2">
49
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">OTA Version</span>
50
+ <span class="fm-typo-en-body-lg-600 block">{{
51
+ device.deviceAppOTAVersion ?? 'Not available'
52
+ }}</span>
53
+ </div>
54
+ <div class="flex flex-col gap-1 mb-2">
55
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">PIN Code</span>
56
+ <span class="fm-typo-en-body-lg-600 block">{{ isPinUpdate ? 'Yes' : 'No' }}</span>
57
+ </div>
58
+ <FmButton
59
+ class="w-auto"
60
+ variant="primary"
61
+ :label="device?.pinInfo ? 'Reset Device PIN' : 'Set Device PIN'"
62
+ @click="showUpdateDevicePin"
63
+ />
64
+ </div>
65
+ <div
66
+ v-for="config in configs.filter((c) => c[1])"
67
+ :key="config[0]"
68
+ class="w-full border p-[1.5rem] fm-corner-radius-lg flex flex-col gap-2"
69
+ >
70
+ <span class="fm-typo-en-title-sm-600" v-if="config[1]">{{ config[0] }}</span>
71
+ <div
72
+ class="flex flex-col gap-1"
73
+ v-if="config[1]"
74
+ v-for="configEntry in Object.entries(config[1])"
75
+ :key="configEntry[0]"
76
+ >
77
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{
78
+ configEntry[0]
79
+ }}</span>
80
+ <span class="fm-typo-en-body-lg-600 block">{{
81
+ configEntry[1] == '' ? '-' : configEntry[1]
82
+ }}</span>
83
+ </div>
84
+ </div>
76
85
  </div>
77
- <FmButton
78
- class="w-1/3"
79
- variant="secondary"
80
- label="Cancel reset PIN"
81
- @click="closeUpdateDevicePin"
82
- />
83
- </div>
86
+ <div v-else>
87
+ <div class="py-[1rem] flex flex-col gap-6 text-center items-center">
88
+ <div class="flex flex-col gap-1 mb-2">
89
+ <span class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary">{{
90
+ pinText
91
+ }}</span>
92
+ <span class="mt-5 block">
93
+ <FmPinField
94
+ v-if="!isPinEntered"
95
+ :length="4"
96
+ @complete="(val) => pinEntered(val)"
97
+ />
98
+ <FmPinField
99
+ v-if="isPinEntered"
100
+ :length="4"
101
+ @complete="(val) => pinEntered(val)"
102
+ />
103
+ </span>
104
+ </div>
105
+ <FmButton
106
+ class="w-1/3"
107
+ variant="secondary"
108
+ label="Cancel reset PIN"
109
+ @click="closeUpdateDevicePin"
110
+ />
111
+ </div>
112
+ </div>
113
+ </FmSideSheet>
114
+ <FmButton
115
+ variant="plain"
116
+ icon="link_off"
117
+ class="text-fm-color-system-error-300"
118
+ @click="openUnbindDialog"
119
+ />
84
120
  </div>
85
- </FmSideSheet>
86
- <FmButton
87
- variant="plain"
88
- append-icon="link_off"
89
- class="text-fm-color-system-error-300 align-middle my-auto"
90
- @click="openUnbindDialog"
91
- />
121
+ </div>
122
+
123
+ <!-- Device Name (truncated for long names) -->
124
+ <div
125
+ class="fm-typo-en-body-md-600 text-fm-color-typo-primary mb-1 overflow-hidden text-ellipsis whitespace-nowrap"
126
+ :title="device.name"
127
+ >
128
+ {{ device.name }}
129
+ </div>
130
+
131
+ <!-- Machine ID Row (clickable to copy) -->
132
+ <div
133
+ class="flex items-center gap-1 mb-2 cursor-pointer group"
134
+ @click="copyMachineId"
135
+ :title="t('order.clickToCopy')"
136
+ >
137
+ <span class="fm-typo-en-body-sm-400 text-fm-color-typo-tertiary font-mono">
138
+ {{ truncatedMachineId }}
139
+ </span>
140
+ <FmIcon
141
+ name="content_copy"
142
+ class="text-fm-color-typo-tertiary opacity-0 group-hover:opacity-100 transition-opacity"
143
+ />
144
+ </div>
145
+
146
+ <!-- Info Row: Version, OTA, PIN, Last Startup -->
147
+ <div
148
+ class="flex items-center gap-3 flex-wrap fm-typo-en-body-sm-400 text-fm-color-typo-secondary"
149
+ >
150
+ <span class="flex items-center gap-1">
151
+ <FmIcon name="smartphone" />
152
+ v{{ device.deviceAppVersion ?? '?' }}
153
+ </span>
154
+ <span v-if="device.deviceAppOTAVersion" class="flex items-center gap-1">
155
+ <FmIcon name="system_update" />
156
+ {{ device.deviceAppOTAVersion }}
157
+ </span>
158
+ <span class="flex items-center gap-1">
159
+ <FmIcon name="lock" />
160
+ {{ isPinUpdate ? t('order.pinSet') : t('order.noPin') }}
161
+ </span>
162
+ <span
163
+ v-if="device.lastStartup"
164
+ class="flex items-center gap-1"
165
+ :title="formatFullDate(device.lastStartup)"
166
+ >
167
+ <FmIcon name="schedule" />
168
+ {{ formatRelativeTime(device.lastStartup) }}
169
+ </span>
170
+ </div>
92
171
  </div>
93
172
  </template>
94
173
  <script setup lang="ts">
95
174
  import type { MfKioskDevice } from '@/modules/kiosk/interface'
96
- import { type SnackbarPosition, useDialog, useSnackbar } from '@feedmepos/ui-library'
97
- import KioskUnbindConfirm from './KioskUnbindConfirm.vue'
98
- import { computed, onUnmounted, ref } from 'vue'
99
175
  import { useKioskStore } from '@/stores/kiosk'
100
- import dayjs from 'dayjs'
101
176
  import { useI18n } from '@feedmepos/mf-common'
177
+ import { useDialog, useSnackbar, type SnackbarPosition } from '@feedmepos/ui-library'
178
+ import dayjs from 'dayjs'
179
+ import relativeTime from 'dayjs/plugin/relativeTime'
180
+ import { computed, ref } from 'vue'
181
+ import KioskUnbindConfirm from './KioskUnbindConfirm.vue'
182
+ dayjs.extend(relativeTime)
102
183
 
103
184
  const { t } = useI18n()
104
185
 
@@ -121,6 +202,41 @@ const appType = computed(() => {
121
202
  return props.device?.deviceAppType === 'TABLET_APP' ? 'Tablet App' : 'Kiosk App'
122
203
  })
123
204
 
205
+ const isTabletApp = computed(() => {
206
+ return props.device?.deviceAppType === 'TABLET_APP'
207
+ })
208
+
209
+ const truncatedMachineId = computed(() => {
210
+ const id = props.device.machineId
211
+ if (!id || id.length <= 16) return id
212
+ return `${id.slice(0, 8)}...${id.slice(-4)}`
213
+ })
214
+
215
+ async function copyMachineId() {
216
+ try {
217
+ await navigator.clipboard.writeText(props.device.machineId)
218
+ Snackbar.open({
219
+ type: 'success',
220
+ message: t('order.machineIdCopied'),
221
+ position: 'bottom' as SnackbarPosition
222
+ })
223
+ } catch {
224
+ Snackbar.open({
225
+ type: 'error',
226
+ message: t('order.copyFailed'),
227
+ position: 'bottom' as SnackbarPosition
228
+ })
229
+ }
230
+ }
231
+
232
+ function formatRelativeTime(date: Date): string {
233
+ return dayjs(date).fromNow()
234
+ }
235
+
236
+ function formatFullDate(date: Date): string {
237
+ return dayjs(date).format('DD MMM YYYY HH:mm:ss')
238
+ }
239
+
124
240
  // function showDeviceDetails() {
125
241
  // Dialog.open({
126
242
  // title: props.device.name,
@@ -1,29 +1,145 @@
1
1
  <template>
2
- <div>
2
+ <div>
3
+ <div class="flex flex-col gap-4">
4
+ <div class="flex items-center justify-between">
3
5
  <div class="flex flex-col gap-2">
4
- <div class="flex flex-col gap-2">
5
- <span class="fm-typo-en-title-sm-600 block"> {{ t('order.kioskDisplay') }} </span>
6
- <span class="fm-typo-en-body-md-400 block"> {{ t('order.kioskDisplayDescription') }} </span>
7
- </div>
8
- <div class="flex flex-col gap-6">
9
- <KioskDeviceCard v-for="device in devices" :key="device.machineId" :device="device"></KioskDeviceCard>
10
- <FmButton variant="plain" class="w-[10rem] border border-fm-color-primary fm-corner-radius-lg"
11
- :label="t('order.bindDevice')" size="md" prepend-icon="link" @click="requestOtp" />
12
- </div>
6
+ <span class="fm-typo-en-title-sm-600 block"> {{ t('order.kioskDisplay') }} </span>
7
+ <span class="fm-typo-en-body-md-400 block"> {{ t('order.kioskDisplayDescription') }} </span>
13
8
  </div>
9
+ <FmButton
10
+ variant="primary"
11
+ :label="t('order.bindDevice')"
12
+ size="md"
13
+ prepend-icon="link"
14
+ @click="$emit('requestOtp')"
15
+ />
16
+ </div>
17
+
18
+ <!-- Search and Filter Bar -->
19
+ <div class="flex items-center gap-3 flex-wrap">
20
+ <FmSearch
21
+ v-model:model-value="searchQuery"
22
+ class="flex-1 min-w-[200px] max-w-[400px]"
23
+ :placeholder="t('order.searchDevices')"
24
+ />
25
+ <FmMenu>
26
+ <template #menu-button>
27
+ <FmButton
28
+ variant="tertiary"
29
+ prepend-icon="filter_list"
30
+ :label="activeFilterLabel"
31
+ class="fm-typo-en-body-md-600"
32
+ />
33
+ </template>
34
+ <FmMenuItem
35
+ :model-value="filterType === 'all'"
36
+ :label="t('order.allTypes')"
37
+ @click="filterType = 'all'"
38
+ />
39
+ <FmMenuItem
40
+ :model-value="filterType === 'tablet'"
41
+ :label="t('order.tabletOnly')"
42
+ @click="filterType = 'tablet'"
43
+ />
44
+ <FmMenuItem
45
+ :model-value="filterType === 'kiosk'"
46
+ :label="t('order.kioskOnly')"
47
+ @click="filterType = 'kiosk'"
48
+ />
49
+ </FmMenu>
50
+ <span class="fm-typo-en-body-sm-400 text-fm-color-typo-tertiary">
51
+ {{ t('order.showingDevices', { count: filteredDevices.length, total: devices.length }) }}
52
+ </span>
53
+ </div>
54
+
55
+ <!-- Device Cards -->
56
+ <div class="grid grid-cols-2 gap-2 py-8">
57
+ <KioskDeviceCard
58
+ v-for="device in filteredDevices"
59
+ :key="device.machineId"
60
+ :device="device"
61
+ />
62
+ <div
63
+ v-if="filteredDevices.length === 0 && devices.length > 0"
64
+ class="text-center py-8 text-fm-color-typo-tertiary fm-typo-en-body-md-400 col-span-2"
65
+ >
66
+ {{ t('order.noDevicesMatch') }}
67
+ </div>
68
+ <div
69
+ v-if="devices.length === 0"
70
+ class="text-center py-8 text-fm-color-typo-tertiary fm-typo-en-body-md-400 col-span-2"
71
+ >
72
+ {{ t('order.noDevicesBound') }}
73
+ </div>
74
+ </div>
14
75
  </div>
76
+ </div>
15
77
  </template>
16
78
  <script setup lang="ts">
17
- import KioskDeviceCard from "./KioskDeviceCard.vue";
18
- import type { MfKioskDevice } from "@/modules/kiosk/interface";
79
+ import type { MfKioskDevice } from '@/modules/kiosk/interface'
19
80
  import { useI18n } from '@feedmepos/mf-common'
81
+ import { computed, ref } from 'vue'
82
+ import KioskDeviceCard from './KioskDeviceCard.vue'
20
83
 
21
- const { t } = useI18n();
84
+ const { t } = useI18n()
22
85
 
23
86
  interface Props {
24
- devices: MfKioskDevice[],
25
- requestOtp: () => Promise<void>;
87
+ devices: MfKioskDevice[]
26
88
  }
27
89
 
28
- const props = defineProps<Props>();
29
- </script>
90
+ const props = defineProps<Props>()
91
+ defineEmits<{
92
+ (e: 'requestOtp'): void
93
+ }>()
94
+
95
+ const searchQuery = ref('')
96
+ const filterType = ref<'all' | 'tablet' | 'kiosk'>('all')
97
+
98
+ const activeFilterLabel = computed(() => {
99
+ switch (filterType.value) {
100
+ case 'tablet':
101
+ return t('order.tabletOnly')
102
+ case 'kiosk':
103
+ return t('order.kioskOnly')
104
+ default:
105
+ return t('order.allTypes')
106
+ }
107
+ })
108
+
109
+ const filteredDevices = computed(() => {
110
+ let result = props.devices
111
+
112
+ // Apply type filter
113
+ if (filterType.value === 'tablet') {
114
+ result = result.filter((d) => d.deviceAppType === 'TABLET_APP')
115
+ } else if (filterType.value === 'kiosk') {
116
+ result = result.filter((d) => d.deviceAppType !== 'TABLET_APP')
117
+ }
118
+
119
+ // Apply search filter
120
+ const query = searchQuery.value.toLowerCase().trim()
121
+ if (query) {
122
+ result = result.filter((d) => {
123
+ const name = d.name?.toLowerCase() || ''
124
+ const machineId = d.machineId?.toLowerCase() || ''
125
+ const slot = d.slotInfo?.toLowerCase() || ''
126
+ return name.includes(query) || machineId.includes(query) || slot.includes(query)
127
+ })
128
+ }
129
+
130
+ // Sort tablets by slot, kiosks by name
131
+ return result.sort((a, b) => {
132
+ // Tablets first
133
+ if (a.deviceAppType === 'TABLET_APP' && b.deviceAppType !== 'TABLET_APP') return -1
134
+ if (a.deviceAppType !== 'TABLET_APP' && b.deviceAppType === 'TABLET_APP') return 1
135
+
136
+ // For tablets, sort by slot
137
+ if (a.deviceAppType === 'TABLET_APP' && b.deviceAppType === 'TABLET_APP') {
138
+ return (a.slotInfo || '').localeCompare(b.slotInfo || '')
139
+ }
140
+
141
+ // For kiosks, sort by name
142
+ return (a.name || '').localeCompare(b.name || '')
143
+ })
144
+ })
145
+ </script>