@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.
- package/dist/DisplaySystemView-BMRCs7o8.js +8891 -0
- package/dist/{KioskDevicesView-Bz6seroM.js → KioskDevicesView-BCZOqou3.js} +1 -1
- package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-I39sE8Pe.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-4wFO9co5.js} +3 -3
- package/dist/{KioskSettingView-Cajjh4Zm.js → KioskSettingView-DIvpRwFV.js} +3 -3
- package/dist/{KioskView-BPSbsMi5.js → KioskView-w4txC9m2.js} +5 -5
- package/dist/{OrderSettingsView-jPxZ3JDM.js → OrderSettingsView-BPuBA2QC.js} +28615 -27747
- package/dist/{app-DIhv5ssA.js → app-xZAaURng.js} +4594 -3733
- package/dist/app.js +1 -1
- package/dist/common/order-platform/index.d.ts +20 -0
- package/dist/featureFlag-Cbj0uMaM.js +846 -0
- package/dist/frontend/mf-order/src/api/brandkit/index.d.ts +3 -0
- package/dist/frontend/mf-order/src/api/reservation/index.d.ts +8 -0
- package/dist/frontend/mf-order/src/app.d.ts +752 -0
- package/dist/frontend/mf-order/src/composables/featureFlag.d.ts +7 -0
- package/dist/frontend/mf-order/src/composables/useBusinessFeatureFlag.d.ts +4 -0
- package/dist/frontend/mf-order/src/composables/useLegacyOdsAccess.d.ts +4 -0
- package/dist/frontend/mf-order/src/composables/useMiniProgramPermission.d.ts +5 -0
- package/dist/frontend/mf-order/src/main.d.ts +752 -0
- package/dist/frontend/mf-order/src/modules/display-system/legacy-ods/interface.d.ts +80 -0
- package/dist/frontend/mf-order/src/stores/order-setting/index.d.ts +3 -3
- package/dist/frontend/mf-order/src/stores/restaurant/index.d.ts +3 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorOverrideDialog.vue.d.ts +26 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorSelection.vue.d.ts +15 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue.d.ts +24 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue.d.ts +4 -0
- package/dist/frontend/mf-order/src/views/display-system/view-selection.d.ts +18 -0
- package/dist/frontend/mf-order/src/views/order-settings/reservation/CopySettingsSheet.vue.d.ts +186 -0
- package/dist/frontend/mf-order/src/views/order-settings/reservation/CustomTimePicker.vue.d.ts +11 -0
- package/dist/frontend/mf-order/src/views/order-settings/reservation/ReservationSetting.vue.d.ts +2 -0
- package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
- package/dist/{index-vaSLX6r3.js → index-BAFwfvqv.js} +2 -2
- package/dist/{index-Clp_YRJ2.js → index-BxQKdXyN.js} +1 -1
- package/dist/{index-CmqNrkNl.js → index-Cipbdi6i.js} +1 -1
- package/dist/order.dto-yLICIGDQ.js +41528 -0
- package/dist/package/entity/delivery/delivery.dto.d.ts +7 -0
- package/dist/package/entity/food-court/food-court.dto.d.ts +315 -264
- package/dist/package/entity/food-court/order-v2.do.d.ts +231 -0
- package/dist/package/entity/food-court/order-v2.do.spec.d.ts +1 -0
- package/dist/package/entity/incoming-order/incoming-order-to-bill.dto.d.ts +15 -6
- package/dist/package/entity/index.d.ts +7 -0
- package/dist/package/entity/index.frontend.d.ts +2 -0
- package/dist/package/entity/member/member.dto.d.ts +2 -2
- package/dist/package/entity/order/dine-in/dine-in.do.d.ts +44 -9
- package/dist/package/entity/order/dine-in/dine-in.dto.d.ts +17 -3
- package/dist/package/entity/order/menu/menu.dto.d.ts +33 -3
- package/dist/package/entity/order/order-item/order-item.dto.d.ts +18 -18
- package/dist/package/entity/order/order.do.d.ts +87 -12
- package/dist/package/entity/order/order.dto.d.ts +762 -358
- package/dist/package/entity/order/order.enum.d.ts +1 -1
- package/dist/package/entity/order-platform/foodpanda/foodpanda-order.dto.d.ts +7 -0
- package/dist/package/entity/order-platform/grabfood/grabfood-order.do.d.ts +7 -0
- package/dist/package/entity/order-platform/menu.dto.d.ts +10 -10
- package/dist/package/entity/order-platform/order-platform.dto.d.ts +92 -0
- package/dist/package/entity/order-platform/shopeefood/shopeefood-order.do.d.ts +7 -0
- package/dist/package/entity/order-platform/tng/tng-menu.do.d.ts +6 -6
- package/dist/package/entity/order-setting/order-setting.do.d.ts +866 -5
- package/dist/package/entity/order-setting/order-setting.dto.d.ts +10 -10
- package/dist/package/entity/order-setting/reservationV2/reservation.do.d.ts +1269 -0
- package/dist/package/entity/order-setting/sms/sms.do.d.ts +3 -3
- package/dist/package/entity/order-setting/sms/sms.dto.d.ts +3 -3
- package/dist/package/entity/queue/queue.dto.d.ts +175 -121
- package/dist/package/entity/reservation/reservation-pos.do.d.ts +222 -0
- package/dist/package/entity/reservation/reservation.do.d.ts +205 -0
- package/dist/package/entity/reservation/reservation.dto.d.ts +428 -0
- package/dist/package/entity/reservation/reservation.enum.d.ts +3 -0
- package/dist/package/entity/reservation/reservation.utils.d.ts +152 -0
- package/dist/package/entity/restaurant/restaurant.dto.d.ts +39 -24
- package/dist/package/entity/user/user.do.d.ts +2 -2
- package/dist/package/entity/websocket/websocket.dto.d.ts +161 -21
- package/dist/package/entity/websocket/websocket.enum.d.ts +1 -1
- package/dist/style.css +1 -1
- package/env.d.ts +2 -0
- package/package.json +3 -2
- package/src/api/brandkit/index.ts +24 -0
- package/src/api/reservation/index.ts +25 -0
- package/src/assets/images/not-found.png +0 -0
- package/src/composables/featureFlag.ts +22 -0
- package/src/composables/useBusinessFeatureFlag.ts +44 -0
- package/src/composables/useLegacyOdsAccess.ts +35 -0
- package/src/composables/useMiniProgramPermission.ts +19 -0
- package/src/composables/useQrOrderingEnabled.ts +1 -21
- package/src/locales/en-US.json +188 -0
- package/src/locales/ja-JP.json +188 -0
- package/src/locales/th-TH.json +188 -0
- package/src/locales/zh-CN.json +188 -0
- package/src/modules/display-system/legacy-ods/interface.ts +108 -0
- package/src/router/routes.ts +1 -1
- package/src/views/display-system/DisplaySystemView.vue +82 -11
- package/src/views/display-system/legacy-ods/ColorOverrideDialog.vue +223 -0
- package/src/views/display-system/legacy-ods/ColorSelection.vue +67 -0
- package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.spec.ts +185 -0
- package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue +1677 -0
- package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.spec.ts +124 -0
- package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue +297 -0
- package/src/views/display-system/legacy-ods/brandkit.spec.ts +67 -0
- package/src/views/display-system/view-selection.spec.ts +93 -0
- package/src/views/display-system/view-selection.ts +54 -0
- package/src/views/order-settings/OrderSettingsView.vue +9 -2
- package/src/views/order-settings/delivery/delivery.spec.ts +21 -0
- package/src/views/order-settings/delivery/delivery.ts +2 -51
- package/src/views/order-settings/dine-in/DineInSetting.vue +11 -1
- package/src/views/order-settings/reservation/CopySettingsSheet.vue +256 -0
- package/src/views/order-settings/reservation/CustomTimePicker.vue +311 -0
- package/src/views/order-settings/reservation/ReservationSetting.vue +1549 -0
- package/vitest.config.ts +2 -1
- package/dist/DisplaySystemView-n7Kqdw-h.js +0 -6564
- package/dist/_plugin-vue_export-helper-CHgC5LLL.js +0 -9
- 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
|
-
|
|
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>
|