@feedmepos/mf-order-setting 0.0.91 → 0.0.93

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 (108) hide show
  1. package/dist/DisplaySystemView-BMRCs7o8.js +8891 -0
  2. package/dist/{KioskDevicesView-Bz6seroM.js → KioskDevicesView-BCZOqou3.js} +1 -1
  3. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-I39sE8Pe.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-4wFO9co5.js} +3 -3
  4. package/dist/{KioskSettingView-Cajjh4Zm.js → KioskSettingView-DIvpRwFV.js} +3 -3
  5. package/dist/{KioskView-BPSbsMi5.js → KioskView-w4txC9m2.js} +5 -5
  6. package/dist/{OrderSettingsView-jPxZ3JDM.js → OrderSettingsView-BPuBA2QC.js} +28615 -27747
  7. package/dist/{app-DIhv5ssA.js → app-xZAaURng.js} +4594 -3733
  8. package/dist/app.js +1 -1
  9. package/dist/common/order-platform/index.d.ts +20 -0
  10. package/dist/featureFlag-Cbj0uMaM.js +846 -0
  11. package/dist/frontend/mf-order/src/api/brandkit/index.d.ts +3 -0
  12. package/dist/frontend/mf-order/src/api/reservation/index.d.ts +8 -0
  13. package/dist/frontend/mf-order/src/app.d.ts +752 -0
  14. package/dist/frontend/mf-order/src/composables/featureFlag.d.ts +7 -0
  15. package/dist/frontend/mf-order/src/composables/useBusinessFeatureFlag.d.ts +4 -0
  16. package/dist/frontend/mf-order/src/composables/useLegacyOdsAccess.d.ts +4 -0
  17. package/dist/frontend/mf-order/src/composables/useMiniProgramPermission.d.ts +5 -0
  18. package/dist/frontend/mf-order/src/main.d.ts +752 -0
  19. package/dist/frontend/mf-order/src/modules/display-system/legacy-ods/interface.d.ts +80 -0
  20. package/dist/frontend/mf-order/src/stores/order-setting/index.d.ts +3 -3
  21. package/dist/frontend/mf-order/src/stores/restaurant/index.d.ts +3 -0
  22. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorOverrideDialog.vue.d.ts +26 -0
  23. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorSelection.vue.d.ts +15 -0
  24. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue.d.ts +24 -0
  25. package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue.d.ts +4 -0
  26. package/dist/frontend/mf-order/src/views/display-system/view-selection.d.ts +18 -0
  27. package/dist/frontend/mf-order/src/views/order-settings/reservation/CopySettingsSheet.vue.d.ts +186 -0
  28. package/dist/frontend/mf-order/src/views/order-settings/reservation/CustomTimePicker.vue.d.ts +11 -0
  29. package/dist/frontend/mf-order/src/views/order-settings/reservation/ReservationSetting.vue.d.ts +2 -0
  30. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
  31. package/dist/{index-vaSLX6r3.js → index-BAFwfvqv.js} +2 -2
  32. package/dist/{index-Clp_YRJ2.js → index-BxQKdXyN.js} +1 -1
  33. package/dist/{index-CmqNrkNl.js → index-Cipbdi6i.js} +1 -1
  34. package/dist/order.dto-yLICIGDQ.js +41528 -0
  35. package/dist/package/entity/delivery/delivery.dto.d.ts +7 -0
  36. package/dist/package/entity/food-court/food-court.dto.d.ts +315 -264
  37. package/dist/package/entity/food-court/order-v2.do.d.ts +231 -0
  38. package/dist/package/entity/food-court/order-v2.do.spec.d.ts +1 -0
  39. package/dist/package/entity/incoming-order/incoming-order-to-bill.dto.d.ts +15 -6
  40. package/dist/package/entity/index.d.ts +7 -0
  41. package/dist/package/entity/index.frontend.d.ts +2 -0
  42. package/dist/package/entity/member/member.dto.d.ts +2 -2
  43. package/dist/package/entity/order/dine-in/dine-in.do.d.ts +44 -9
  44. package/dist/package/entity/order/dine-in/dine-in.dto.d.ts +17 -3
  45. package/dist/package/entity/order/menu/menu.dto.d.ts +33 -3
  46. package/dist/package/entity/order/order-item/order-item.dto.d.ts +18 -18
  47. package/dist/package/entity/order/order.do.d.ts +87 -12
  48. package/dist/package/entity/order/order.dto.d.ts +762 -358
  49. package/dist/package/entity/order/order.enum.d.ts +1 -1
  50. package/dist/package/entity/order-platform/foodpanda/foodpanda-order.dto.d.ts +7 -0
  51. package/dist/package/entity/order-platform/grabfood/grabfood-order.do.d.ts +7 -0
  52. package/dist/package/entity/order-platform/menu.dto.d.ts +10 -10
  53. package/dist/package/entity/order-platform/order-platform.dto.d.ts +92 -0
  54. package/dist/package/entity/order-platform/shopeefood/shopeefood-order.do.d.ts +7 -0
  55. package/dist/package/entity/order-platform/tng/tng-menu.do.d.ts +6 -6
  56. package/dist/package/entity/order-setting/order-setting.do.d.ts +866 -5
  57. package/dist/package/entity/order-setting/order-setting.dto.d.ts +10 -10
  58. package/dist/package/entity/order-setting/reservationV2/reservation.do.d.ts +1269 -0
  59. package/dist/package/entity/order-setting/sms/sms.do.d.ts +3 -3
  60. package/dist/package/entity/order-setting/sms/sms.dto.d.ts +3 -3
  61. package/dist/package/entity/queue/queue.dto.d.ts +175 -121
  62. package/dist/package/entity/reservation/reservation-pos.do.d.ts +222 -0
  63. package/dist/package/entity/reservation/reservation.do.d.ts +205 -0
  64. package/dist/package/entity/reservation/reservation.dto.d.ts +428 -0
  65. package/dist/package/entity/reservation/reservation.enum.d.ts +3 -0
  66. package/dist/package/entity/reservation/reservation.utils.d.ts +152 -0
  67. package/dist/package/entity/restaurant/restaurant.dto.d.ts +39 -24
  68. package/dist/package/entity/user/user.do.d.ts +2 -2
  69. package/dist/package/entity/websocket/websocket.dto.d.ts +161 -21
  70. package/dist/package/entity/websocket/websocket.enum.d.ts +1 -1
  71. package/dist/style.css +1 -1
  72. package/env.d.ts +2 -0
  73. package/package.json +3 -2
  74. package/src/api/brandkit/index.ts +24 -0
  75. package/src/api/reservation/index.ts +25 -0
  76. package/src/assets/images/not-found.png +0 -0
  77. package/src/composables/featureFlag.ts +22 -0
  78. package/src/composables/useBusinessFeatureFlag.ts +44 -0
  79. package/src/composables/useLegacyOdsAccess.ts +35 -0
  80. package/src/composables/useMiniProgramPermission.ts +19 -0
  81. package/src/composables/useQrOrderingEnabled.ts +1 -21
  82. package/src/locales/en-US.json +188 -0
  83. package/src/locales/ja-JP.json +188 -0
  84. package/src/locales/th-TH.json +188 -0
  85. package/src/locales/zh-CN.json +188 -0
  86. package/src/modules/display-system/legacy-ods/interface.ts +108 -0
  87. package/src/router/routes.ts +1 -1
  88. package/src/views/display-system/DisplaySystemView.vue +82 -11
  89. package/src/views/display-system/legacy-ods/ColorOverrideDialog.vue +223 -0
  90. package/src/views/display-system/legacy-ods/ColorSelection.vue +67 -0
  91. package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.spec.ts +185 -0
  92. package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue +1677 -0
  93. package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.spec.ts +124 -0
  94. package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue +297 -0
  95. package/src/views/display-system/legacy-ods/brandkit.spec.ts +67 -0
  96. package/src/views/display-system/view-selection.spec.ts +93 -0
  97. package/src/views/display-system/view-selection.ts +54 -0
  98. package/src/views/order-settings/OrderSettingsView.vue +9 -2
  99. package/src/views/order-settings/delivery/delivery.spec.ts +21 -0
  100. package/src/views/order-settings/delivery/delivery.ts +2 -51
  101. package/src/views/order-settings/dine-in/DineInSetting.vue +11 -1
  102. package/src/views/order-settings/reservation/CopySettingsSheet.vue +256 -0
  103. package/src/views/order-settings/reservation/CustomTimePicker.vue +311 -0
  104. package/src/views/order-settings/reservation/ReservationSetting.vue +1549 -0
  105. package/vitest.config.ts +2 -1
  106. package/dist/DisplaySystemView-n7Kqdw-h.js +0 -6564
  107. package/dist/_plugin-vue_export-helper-CHgC5LLL.js +0 -9
  108. package/dist/order.dto-DV1qL3Q_.js +0 -40301
@@ -42,6 +42,27 @@ describe('isIntegratedPlatformSupported', () => {
42
42
  )
43
43
  })
44
44
 
45
+ it('offers exactly these platforms per country', () => {
46
+ const C = AUTO_INTEGRATION_CLIENT
47
+ const expected: Record<string, AUTO_INTEGRATION_CLIENT[]> = {
48
+ MY: [C.foodpanda, C.grabfood, C.shopeefood, C.tng, C.external],
49
+ SG: [C.foodpanda, C.grabfood, C.deliveroo, C.external],
50
+ TH: [C.grabfood, C.external, C.lineman],
51
+ PK: [C.foodpanda, C.external],
52
+ ID: [C.grabfood, C.gofood, C.external],
53
+ GB: [C.external],
54
+ BN: [],
55
+ VN: []
56
+ }
57
+
58
+ for (const [country, offered] of Object.entries(expected)) {
59
+ const actual = Object.values(C).filter((key) =>
60
+ isIntegratedPlatformSupported(key, country as F_COUNTRY)
61
+ )
62
+ expect([country, actual.sort()]).toEqual([country, [...offered].sort()])
63
+ }
64
+ })
65
+
45
66
  it('offers no other integrated platform in the UK (GB)', () => {
46
67
  const others = Object.values(AUTO_INTEGRATION_CLIENT).filter(
47
68
  (key) => key !== AUTO_INTEGRATION_CLIENT.external
@@ -6,6 +6,7 @@ import {
6
6
  } from '@feedmepos/core/entity'
7
7
  import { useCoreStore } from '@feedmepos/mf-common'
8
8
  import { F_ORDER_PLATFORM, type FdoExternalSetting } from '@entity'
9
+ import { isIntegratedPlatformOffered } from '@common/order-platform'
9
10
 
10
11
  export type LinkedDeliveryDoc = FdoLinkedDelivery & { _rev: string }
11
12
 
@@ -100,57 +101,7 @@ export function useDelivery() {
100
101
  country?: F_COUNTRY | null
101
102
  ): boolean {
102
103
  country = country || useCoreStore().currentCountry.value || F_COUNTRY.enum.MY
103
- const mySupported = [
104
- AUTO_INTEGRATION_CLIENT.foodpanda,
105
- AUTO_INTEGRATION_CLIENT.grabfood,
106
- AUTO_INTEGRATION_CLIENT.shopeefood,
107
- AUTO_INTEGRATION_CLIENT.tng,
108
- AUTO_INTEGRATION_CLIENT.external
109
- ]
110
- const sgSupported = [
111
- AUTO_INTEGRATION_CLIENT.foodpanda,
112
- AUTO_INTEGRATION_CLIENT.grabfood,
113
- AUTO_INTEGRATION_CLIENT.deliveroo,
114
- AUTO_INTEGRATION_CLIENT.external
115
- ]
116
- const thSupported = [
117
- AUTO_INTEGRATION_CLIENT.grabfood,
118
- AUTO_INTEGRATION_CLIENT.external,
119
- AUTO_INTEGRATION_CLIENT.lineman
120
- ]
121
- const pkSupported = [AUTO_INTEGRATION_CLIENT.foodpanda, AUTO_INTEGRATION_CLIENT.external]
122
- const idSupported = [
123
- AUTO_INTEGRATION_CLIENT.grabfood,
124
- AUTO_INTEGRATION_CLIENT.gofood,
125
- AUTO_INTEGRATION_CLIENT.external
126
- ]
127
- const gbSupported = [AUTO_INTEGRATION_CLIENT.external]
128
-
129
- let supportedPlatforms: AUTO_INTEGRATION_CLIENT[] = []
130
- switch (country) {
131
- case F_COUNTRY.enum.MY:
132
- supportedPlatforms = mySupported
133
- break
134
- case F_COUNTRY.enum.SG:
135
- supportedPlatforms = sgSupported
136
- break
137
- case F_COUNTRY.enum.TH:
138
- supportedPlatforms = thSupported
139
- break
140
- case F_COUNTRY.enum.PK:
141
- supportedPlatforms = pkSupported
142
- break
143
- case F_COUNTRY.enum.ID:
144
- supportedPlatforms = idSupported
145
- break
146
- case F_COUNTRY.enum.GB:
147
- supportedPlatforms = gbSupported
148
- break
149
- default:
150
- break
151
- }
152
-
153
- return supportedPlatforms.includes(platform)
104
+ return isIntegratedPlatformOffered(platform, country)
154
105
  }
155
106
 
156
107
  return {
@@ -60,6 +60,14 @@
60
60
  :sublabel="t('order.acceptQueueSublabel')"
61
61
  label-placement="right"
62
62
  />
63
+ <FmSwitch
64
+ v-if="isAdmin"
65
+ v-model="dineInSetting.acceptReservation as boolean"
66
+ value="acceptReservation"
67
+ :label="t('order.acceptReservation')"
68
+ :sublabel="t('order.acceptReservationSublabel')"
69
+ label-placement="right"
70
+ />
63
71
 
64
72
  <FmSwitch
65
73
  :model-value="lastOrderBeforeCloseEnable"
@@ -201,8 +209,10 @@ const { t } = useI18n()
201
209
 
202
210
  type OrderDineInSettings = NonNullable<OrderSettingsDto['dineInSettings']>
203
211
 
204
- const { currentRestaurant } = useCoreStore()
212
+ const { currentRestaurant, sessionUser } = useCoreStore()
205
213
  const restaurantStore = useRestaurantStore()
214
+ // Reservations are switched on per outlet by FeedMe admins during the pilot.
215
+ const isAdmin = computed(() => sessionUser.value?.role?.isAdmin ?? false)
206
216
  const dineInSetting = ref<FdoRestaurantDineIn>(initDineInSetting())
207
217
  const orderSetting = ref<OrderSettingsDto | undefined>(undefined)
208
218
  const lastOrderBeforeCloseEnable = ref<boolean>(false)
@@ -0,0 +1,256 @@
1
+ <template>
2
+ <FmSideSheet :max-width="560" v-model="showSheet">
3
+ <template #side-sheet-button>
4
+ <FmButton label="Copy settings from another restaurant" icon="edit" variant="plain"
5
+ class="border-1 rounded-lg border-fm-color-primary" />
6
+ </template>
7
+
8
+ <template #side-sheet-header> Copy settings from </template>
9
+
10
+ <template #default>
11
+ <div class="flex flex-col gap-24">
12
+ <!-- Restaurant Selector -->
13
+ <div>
14
+ <FmSelect :items="availableRestaurants" :model-value="selectedRestaurantId"
15
+ @update:model-value="(v: string) => (selectedRestaurantId = v)" placeholder="Select" :width="512" />
16
+ </div>
17
+
18
+ <!-- Configuration Section -->
19
+ <div>
20
+ <div class="fm-typo-en-title-sm-600 mb-16">Configuration</div>
21
+
22
+ <div class="flex flex-col gap-12 pl-8">
23
+ <!-- All Checkbox -->
24
+ <FmCheckbox label="All" :model-value="isAllSelected" value="all" @update:model-value="toggleAll" />
25
+
26
+ <!-- Individual Configuration Checkboxes -->
27
+ <FmCheckbox label="Reservation Availability" :model-value="configOptions.reservationAvailability"
28
+ value="reservationAvailability"
29
+ @update:model-value="(v: boolean) => updateConfigOption('reservationAvailability', v)" />
30
+
31
+ <FmCheckbox label="Time Settings" :model-value="configOptions.timeSettings" value="timeSettings"
32
+ @update:model-value="(v: boolean) => updateConfigOption('timeSettings', v)" />
33
+
34
+ <FmCheckbox label="Table Capacity Settings" :model-value="configOptions.tableCapacity" value="tableCapacity"
35
+ @update:model-value="(v: boolean) => updateConfigOption('tableCapacity', v)" />
36
+
37
+ <FmCheckbox label="Pre-order" :model-value="configOptions.preorder" value="preorder"
38
+ @update:model-value="(v: boolean) => updateConfigOption('preorder', v)" />
39
+
40
+ <FmCheckbox label="Guest Preferences" :model-value="configOptions.guestPreferences" value="guestPreferences"
41
+ @update:model-value="(v: boolean) => updateConfigOption('guestPreferences', v)" />
42
+
43
+ <FmCheckbox label="Guest Message" :model-value="configOptions.guestMessage" value="guestMessage"
44
+ @update:model-value="(v: boolean) => updateConfigOption('guestMessage', v)" />
45
+
46
+ <FmCheckbox label="Cancellation Policy" :model-value="configOptions.cancellationPolicy"
47
+ value="cancellationPolicy"
48
+ @update:model-value="(v: boolean) => updateConfigOption('cancellationPolicy', v)" />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </template>
53
+
54
+ <template #side-sheet-footer>
55
+ <div class="flex flex-col gap-16 w-full">
56
+ <FmSnackbar variant="warning" class="!shadow-none"
57
+ :description="`This will overwrite all reservation settings for ${targetRestaurantName}`" />
58
+ <div class="flex items-center justify-start gap-8">
59
+ <FmButton label="Apply" @click="handleApply" :disabled="!canApply" :loading="isLoading" />
60
+ <FmButton label="Cancel" variant="tertiary" @click="handleCancel" />
61
+ </div>
62
+ </div>
63
+ </template>
64
+ </FmSideSheet>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ import { useCoreStore, useI18n } from '@feedmepos/mf-common'
69
+ import { computed, ref, watch } from 'vue'
70
+ import { ReservationApi } from '@/api/reservation'
71
+ import { useSnackbarFunctions } from '@/components/snackbar'
72
+ import type { FdoOrderReservationSettingsV2 } from '@entity'
73
+
74
+ const { t } = useI18n()
75
+ const { showSuccess, showError } = useSnackbarFunctions()
76
+ const Core = useCoreStore()
77
+
78
+ // Props
79
+ const props = defineProps<{
80
+ currentSettings: FdoOrderReservationSettingsV2
81
+ }>()
82
+
83
+ // Emits
84
+ const emit = defineEmits<{
85
+ apply: [settings: Partial<FdoOrderReservationSettingsV2>]
86
+ }>()
87
+
88
+ // State
89
+ const showSheet = ref(false)
90
+ const isLoading = ref(false)
91
+
92
+ const configOptions = ref({
93
+ reservationAvailability: false,
94
+ timeSettings: false,
95
+ tableCapacity: false,
96
+ preorder: false,
97
+ guestPreferences: false,
98
+ guestMessage: false,
99
+ cancellationPolicy: false
100
+ })
101
+
102
+ // Computed
103
+ const { currentRestaurant } = Core
104
+
105
+ const availableRestaurants = computed(() => {
106
+ // Filter out current restaurant from the list
107
+ return (Core.restaurants.value ?? [])
108
+ .filter((r) => r._id !== currentRestaurant.value?._id)
109
+ .map((r) => ({ label: r.profile.name, value: r._id }))
110
+ })
111
+
112
+ const selectedRestaurantId = ref<string>('')
113
+
114
+ // Watch to set initial value when availableRestaurants loads
115
+ watch(availableRestaurants, (newRestaurants) => {
116
+ if (newRestaurants.length > 0 && !selectedRestaurantId.value) {
117
+ selectedRestaurantId.value = newRestaurants[0].value
118
+ }
119
+ }, { immediate: true })
120
+
121
+ const targetRestaurantName = computed(() => {
122
+ return currentRestaurant.value?.profile.name ?? ''
123
+ })
124
+
125
+ const isAllSelected = computed(() => {
126
+ return Object.values(configOptions.value).every((v) => v === true)
127
+ })
128
+
129
+ const canApply = computed(() => {
130
+ return (
131
+ selectedRestaurantId.value !== '' && Object.values(configOptions.value).some((v) => v === true)
132
+ )
133
+ })
134
+
135
+ // Methods
136
+ function toggleAll(value: boolean) {
137
+ Object.keys(configOptions.value).forEach((key) => {
138
+ configOptions.value[key as keyof typeof configOptions.value] = value
139
+ })
140
+ }
141
+
142
+ function updateConfigOption(key: keyof typeof configOptions.value, value: boolean) {
143
+ configOptions.value[key] = value
144
+ }
145
+
146
+ async function handleApply() {
147
+ if (!selectedRestaurantId.value) {
148
+ showError('Please select a restaurant')
149
+ return
150
+ }
151
+
152
+ if (!Object.values(configOptions.value).some((v) => v)) {
153
+ showError('Please select at least one configuration option')
154
+ return
155
+ }
156
+
157
+ try {
158
+ isLoading.value = true
159
+
160
+ // Fetch settings from selected restaurant
161
+ const sourceSettings = await ReservationApi.getReservationSetting(selectedRestaurantId.value)
162
+
163
+ if (!sourceSettings || !sourceSettings.ranges || sourceSettings.ranges.length === 0) {
164
+ showError('No settings found for the selected restaurant')
165
+ return
166
+ }
167
+
168
+ // Build partial settings object based on selected options
169
+ const updatedSettings: Partial<FdoOrderReservationSettingsV2> = {}
170
+ const sourceRange = sourceSettings.ranges[0]
171
+ const currentRange = props.currentSettings.ranges[0]
172
+
173
+ // Create a copy of the current range to modify
174
+ const updatedRange = { ...currentRange }
175
+
176
+ // Copy selected configuration sections
177
+ if (configOptions.value.reservationAvailability) {
178
+ // Copy enable state, min/max lead duration
179
+ updatedRange.enable = sourceRange.enable
180
+ updatedRange.minLeadDuration = { ...sourceRange.minLeadDuration }
181
+ updatedRange.maxLeadDuration = { ...sourceRange.maxLeadDuration }
182
+ }
183
+
184
+ if (configOptions.value.timeSettings) {
185
+ // Copy operating hours, slot interval, booking duration
186
+ updatedRange.operatingHours = JSON.parse(JSON.stringify(sourceRange.operatingHours))
187
+ updatedRange.slotInterval = sourceRange.slotInterval
188
+ updatedRange.bookingDuration = sourceRange.bookingDuration
189
+ }
190
+
191
+ if (configOptions.value.tableCapacity) {
192
+ // Copy capacity tiers
193
+ updatedRange.capacityTiers = JSON.parse(JSON.stringify(sourceRange.capacityTiers))
194
+ }
195
+
196
+ if (configOptions.value.preorder) {
197
+ // Copy preorder setting
198
+ updatedRange.enablePreorder = sourceRange.enablePreorder
199
+ }
200
+
201
+ if (configOptions.value.guestPreferences) {
202
+ // Copy preferences
203
+ updatedRange.preferences = JSON.parse(JSON.stringify(sourceRange.preferences))
204
+ }
205
+
206
+ if (configOptions.value.guestMessage) {
207
+ // Copy guest message
208
+ updatedRange.guestMessage = sourceRange.guestMessage
209
+ }
210
+
211
+ if (configOptions.value.cancellationPolicy) {
212
+ // Copy cancellation policy
213
+ updatedRange.cancellationPolicy = sourceRange.cancellationPolicy
214
+ }
215
+
216
+ // Update the settings with the modified range
217
+ updatedSettings.ranges = [updatedRange]
218
+
219
+ // Emit the updated settings to parent
220
+ emit('apply', updatedSettings)
221
+
222
+ // Show success with source outlet name
223
+ const sourceName = availableRestaurants.value.find(r => r.value === selectedRestaurantId.value)?.label ?? 'selected outlet'
224
+ showSuccess(`Settings from "${sourceName}" have been applied. Please review and save changes.`)
225
+
226
+ // Close the side sheet
227
+ showSheet.value = false
228
+
229
+ // Reset form
230
+ resetForm()
231
+ } catch (error) {
232
+ console.error('Error copying settings:', error)
233
+ showError('Failed to copy settings. Please try again.')
234
+ } finally {
235
+ isLoading.value = false
236
+ }
237
+ }
238
+
239
+ function handleCancel() {
240
+ showSheet.value = false
241
+ resetForm()
242
+ }
243
+
244
+ function resetForm() {
245
+ selectedRestaurantId.value = ''
246
+ configOptions.value = {
247
+ reservationAvailability: false,
248
+ timeSettings: false,
249
+ tableCapacity: false,
250
+ preorder: false,
251
+ guestPreferences: false,
252
+ guestMessage: false,
253
+ cancellationPolicy: false
254
+ }
255
+ }
256
+ </script>
@@ -0,0 +1,311 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ modelValue: string
6
+ minTime?: string // Optional minimum time (HH:MM format)
7
+ restrictMinTime?: boolean
8
+ }>()
9
+
10
+ const emit = defineEmits<{
11
+ (e: 'update:modelValue', value: string): void
12
+ }>()
13
+
14
+ const isOpen = ref(false)
15
+ const wrapperRef = ref<HTMLDivElement | null>(null)
16
+ const inputRef = ref<HTMLInputElement | null>(null)
17
+ const dropdownRef = ref<HTMLDivElement | null>(null)
18
+
19
+ // Generate time options in 30-minute intervals
20
+ const timeOptions = computed(() => {
21
+ const options: string[] = []
22
+ for (let hour = 0; hour < 24; hour++) {
23
+ for (let minute = 0; minute < 60; minute += 30) {
24
+ const timeString = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`
25
+ options.push(timeString)
26
+ }
27
+ }
28
+
29
+ options.push('23:59')
30
+
31
+ // Filter out times earlier than minTime if provided
32
+ if (props.minTime && props.restrictMinTime !== false) {
33
+ return options.filter((time) => isTimeGreaterOrEqual(time, props.minTime!))
34
+ }
35
+
36
+ return options
37
+ })
38
+
39
+ // Helper function to compare times in HH:MM format
40
+ function isTimeGreaterOrEqual(time1: string, time2: string): boolean {
41
+ const [hours1, minutes1] = time1.split(':').map(Number)
42
+ const [hours2, minutes2] = time2.split(':').map(Number)
43
+
44
+ const totalMinutes1 = hours1 * 60 + minutes1
45
+ const totalMinutes2 = hours2 * 60 + minutes2
46
+
47
+ return totalMinutes1 >= totalMinutes2
48
+ }
49
+
50
+ const displayValue = computed({
51
+ get: () => props.modelValue || '',
52
+ set: (value: string) => {
53
+ emit('update:modelValue', value)
54
+ }
55
+ })
56
+
57
+ function selectTime(time: string) {
58
+ displayValue.value = time
59
+ isOpen.value = false
60
+ }
61
+
62
+ function handleClickOutside(event: MouseEvent) {
63
+ const target = event.target as Node
64
+ const wrapperElement = wrapperRef.value
65
+
66
+ if (
67
+ dropdownRef.value &&
68
+ wrapperElement &&
69
+ !dropdownRef.value.contains(target) &&
70
+ !wrapperElement.contains(target)
71
+ ) {
72
+ isOpen.value = false
73
+ }
74
+ }
75
+
76
+ function handleFocus() {
77
+ isOpen.value = true
78
+ }
79
+
80
+ function toggleDropdown(event?: Event) {
81
+ event?.stopPropagation()
82
+ const willOpen = !isOpen.value
83
+
84
+ // Close all other open dropdowns by dispatching a global event
85
+ if (willOpen) {
86
+ window.dispatchEvent(new CustomEvent('closeTimePickers'))
87
+ }
88
+ isOpen.value = willOpen
89
+
90
+ // Scroll to selected time when opening
91
+ if (willOpen) {
92
+ nextTick(() => {
93
+ inputRef.value?.focus()
94
+ scrollToSelectedTime()
95
+ })
96
+ }
97
+ }
98
+
99
+ function scrollToSelectedTime() {
100
+ const scrollContainer = dropdownRef.value?.querySelector('.overflow-y-auto')
101
+ if (!scrollContainer) return
102
+
103
+ const selectedElement = scrollContainer.querySelector(
104
+ '[data-selected="true"]'
105
+ ) as HTMLElement | null
106
+ if (selectedElement) {
107
+ const container = scrollContainer as HTMLElement
108
+ const targetScrollTop =
109
+ selectedElement.offsetTop - container.clientHeight / 2 + selectedElement.clientHeight / 2
110
+ container.scrollTop = Math.max(0, targetScrollTop)
111
+ }
112
+ }
113
+
114
+ function formatTimeInput(value: string) {
115
+ const digits = value.replace(/\D/g, '').slice(0, 4)
116
+
117
+ if (!digits) {
118
+ return ''
119
+ }
120
+
121
+ let hours = digits.slice(0, 2)
122
+ let minutes = digits.slice(2, 4)
123
+
124
+ if (hours.length === 2) {
125
+ hours = String(Math.min(Number(hours), 23)).padStart(2, '0')
126
+ }
127
+
128
+ if (minutes.length === 1) {
129
+ minutes = String(Math.min(Number(minutes), 5))
130
+ }
131
+
132
+ if (minutes.length === 2) {
133
+ minutes = String(Math.min(Number(minutes), 59)).padStart(2, '0')
134
+ }
135
+
136
+ if (digits.length <= 2) {
137
+ return digits.length === 2 ? `${hours}:` : hours
138
+ }
139
+
140
+ return `${hours}:${minutes}`
141
+ }
142
+
143
+ function hasSelection(input: HTMLInputElement) {
144
+ return (input.selectionStart ?? 0) !== (input.selectionEnd ?? 0)
145
+ }
146
+
147
+ function handleKeydown(event: KeyboardEvent) {
148
+ if (event.ctrlKey || event.metaKey || event.altKey) {
149
+ return
150
+ }
151
+
152
+ const allowedKeys = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End']
153
+ if (allowedKeys.includes(event.key)) {
154
+ return
155
+ }
156
+
157
+ if (!/^\d$/.test(event.key)) {
158
+ event.preventDefault()
159
+ return
160
+ }
161
+
162
+ const input = event.target as HTMLInputElement | null
163
+ if (!input) {
164
+ return
165
+ }
166
+
167
+ const digitCount = input.value.replace(/\D/g, '').length
168
+ if (digitCount >= 4 && !hasSelection(input)) {
169
+ event.preventDefault()
170
+ }
171
+ }
172
+
173
+ function handleInput(event: Event) {
174
+ const input = event.target as HTMLInputElement | null
175
+ if (!input) {
176
+ return
177
+ }
178
+
179
+ const formattedValue = formatTimeInput(input.value)
180
+ input.value = formattedValue
181
+ displayValue.value = formattedValue
182
+ }
183
+
184
+ function handlePaste(event: ClipboardEvent) {
185
+ event.preventDefault()
186
+
187
+ const pastedText = event.clipboardData?.getData('text') || ''
188
+ const formattedValue = formatTimeInput(pastedText)
189
+
190
+ displayValue.value = formattedValue
191
+ }
192
+
193
+ function focusInput() {
194
+ inputRef.value?.focus()
195
+ isOpen.value = true
196
+ }
197
+
198
+ function handleCloseTimePickers() {
199
+ isOpen.value = false
200
+ }
201
+
202
+ onMounted(() => {
203
+ document.addEventListener('click', handleClickOutside)
204
+ window.addEventListener('closeTimePickers', handleCloseTimePickers)
205
+ })
206
+
207
+ onUnmounted(() => {
208
+ document.removeEventListener('click', handleClickOutside)
209
+ window.removeEventListener('closeTimePickers', handleCloseTimePickers)
210
+ })
211
+ </script>
212
+
213
+ <template>
214
+ <div class="relative">
215
+ <div ref="wrapperRef" @click="focusInput" class="relative inline-flex w-[132px] max-w-full">
216
+ <input
217
+ ref="inputRef"
218
+ :value="displayValue"
219
+ @input="handleInput"
220
+ @keydown="handleKeydown"
221
+ @paste="handlePaste"
222
+ @focus="handleFocus"
223
+ placeholder="HH:MM"
224
+ class="time-picker-field"
225
+ maxlength="5"
226
+ inputmode="numeric"
227
+ autocomplete="off"
228
+ />
229
+ <button
230
+ type="button"
231
+ @click.stop="toggleDropdown"
232
+ class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 hover:text-gray-700"
233
+ >
234
+ <FmIcon
235
+ class="text-[18px] transition-all duration-200"
236
+ name="chevron_right"
237
+ :class="{ 'rotate-[-90deg]': isOpen, 'rotate-[90deg]': !isOpen }"
238
+ />
239
+ </button>
240
+ </div>
241
+
242
+ <div
243
+ v-if="isOpen"
244
+ ref="dropdownRef"
245
+ class="absolute z-50 w-full mt-2 bg-white border border-gray-200 rounded-md shadow-lg max-h-[240px] overflow-hidden"
246
+ >
247
+ <div class="max-h-[240px] overflow-y-auto scrollbar-thin">
248
+ <div
249
+ v-for="time in timeOptions"
250
+ :key="time"
251
+ @click.stop="selectTime(time)"
252
+ :data-selected="time === displayValue"
253
+ class="px-3 py-2 text-sm cursor-pointer transition-colors hover:bg-gray-50"
254
+ :class="{
255
+ 'bg-orange-50 text-orange-600 font-medium': time === displayValue
256
+ }"
257
+ >
258
+ {{ time }}
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </template>
264
+
265
+ <style scoped>
266
+ /* Custom scrollbar for dropdown - target the scrollable container */
267
+ .scrollbar-thin::-webkit-scrollbar {
268
+ width: 6px;
269
+ }
270
+
271
+ .scrollbar-thin::-webkit-scrollbar-track {
272
+ background: transparent;
273
+ border-radius: 3px;
274
+ margin: 4px 0;
275
+ }
276
+
277
+ .scrollbar-thin::-webkit-scrollbar-thumb {
278
+ background: #d1d5db;
279
+ border-radius: 3px;
280
+ }
281
+
282
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
283
+ background: #9ca3af;
284
+ }
285
+
286
+ .time-picker-field {
287
+ width: 100%;
288
+ height: 40px;
289
+ border-width: 1.5px;
290
+ border-style: solid;
291
+ border-color: rgb(209 213 219);
292
+ border-radius: 0.625rem;
293
+ padding: 8px 36px 8px 12px;
294
+ font-size: 0.95rem;
295
+ line-height: 1.25rem;
296
+ font-weight: 500;
297
+ transition: all 0.2s;
298
+ background-color: white;
299
+ outline: none;
300
+ box-sizing: border-box;
301
+ }
302
+
303
+ .time-picker-field:hover {
304
+ border-color: rgb(156 163 175);
305
+ }
306
+
307
+ .time-picker-field:focus {
308
+ border-color: rgb(249 115 22) !important;
309
+ background-color: rgb(255 247 237) !important;
310
+ }
311
+ </style>