@feedmepos/mf-order-setting 0.0.50 → 0.0.52-dev.0
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/.tsbuildinfo +1 -0
- package/dist/{KioskDevicesView-u14hzPbE.js → KioskDevicesView-Vy9FLX1n.js} +1 -1
- package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-DBgRDIoS.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-DhZPOEEQ.js} +2 -2
- package/dist/{KioskSettingView-DmvtZcV1.js → KioskSettingView-cE-JdCBB.js} +206 -208
- package/dist/{KioskView-M8V91gD5.js → KioskView-BYs5bem0.js} +4 -4
- package/dist/OrderSettingsView-C4aEpC1j.js +56063 -0
- package/dist/{app-CLewMjcd.js → app-CwYXsqxX.js} +184 -20
- package/dist/app.js +1 -1
- package/dist/{dayjs.min-DCTYRWyD.js → dayjs.min-JEYIJz2D.js} +1 -1
- package/dist/frontend/mf-order/src/api/reservation/index.d.ts +8 -0
- package/dist/frontend/mf-order/src/app.d.ts +164 -0
- package/dist/frontend/mf-order/src/main.d.ts +164 -0
- package/dist/frontend/mf-order/src/stores/restaurant/index.d.ts +3 -3
- package/dist/frontend/mf-order/src/views/all-orders/ReflowOrder.vue.d.ts +2 -2
- package/dist/frontend/mf-order/src/views/order-settings/delivery/inhouse/InHouseDelivery.vue.d.ts +2 -2
- 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/CustomSelect.vue.d.ts +15 -0
- package/dist/frontend/mf-order/src/views/order-settings/reservation/CustomTimePicker.vue.d.ts +10 -0
- package/dist/frontend/mf-order/src/views/order-settings/reservation/ReservationSetting.vue.d.ts +2 -0
- package/dist/{index-B7LtJeBJ.js → index-DZCjODMx.js} +2 -2
- package/dist/{menu.dto-Co7iXHNr.js → menu.dto-D9CDVLiP.js} +22865 -20028
- package/dist/package/entity/food-court/order.do.d.ts +47 -2
- package/dist/package/entity/food-court/order.dto.d.ts +0 -3
- package/dist/package/entity/incoming-order/incoming-order-to-bill.dto.d.ts +12356 -1
- package/dist/package/entity/incoming-order/incoming-order.do.d.ts +3 -22266
- package/dist/package/entity/incoming-order/incoming-order.dto.d.ts +18 -0
- package/dist/package/entity/index.d.ts +5 -0
- package/dist/package/entity/kiosk/marketing/marketing.dto.d.ts +1 -19864
- package/dist/package/entity/order/order-item/order-item.dto.d.ts +24 -3714
- package/dist/package/entity/order/order.do.d.ts +8 -0
- package/dist/package/entity/order/order.dto.d.ts +118 -0
- package/dist/package/entity/order-platform/external/menu/external-master-menu.do.d.ts +20 -0
- package/dist/package/entity/order-platform/external/menu/external-menu.do.d.ts +23 -0
- package/dist/package/entity/order-platform/menu.dto.d.ts +34 -0
- package/dist/package/entity/order-setting/order-setting.do.d.ts +861 -0
- package/dist/package/entity/order-setting/reservationV2/reservation.do.d.ts +1269 -0
- package/dist/package/entity/queue/queue.do.d.ts +1 -11
- package/dist/package/entity/queue/queue.dto.d.ts +25 -0
- package/dist/package/entity/reservation/reservation.do.d.ts +101 -0
- package/dist/package/entity/reservation/reservation.dto.d.ts +325 -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/style.css +1 -0
- package/package.json +3 -3
- package/src/api/reservation/index.ts +28 -0
- package/src/assets/images/not-found.png +0 -0
- package/src/locales/en-US.json +56 -0
- package/src/locales/th-TH.json +54 -0
- package/src/locales/zh-CN.json +54 -0
- package/src/main.ts +7 -5
- package/src/stores/order-setting/mapper.ts +50 -50
- package/src/views/kiosk/settings/KioskPaymentTypeSection.vue +1 -19
- package/src/views/order-settings/OrderSettingsView.vue +7 -2
- package/src/views/order-settings/delivery/integrated-delivery/IntegratedDelivery.vue +3 -1
- package/src/views/order-settings/drive-thru/DriveThruSetting.vue +13 -28
- package/src/views/order-settings/reservation/CopySettingsSheet.vue +238 -0
- package/src/views/order-settings/reservation/CustomSelect.vue +99 -0
- package/src/views/order-settings/reservation/CustomTimePicker.vue +201 -0
- package/src/views/order-settings/reservation/ReservationSetting.vue +1246 -0
- package/src/views/order-settings/servicecharge/ServiceChargeRule.vue +5 -1
- package/tsconfig.app.json +8 -6
- package/dist/OrderSettingsView-Bl3LshG3.js +0 -51603
- package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +0 -1
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, onMounted, computed } from 'vue'
|
|
3
|
+
import { ReservationApi } from '@/api/reservation'
|
|
4
|
+
import { useLoading } from '@/composables/loading'
|
|
5
|
+
import {
|
|
6
|
+
type FdoOrderReservationSettingsV2,
|
|
7
|
+
FdoReservationRange,
|
|
8
|
+
FdoOrderReservationSettingsV2 as ReservationSettingsSchema,
|
|
9
|
+
generateDayTimeSlotsWithStatus,
|
|
10
|
+
categorizeTimeSlotsWithStatus,
|
|
11
|
+
validateTimeRangeOverlap,
|
|
12
|
+
validateSameDayTimeRange,
|
|
13
|
+
validateLeadDuration
|
|
14
|
+
} from '@entity'
|
|
15
|
+
import RestaurantSelector from '../components/RestaurantSelector.vue'
|
|
16
|
+
import CustomTimePicker from './CustomTimePicker.vue'
|
|
17
|
+
import CustomSelect from './CustomSelect.vue'
|
|
18
|
+
import { useCoreStore, useI18n } from '@feedmepos/mf-common'
|
|
19
|
+
import { useSnackbarFunctions } from '@/components/snackbar'
|
|
20
|
+
import { useDialog } from '@feedmepos/ui-library'
|
|
21
|
+
import moment from 'moment'
|
|
22
|
+
import CopySettingsSheet from './CopySettingsSheet.vue'
|
|
23
|
+
import notfound from '@/assets/images/not-found.png'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n()
|
|
27
|
+
const { showSuccess } = useSnackbarFunctions()
|
|
28
|
+
const { currentRestaurant } = useCoreStore()
|
|
29
|
+
const { isLoading, startAsyncCallWithErr } = useLoading()
|
|
30
|
+
const dialog = useDialog()
|
|
31
|
+
|
|
32
|
+
const DEFAULT_GUEST_MESSAGE = `Please take note of the following important details before making a reservation:
|
|
33
|
+
|
|
34
|
+
1. Dining Time Limit
|
|
35
|
+
- Our restaurant enforces a dining time limit of 1 hour and 30 minutes.
|
|
36
|
+
|
|
37
|
+
2. 15-Minute Holding Policy
|
|
38
|
+
- We can hold your reservation for a maximum of 15 minutes. If we are unable to reach you after this time, your table may be released without prior notice. If you anticipate being late or need to cancel, please call us to let us know so we can either hold your table or offer it to other guests.`
|
|
39
|
+
|
|
40
|
+
const DEFAULT_CANCELLATION_POLICY = `Cancellation Policy
|
|
41
|
+
|
|
42
|
+
Free cancellation up to 24 hours before your reservation. Please contact the outlet for any last-minute changes.`
|
|
43
|
+
|
|
44
|
+
const reservationSettings = ref<FdoOrderReservationSettingsV2>({
|
|
45
|
+
ranges: [
|
|
46
|
+
{
|
|
47
|
+
_id: '',
|
|
48
|
+
enable: false,
|
|
49
|
+
bookingDuration: 60,
|
|
50
|
+
enablePreorder: true,
|
|
51
|
+
minLeadDuration: {
|
|
52
|
+
value: 0,
|
|
53
|
+
unit: 'day'
|
|
54
|
+
},
|
|
55
|
+
maxLeadDuration: {
|
|
56
|
+
value: 30,
|
|
57
|
+
unit: 'day'
|
|
58
|
+
},
|
|
59
|
+
name: '',
|
|
60
|
+
operatingHours: {
|
|
61
|
+
0: {
|
|
62
|
+
enable: false,
|
|
63
|
+
hours: []
|
|
64
|
+
},
|
|
65
|
+
1: {
|
|
66
|
+
enable: false,
|
|
67
|
+
hours: []
|
|
68
|
+
},
|
|
69
|
+
2: {
|
|
70
|
+
enable: false,
|
|
71
|
+
hours: []
|
|
72
|
+
},
|
|
73
|
+
3: {
|
|
74
|
+
enable: false,
|
|
75
|
+
hours: []
|
|
76
|
+
},
|
|
77
|
+
4: {
|
|
78
|
+
enable: false,
|
|
79
|
+
hours: []
|
|
80
|
+
},
|
|
81
|
+
5: {
|
|
82
|
+
enable: false,
|
|
83
|
+
hours: []
|
|
84
|
+
},
|
|
85
|
+
6: {
|
|
86
|
+
enable: false,
|
|
87
|
+
hours: []
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
preferences: [],
|
|
91
|
+
capacityTiers: [],
|
|
92
|
+
slotInterval: 30,
|
|
93
|
+
guestMessage: DEFAULT_GUEST_MESSAGE,
|
|
94
|
+
cancellationPolicy: DEFAULT_CANCELLATION_POLICY
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
posCanOverbook: false,
|
|
98
|
+
draftHoldTimeMinutes: 15,
|
|
99
|
+
smsEnabled: true,
|
|
100
|
+
emailEnabled: true,
|
|
101
|
+
notifyOnConfirm: true,
|
|
102
|
+
notifyOnCancel: true
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const rangeSetting = ref<FdoReservationRange>({
|
|
106
|
+
_id: '',
|
|
107
|
+
enable: true,
|
|
108
|
+
bookingDuration: 60,
|
|
109
|
+
enablePreorder: true,
|
|
110
|
+
minLeadDuration: {
|
|
111
|
+
value: 0,
|
|
112
|
+
unit: 'day'
|
|
113
|
+
},
|
|
114
|
+
maxLeadDuration: {
|
|
115
|
+
value: 30,
|
|
116
|
+
unit: 'day'
|
|
117
|
+
},
|
|
118
|
+
name: '',
|
|
119
|
+
operatingHours: {
|
|
120
|
+
0: {
|
|
121
|
+
enable: true,
|
|
122
|
+
hours: []
|
|
123
|
+
},
|
|
124
|
+
1: {
|
|
125
|
+
enable: true,
|
|
126
|
+
hours: [
|
|
127
|
+
{ start: '09:00', end: '14:00' },
|
|
128
|
+
{ start: '17:00', end: '22:00' }
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
2: {
|
|
132
|
+
enable: true,
|
|
133
|
+
hours: [
|
|
134
|
+
{ start: '09:00', end: '14:00' },
|
|
135
|
+
{ start: '17:00', end: '22:00' }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
3: {
|
|
139
|
+
enable: true,
|
|
140
|
+
hours: [
|
|
141
|
+
{ start: '09:00', end: '14:00' },
|
|
142
|
+
{ start: '17:00', end: '22:00' }
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
4: {
|
|
146
|
+
enable: true,
|
|
147
|
+
hours: [
|
|
148
|
+
{ start: '09:00', end: '14:00' },
|
|
149
|
+
{ start: '17:00', end: '22:00' }
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
5: {
|
|
153
|
+
enable: true,
|
|
154
|
+
hours: [
|
|
155
|
+
{ start: '09:00', end: '14:00' },
|
|
156
|
+
{ start: '17:00', end: '22:00' }
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
6: {
|
|
160
|
+
enable: true,
|
|
161
|
+
hours: []
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
preferences: [],
|
|
165
|
+
capacityTiers: [
|
|
166
|
+
{
|
|
167
|
+
_id: '1',
|
|
168
|
+
minPax: 1,
|
|
169
|
+
maxPax: 2,
|
|
170
|
+
capacity: 10
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
_id: '2',
|
|
174
|
+
minPax: 3,
|
|
175
|
+
maxPax: 4,
|
|
176
|
+
capacity: 8
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
_id: '3',
|
|
180
|
+
minPax: 5,
|
|
181
|
+
maxPax: 6,
|
|
182
|
+
capacity: 5
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
_id: '4',
|
|
186
|
+
minPax: 7,
|
|
187
|
+
maxPax: null,
|
|
188
|
+
capacity: 3
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
slotInterval: 30,
|
|
192
|
+
guestMessage: DEFAULT_GUEST_MESSAGE,
|
|
193
|
+
cancellationPolicy: DEFAULT_CANCELLATION_POLICY
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const isSaving = ref(false)
|
|
197
|
+
|
|
198
|
+
const optionText = ref('')
|
|
199
|
+
const selectedPreviewDay = ref(1) // Monday by default
|
|
200
|
+
const editingOption = ref<{ prefIndex: number; optIndex: number } | null>(null)
|
|
201
|
+
|
|
202
|
+
// Track if user explicitly selected custom mode to prevent auto-switching to preset
|
|
203
|
+
const isCustomMode = ref(false)
|
|
204
|
+
|
|
205
|
+
// Track expanded state for each time segment
|
|
206
|
+
const expandedSegments = ref({
|
|
207
|
+
morning: false,
|
|
208
|
+
afternoon: false,
|
|
209
|
+
evening: false
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Sync rangeSetting with the first range in reservationSettings
|
|
213
|
+
// This ensures rangeSetting always reflects the first range (for future multi-range support)
|
|
214
|
+
watch(
|
|
215
|
+
() => reservationSettings.value.ranges[0],
|
|
216
|
+
(firstRange) => {
|
|
217
|
+
if (firstRange) {
|
|
218
|
+
rangeSetting.value = firstRange
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{ deep: true, immediate: true }
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// Also sync changes from rangeSetting back to the first range
|
|
225
|
+
watch(
|
|
226
|
+
() => rangeSetting.value,
|
|
227
|
+
(newRangeSetting) => {
|
|
228
|
+
if (reservationSettings.value.ranges[0]) {
|
|
229
|
+
reservationSettings.value.ranges[0] = newRangeSetting
|
|
230
|
+
} else {
|
|
231
|
+
reservationSettings.value.ranges.push(newRangeSetting)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{ deep: true }
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Computed property for preset selection
|
|
238
|
+
const selectedPreset = computed(() => {
|
|
239
|
+
// If user explicitly chose custom mode, stay in custom regardless of value
|
|
240
|
+
if (isCustomMode.value) {
|
|
241
|
+
return 'custom'
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Otherwise, check if value matches a preset
|
|
245
|
+
if (
|
|
246
|
+
rangeSetting.value.maxLeadDuration.unit === 'day' &&
|
|
247
|
+
(rangeSetting.value.maxLeadDuration.value === 30 ||
|
|
248
|
+
rangeSetting.value.maxLeadDuration.value === 60 ||
|
|
249
|
+
rangeSetting.value.maxLeadDuration.value === 90)
|
|
250
|
+
) {
|
|
251
|
+
return rangeSetting.value.maxLeadDuration.value
|
|
252
|
+
}
|
|
253
|
+
return 'custom'
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Generate time slots based on operating hours and slot interval
|
|
257
|
+
// Now includes unavailable slots (between operating hour gaps)
|
|
258
|
+
const availableSlots = computed(() => {
|
|
259
|
+
const day = selectedPreviewDay.value as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
260
|
+
const { operatingHours, slotInterval, bookingDuration } = rangeSetting.value
|
|
261
|
+
|
|
262
|
+
if (!operatingHours[day]?.enable || !operatingHours[day].hours.length) {
|
|
263
|
+
return { morning: [], afternoon: [], evening: [] }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Use shared utility to generate slots with availability status
|
|
267
|
+
// This generates slots only from earliest to latest operating hour (not full 24h)
|
|
268
|
+
// Example: If hours are 10:00-12:00, 15:00-21:00, shows 10:00-21:00 range
|
|
269
|
+
const slotsWithStatus = generateDayTimeSlotsWithStatus(
|
|
270
|
+
operatingHours[day],
|
|
271
|
+
slotInterval,
|
|
272
|
+
bookingDuration
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Categorize slots by time of day
|
|
276
|
+
return categorizeTimeSlotsWithStatus(slotsWithStatus)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Computed properties for displaying limited slots (max 2 rows = 10 items per segment)
|
|
280
|
+
const displayedSlots = computed(() => {
|
|
281
|
+
const ITEMS_PER_ROW = 5
|
|
282
|
+
const MAX_ROWS = 2
|
|
283
|
+
const MAX_ITEMS = ITEMS_PER_ROW * MAX_ROWS // 10 items
|
|
284
|
+
|
|
285
|
+
const limitSlots = (slots: any[], isExpanded: boolean) => {
|
|
286
|
+
if (isExpanded || slots.length <= MAX_ITEMS) {
|
|
287
|
+
return slots
|
|
288
|
+
}
|
|
289
|
+
// Show MAX_ITEMS - 1 slots, then replace the last one with "more" indicator
|
|
290
|
+
return slots.slice(0, MAX_ITEMS - 1)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
morning: limitSlots(availableSlots.value.morning, expandedSegments.value.morning),
|
|
295
|
+
afternoon: limitSlots(availableSlots.value.afternoon, expandedSegments.value.afternoon),
|
|
296
|
+
evening: limitSlots(availableSlots.value.evening, expandedSegments.value.evening)
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// Check if segment has more items than can be displayed (needs "more" button)
|
|
301
|
+
const hasMoreItems = computed(() => {
|
|
302
|
+
const MAX_ITEMS = 10
|
|
303
|
+
return {
|
|
304
|
+
morning: availableSlots.value.morning.length > MAX_ITEMS,
|
|
305
|
+
afternoon: availableSlots.value.afternoon.length > MAX_ITEMS,
|
|
306
|
+
evening: availableSlots.value.evening.length > MAX_ITEMS
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const reservationPreview = computed(() => {
|
|
311
|
+
const today = moment()
|
|
312
|
+
const { minLeadDuration, maxLeadDuration, operatingHours } = rangeSetting.value
|
|
313
|
+
const startDate = today.clone().add(minLeadDuration.value, minLeadDuration.unit).startOf('d')
|
|
314
|
+
const endDate = today.clone().add(maxLeadDuration.value, maxLeadDuration.unit).startOf('d')
|
|
315
|
+
|
|
316
|
+
type Day = keyof typeof operatingHours
|
|
317
|
+
|
|
318
|
+
// Get the hours for start and end days
|
|
319
|
+
const startDayHours = operatingHours[startDate.day() as Day].hours
|
|
320
|
+
const endDayHours = operatingHours[endDate.day() as Day].hours
|
|
321
|
+
|
|
322
|
+
// Check if hours exist for both days
|
|
323
|
+
if (!startDayHours || startDayHours.length === 0 || !endDayHours || endDayHours.length === 0) {
|
|
324
|
+
return {
|
|
325
|
+
start: startDate.format('DD MMM YYYY'),
|
|
326
|
+
end: endDate.format('DD MMM YYYY'),
|
|
327
|
+
startHour: '--:--',
|
|
328
|
+
endHour: '--:--'
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// use operatingHours and get the earlier starting hour to return a start&end for the preview, return the formatted time
|
|
333
|
+
const startHour = [...startDayHours].sort((a, b) =>
|
|
334
|
+
moment(a.start, 'HH:mm').diff(moment(b.start, 'HH:mm'))
|
|
335
|
+
)[0]
|
|
336
|
+
const endHour = [...endDayHours].sort((a, b) =>
|
|
337
|
+
moment(b.end, 'HH:mm').diff(moment(a.end, 'HH:mm'))
|
|
338
|
+
)[0]
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
start: startDate.format('DD MMM YYYY'),
|
|
342
|
+
end: endDate.format('DD MMM YYYY'),
|
|
343
|
+
startHour: moment(startHour.start, 'HH:mm').format('hh:mm A'),
|
|
344
|
+
endHour: moment(endHour.end, 'HH:mm').format('hh:mm A')
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
onMounted(() => {
|
|
349
|
+
init()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
watch(
|
|
353
|
+
() => currentRestaurant.value,
|
|
354
|
+
async (v) => {
|
|
355
|
+
await init()
|
|
356
|
+
// Only use restaurant's operating hours as fallback if no settings from backend
|
|
357
|
+
// or if ranges is empty (new setup)
|
|
358
|
+
if (
|
|
359
|
+
v &&
|
|
360
|
+
v.profile.operatingHours &&
|
|
361
|
+
(!reservationSettings.value.ranges || reservationSettings.value.ranges.length === 0)
|
|
362
|
+
) {
|
|
363
|
+
console.log('Using restaurant operating hours as fallback:', v.profile.operatingHours)
|
|
364
|
+
rangeSetting.value.operatingHours = v.profile.operatingHours
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{ immediate: true }
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
async function readReservationSetting(): Promise<FdoOrderReservationSettingsV2> {
|
|
371
|
+
return await startAsyncCallWithErr(async () => {
|
|
372
|
+
if (!currentRestaurant.value?._id) {
|
|
373
|
+
throw new Error('No restaurant selected')
|
|
374
|
+
}
|
|
375
|
+
return await ReservationApi.getReservationSetting(currentRestaurant.value._id)
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function updateReservationSetting() {
|
|
380
|
+
// Check for lead duration validation errors
|
|
381
|
+
const leadDurationError = validateLeadDurationLocal()
|
|
382
|
+
if (leadDurationError) {
|
|
383
|
+
dialog.open({
|
|
384
|
+
title: t('order.validationError'),
|
|
385
|
+
message: leadDurationError,
|
|
386
|
+
primaryActions: { text: t('order.ok'), close: true }
|
|
387
|
+
})
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Check for time range validation errors
|
|
392
|
+
if (hasAnyTimeRangeErrors()) {
|
|
393
|
+
dialog.open({
|
|
394
|
+
title: t('order.validationError'),
|
|
395
|
+
message: 'Please fix all time range errors before saving.',
|
|
396
|
+
primaryActions: { text: t('order.ok'), close: true }
|
|
397
|
+
})
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
isSaving.value = true
|
|
402
|
+
try {
|
|
403
|
+
await startAsyncCallWithErr(async () => {
|
|
404
|
+
if (!currentRestaurant.value?._id) {
|
|
405
|
+
throw new Error('No restaurant selected')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Validate with Zod schema
|
|
409
|
+
const validated = ReservationSettingsSchema.parse(reservationSettings.value)
|
|
410
|
+
console.log(validated);
|
|
411
|
+
await ReservationApi.updateReservationSetting(currentRestaurant.value._id, validated)
|
|
412
|
+
await init()
|
|
413
|
+
})
|
|
414
|
+
showSuccess(t('order.settingUpdated'))
|
|
415
|
+
} finally {
|
|
416
|
+
isSaving.value = false
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function init() {
|
|
421
|
+
if (currentRestaurant.value) {
|
|
422
|
+
const settings = await readReservationSetting()
|
|
423
|
+
reservationSettings.value = {
|
|
424
|
+
ranges: settings.ranges || [],
|
|
425
|
+
posCanOverbook: settings.posCanOverbook || false,
|
|
426
|
+
draftHoldTimeMinutes: settings.draftHoldTimeMinutes || 15,
|
|
427
|
+
smsEnabled: settings.smsEnabled ?? true,
|
|
428
|
+
emailEnabled: settings.emailEnabled ?? true,
|
|
429
|
+
notifyOnConfirm: settings.notifyOnConfirm ?? true,
|
|
430
|
+
notifyOnCancel: settings.notifyOnCancel ?? true
|
|
431
|
+
}
|
|
432
|
+
// Sync rangeSetting with the first range if it exists
|
|
433
|
+
if (settings.ranges && settings.ranges.length > 0) {
|
|
434
|
+
rangeSetting.value = settings.ranges[0]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Reset custom mode - let the computed property determine the initial state
|
|
438
|
+
isCustomMode.value = false
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function updateSetting<K extends keyof FdoOrderReservationSettingsV2>(
|
|
443
|
+
key: K,
|
|
444
|
+
value: FdoOrderReservationSettingsV2[K]
|
|
445
|
+
) {
|
|
446
|
+
reservationSettings.value[key] = value
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function formatOperatingHours(windows: Array<{ start: string; end: string }>) {
|
|
450
|
+
if (windows.length === 0) return t('order.noOperatingHours')
|
|
451
|
+
if (windows.length === 1) return `${windows[0].start} - ${windows[0].end}`
|
|
452
|
+
return `${windows[0].start} - ${windows[0].end} (+${windows.length - 1})`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function updateMaxLeadPreset(value: number | 'custom') {
|
|
456
|
+
if (value === 'custom') {
|
|
457
|
+
// User explicitly selected custom mode
|
|
458
|
+
isCustomMode.value = true
|
|
459
|
+
// Switch to custom mode - set to a non-preset value to trigger custom UI
|
|
460
|
+
// Keep the current value if it's already custom, otherwise set to 1 day as starting point
|
|
461
|
+
if (
|
|
462
|
+
rangeSetting.value.maxLeadDuration.unit !== 'day' ||
|
|
463
|
+
![30, 60, 90].includes(rangeSetting.value.maxLeadDuration.value)
|
|
464
|
+
) {
|
|
465
|
+
// Already custom, don't change
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
// Set to 1 day as default custom value
|
|
469
|
+
rangeSetting.value.maxLeadDuration = {
|
|
470
|
+
value: 1,
|
|
471
|
+
unit: 'day'
|
|
472
|
+
}
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
// User selected a preset - exit custom mode
|
|
476
|
+
isCustomMode.value = false
|
|
477
|
+
rangeSetting.value.maxLeadDuration = {
|
|
478
|
+
value,
|
|
479
|
+
unit: 'day'
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function updateMinLeadValue(value: number) {
|
|
484
|
+
rangeSetting.value.minLeadDuration.value = value
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function updateMinLeadUnit(unit: 'hour' | 'day') {
|
|
488
|
+
rangeSetting.value.minLeadDuration.unit = unit
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function updateMaxLeadValue(value: number) {
|
|
492
|
+
rangeSetting.value.maxLeadDuration.value = value
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function updateMaxLeadUnit(unit: 'hour' | 'day') {
|
|
496
|
+
rangeSetting.value.maxLeadDuration.unit = unit
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function updateSlotInterval(value: number) {
|
|
500
|
+
rangeSetting.value.slotInterval = value
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function updateBookingDuration(value: number) {
|
|
504
|
+
rangeSetting.value.bookingDuration = value
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function addPreferenceOption(
|
|
508
|
+
preference: (typeof rangeSetting.value.preferences)[number],
|
|
509
|
+
value?: string
|
|
510
|
+
) {
|
|
511
|
+
if (value || optionText.value.trim()) {
|
|
512
|
+
if (!preference.options) {
|
|
513
|
+
preference.options = []
|
|
514
|
+
}
|
|
515
|
+
preference.options.push(value ?? optionText.value.trim())
|
|
516
|
+
optionText.value = ''
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function removePreferenceOption(
|
|
521
|
+
preference: (typeof rangeSetting.value.preferences)[number],
|
|
522
|
+
option: string
|
|
523
|
+
) {
|
|
524
|
+
if (preference.options) {
|
|
525
|
+
const index = preference.options.indexOf(option)
|
|
526
|
+
if (index > -1) {
|
|
527
|
+
preference.options.splice(index, 1)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function addPreference() {
|
|
533
|
+
rangeSetting.value.preferences.push({
|
|
534
|
+
name: '',
|
|
535
|
+
type: 'radio',
|
|
536
|
+
options: ['Option 1']
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function removePreference(index: number) {
|
|
541
|
+
rangeSetting.value.preferences.splice(index, 1)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function startEditingOption(prefIndex: number, optIndex: number) {
|
|
545
|
+
editingOption.value = { prefIndex, optIndex }
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function saveOptionEdit(prefIndex: number, optIndex: number, newValue: string) {
|
|
549
|
+
if (newValue.trim()) {
|
|
550
|
+
rangeSetting.value.preferences[prefIndex].options[optIndex] = newValue.trim()
|
|
551
|
+
}
|
|
552
|
+
editingOption.value = null
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isEditingOption(prefIndex: number, optIndex: number): boolean {
|
|
556
|
+
return editingOption.value?.prefIndex === prefIndex && editingOption.value?.optIndex === optIndex
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function toggleGuestMessage(enabled: boolean) {
|
|
560
|
+
if (enabled) {
|
|
561
|
+
rangeSetting.value.guestMessage = DEFAULT_GUEST_MESSAGE
|
|
562
|
+
} else {
|
|
563
|
+
rangeSetting.value.guestMessage = null
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function toggleCancellationPolicy(enabled: boolean) {
|
|
568
|
+
if (enabled) {
|
|
569
|
+
rangeSetting.value.cancellationPolicy = DEFAULT_CANCELLATION_POLICY
|
|
570
|
+
} else {
|
|
571
|
+
rangeSetting.value.cancellationPolicy = null
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function toggleSegmentExpansion(segment: 'morning' | 'afternoon' | 'evening') {
|
|
576
|
+
expandedSegments.value[segment] = !expandedSegments.value[segment]
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Validation for operating hours
|
|
580
|
+
function validateTimeRange(day: 0 | 1 | 2 | 3 | 4 | 5 | 6, hourIndex: number): string | null {
|
|
581
|
+
const dayHours = rangeSetting.value.operatingHours[day].hours
|
|
582
|
+
const range = dayHours[hourIndex]
|
|
583
|
+
|
|
584
|
+
// Validate same day time range (end > start)
|
|
585
|
+
if (!validateSameDayTimeRange(range)) {
|
|
586
|
+
return 'End time must be after start time'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Check for overlaps with other time ranges
|
|
590
|
+
for (let i = 0; i < dayHours.length; i++) {
|
|
591
|
+
if (i === hourIndex) continue
|
|
592
|
+
|
|
593
|
+
// Use shared utility to check overlap
|
|
594
|
+
if (validateTimeRangeOverlap(range, dayHours[i])) {
|
|
595
|
+
return 'Time ranges cannot overlap'
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return null
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function getTimeRangeError<T>(day: T, hourIndex: number): string | null {
|
|
603
|
+
return validateTimeRange(day as 0 | 1 | 2 | 3 | 4 | 5 | 6, hourIndex)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function hasAnyTimeRangeErrors(): boolean {
|
|
607
|
+
for (let day = 0; day <= 6; day++) {
|
|
608
|
+
const dayHours = rangeSetting.value.operatingHours[day as 0 | 1 | 2 | 3 | 4 | 5 | 6].hours
|
|
609
|
+
for (let i = 0; i < dayHours.length; i++) {
|
|
610
|
+
if (validateTimeRange(day as 0 | 1 | 2 | 3 | 4 | 5 | 6, i)) {
|
|
611
|
+
return true
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return false
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function deleteTimeRange(day: 0 | 1 | 2 | 3 | 4 | 5 | 6, hourIndex: number) {
|
|
619
|
+
rangeSetting.value.operatingHours[day].hours.splice(hourIndex, 1)
|
|
620
|
+
|
|
621
|
+
// If no time ranges left, turn off the toggle
|
|
622
|
+
if (rangeSetting.value.operatingHours[day].hours.length === 0) {
|
|
623
|
+
rangeSetting.value.operatingHours[day].enable = false
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Validation for lead duration
|
|
628
|
+
function validateLeadDurationLocal(): string | null {
|
|
629
|
+
const { minLeadDuration, maxLeadDuration } = rangeSetting.value
|
|
630
|
+
|
|
631
|
+
// Use shared utility to validate
|
|
632
|
+
if (!validateLeadDuration(minLeadDuration, maxLeadDuration)) {
|
|
633
|
+
return 'Maximum lead time must be greater than minimum lead time'
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (minLeadDuration.value < 0 || maxLeadDuration.value < 0) {
|
|
637
|
+
return 'Lead time values must be positive'
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return null
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function convertToMinutes(value: number, unit: 'minute' | 'hour' | 'day'): number {
|
|
644
|
+
// This function is kept for backward compatibility but could be removed
|
|
645
|
+
// in favor of using convertDurationToMinutes directly
|
|
646
|
+
switch (unit) {
|
|
647
|
+
case 'minute':
|
|
648
|
+
return value
|
|
649
|
+
case 'hour':
|
|
650
|
+
return value * 60
|
|
651
|
+
case 'day':
|
|
652
|
+
return value * 60 * 24
|
|
653
|
+
default:
|
|
654
|
+
return value
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function handleCopySettings(copiedSettings: Partial<FdoOrderReservationSettingsV2>) {
|
|
659
|
+
// Apply the copied settings to the current settings
|
|
660
|
+
if (copiedSettings.ranges && copiedSettings.ranges.length > 0) {
|
|
661
|
+
const copiedRange = copiedSettings.ranges[0]
|
|
662
|
+
|
|
663
|
+
// Update the first range in reservationSettings
|
|
664
|
+
if (reservationSettings.value.ranges && reservationSettings.value.ranges.length > 0) {
|
|
665
|
+
// Merge the copied range properties into the existing range
|
|
666
|
+
reservationSettings.value.ranges[0] = {
|
|
667
|
+
...reservationSettings.value.ranges[0],
|
|
668
|
+
...copiedRange
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// This will automatically sync to rangeSetting via the watch
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Reset custom mode when copying settings - let computed property determine state
|
|
676
|
+
isCustomMode.value = false
|
|
677
|
+
|
|
678
|
+
showSuccess('Settings copied successfully. Please review and save changes.')
|
|
679
|
+
}
|
|
680
|
+
</script>
|
|
681
|
+
|
|
682
|
+
<template>
|
|
683
|
+
<RestaurantSelector />
|
|
684
|
+
<div v-if="isLoading" class="flex justify-center items-center min-h-[400px]">
|
|
685
|
+
<FmCircularProgress size="xxl" />
|
|
686
|
+
</div>
|
|
687
|
+
<div v-else class="p-[1.5rem] flex flex-col gap-24 w-full max-w-4xl">
|
|
688
|
+
<!-- <FmCard variant="outlined" class="p-5">
|
|
689
|
+
<div class="mb-5">
|
|
690
|
+
<FmSwitch
|
|
691
|
+
:model-value="reservationSettings.smsEnabled"
|
|
692
|
+
@update:model-value="(v: boolean) => updateSetting('smsEnabled', v)"
|
|
693
|
+
:label="t('order.smsEnabled')"
|
|
694
|
+
label-placement="right"
|
|
695
|
+
:sublabel="t('order.smsEnabledDescription')"
|
|
696
|
+
/>
|
|
697
|
+
</div>
|
|
698
|
+
<div class="mb-5">
|
|
699
|
+
<FmSwitch
|
|
700
|
+
:model-value="reservationSettings.emailEnabled"
|
|
701
|
+
@update:model-value="(v: boolean) => updateSetting('emailEnabled', v)"
|
|
702
|
+
:label="t('order.emailEnabled')"
|
|
703
|
+
label-placement="right"
|
|
704
|
+
:sublabel="t('order.emailEnabledDescription')"
|
|
705
|
+
/>
|
|
706
|
+
</div>
|
|
707
|
+
<div class="mb-5">
|
|
708
|
+
<FmSwitch
|
|
709
|
+
:model-value="reservationSettings.notifyOnConfirm"
|
|
710
|
+
@update:model-value="(v: boolean) => updateSetting('notifyOnConfirm', v)"
|
|
711
|
+
:label="t('order.notifyOnConfirm')"
|
|
712
|
+
label-placement="right"
|
|
713
|
+
:sublabel="t('order.notifyOnConfirmDescription')"
|
|
714
|
+
/>
|
|
715
|
+
</div>
|
|
716
|
+
<div class="mb-5">
|
|
717
|
+
<FmSwitch
|
|
718
|
+
:model-value="reservationSettings.notifyOnCancel"
|
|
719
|
+
@update:model-value="(v: boolean) => updateSetting('notifyOnCancel', v)"
|
|
720
|
+
:label="t('order.notifyOnCancel')"
|
|
721
|
+
label-placement="right"
|
|
722
|
+
:sublabel="t('order.notifyOnCancelDescription')"
|
|
723
|
+
/>
|
|
724
|
+
</div>
|
|
725
|
+
</FmCard> -->
|
|
726
|
+
|
|
727
|
+
<div>
|
|
728
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-16">
|
|
729
|
+
{{ t('order.reservationStatus') }}
|
|
730
|
+
</div>
|
|
731
|
+
<FmSwitch label-placement="right" :label="'Enable reservation'"
|
|
732
|
+
:sublabel="'Enable this to make the outlet available for reservations. This setting does not impact walk-in dining.'"
|
|
733
|
+
:model-value="rangeSetting.enable ?? false" @update:model-value="(v: boolean) => (rangeSetting.enable = v)" />
|
|
734
|
+
|
|
735
|
+
<div class="ml-56 my-8">
|
|
736
|
+
<CopySettingsSheet :current-settings="reservationSettings" @apply="handleCopySettings" />
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
|
|
740
|
+
<template v-if="rangeSetting.enable">
|
|
741
|
+
<!-- Notification Settings Section -->
|
|
742
|
+
<div class="flex flex-col">
|
|
743
|
+
<div class="flex-grow fm-typo-en-title-sm-600 my-8">
|
|
744
|
+
{{ t('order.notificationSettings') }}
|
|
745
|
+
</div>
|
|
746
|
+
<FmCard variant="outlined" class="p-5">
|
|
747
|
+
<div class="mb-5">
|
|
748
|
+
<FmTextField type="number" :model-value="reservationSettings.draftHoldTimeMinutes" @update:model-value="
|
|
749
|
+
(v: string | number) =>
|
|
750
|
+
updateSetting('draftHoldTimeMinutes', v === '' ? 15 : Number(v))
|
|
751
|
+
" :label="t('order.draftHoldTimeMinutes')" suffix="minutes" :hint="t('order.draftHoldTimeDescription')"
|
|
752
|
+
class="max-w-md" />
|
|
753
|
+
</div>
|
|
754
|
+
<div class="mb-5">
|
|
755
|
+
<FmSwitch :model-value="reservationSettings.posCanOverbook"
|
|
756
|
+
@update:model-value="(v: boolean) => updateSetting('posCanOverbook', v)"
|
|
757
|
+
:label="t('order.posCanOverbook')" label-placement="right"
|
|
758
|
+
:sublabel="t('order.posCanOverbookDescription')" />
|
|
759
|
+
</div>
|
|
760
|
+
</FmCard>
|
|
761
|
+
</div>
|
|
762
|
+
|
|
763
|
+
<div class="flex flex-col">
|
|
764
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-4">
|
|
765
|
+
{{ t('order.reservationAvailability') }}
|
|
766
|
+
</div>
|
|
767
|
+
<div class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary mb-24">
|
|
768
|
+
This shows when guests can make reservations based on your current settings. The
|
|
769
|
+
availability updates automatically.
|
|
770
|
+
</div>
|
|
771
|
+
<!-- Availability settings (eg. the min/max lead, and time interval + booking duration) -->
|
|
772
|
+
<div class="mb-24">
|
|
773
|
+
<div class="mb-8">Preview</div>
|
|
774
|
+
<div class="flex items-center gap-4 mb-8">
|
|
775
|
+
<div
|
|
776
|
+
class="w-fit border-1 border-fm-color-neutral-gray-200 rounded-md py-4 px-8 bg-fm-color-neutral-gray-100 flex items-center gap-8 text-fm-color-typo-tertiary">
|
|
777
|
+
<FmIcon name="calendar_month" outline />
|
|
778
|
+
<div class="pr-6">
|
|
779
|
+
{{ reservationPreview.start + ' ' + reservationPreview.startHour }}
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
<div>to</div>
|
|
783
|
+
<div
|
|
784
|
+
class="w-fit border-1 border-fm-color-neutral-gray-200 rounded-md py-4 px-8 bg-fm-color-neutral-gray-100 flex items-center gap-8 text-fm-color-typo-tertiary">
|
|
785
|
+
<FmIcon name="calendar_month" outline />
|
|
786
|
+
<div class="pr-6">
|
|
787
|
+
{{ reservationPreview.end + ' ' + reservationPreview.endHour }}
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
<div class="fm-typo-en-body-md-400 text-fm-color-typo-secondary">
|
|
792
|
+
Reservations will open on
|
|
793
|
+
<strong> {{ reservationPreview.start + ', ' + reservationPreview.startHour }} </strong>.
|
|
794
|
+
Guests will not be able to make reservations before this time.
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
|
|
798
|
+
<div class="mb-24">
|
|
799
|
+
<div class="mb-8">Presets</div>
|
|
800
|
+
<FmRadioGroup :model-value="selectedPreset"
|
|
801
|
+
@update:model-value="(v: number | 'custom') => updateMaxLeadPreset(v)">
|
|
802
|
+
<FmRadio label="30 days" :value="30">
|
|
803
|
+
<template #label>
|
|
804
|
+
<div>30 days <span class="text-fm-color-typo-secondary">(default)</span></div>
|
|
805
|
+
</template>
|
|
806
|
+
</FmRadio>
|
|
807
|
+
<FmRadio label="60 days" :value="60" />
|
|
808
|
+
<FmRadio label="90 days" :value="90" />
|
|
809
|
+
<FmRadio label="Custom" :value="'custom'" />
|
|
810
|
+
</FmRadioGroup>
|
|
811
|
+
|
|
812
|
+
<!-- Custom preset fields -->
|
|
813
|
+
<div v-if="selectedPreset === 'custom'"
|
|
814
|
+
class="ml-32 mt-12 p-16 border rounded-md bg-fm-color-neutral-gray-50">
|
|
815
|
+
<div class="mb-16">
|
|
816
|
+
<div class="mb-8 fm-typo-en-body-md-600 flex items-center gap-8">
|
|
817
|
+
Minimum Lead Time
|
|
818
|
+
<FmTooltip
|
|
819
|
+
:content="'The earliest time a guest can make a reservation. For example, if you set this to 1 day, guests can only make reservations from tomorrow onwards.'">
|
|
820
|
+
<FmIcon name="info" outline size="sm" class="cursor-pointer" />
|
|
821
|
+
</FmTooltip>
|
|
822
|
+
</div>
|
|
823
|
+
<div class="flex items-center gap-8">
|
|
824
|
+
<FmStepperField :model-value="rangeSetting.minLeadDuration.value"
|
|
825
|
+
@update:model-value="(v: number) => updateMinLeadValue(v)" :min="0" class="" />
|
|
826
|
+
<FmSelect :model-value="rangeSetting.minLeadDuration.unit"
|
|
827
|
+
@update:model-value="(v: 'hour' | 'day') => updateMinLeadUnit(v)" :items="[
|
|
828
|
+
{ label: 'Hours', value: 'hour' },
|
|
829
|
+
{ label: 'Days', value: 'day' }
|
|
830
|
+
]" class="w-120" />
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
<div>
|
|
835
|
+
<div class="mb-8 fm-typo-en-body-md-600 flex items-center gap-8">
|
|
836
|
+
Maximum Lead Time
|
|
837
|
+
<FmTooltip
|
|
838
|
+
:content="'The furthest in advance a guest can make a reservation. For example, if you set this to 30 days, guests can only make reservations up to 30 days from today.'">
|
|
839
|
+
<FmIcon name="info" outline size="sm" class="cursor-pointer" />
|
|
840
|
+
</FmTooltip>
|
|
841
|
+
</div>
|
|
842
|
+
<div class="flex items-center gap-8">
|
|
843
|
+
<FmStepperField :model-value="rangeSetting.maxLeadDuration.value"
|
|
844
|
+
@update:model-value="(v: number) => updateMaxLeadValue(v)" :min="0" class="" />
|
|
845
|
+
<FmSelect :model-value="rangeSetting.maxLeadDuration.unit"
|
|
846
|
+
@update:model-value="(v: 'hour' | 'day') => updateMaxLeadUnit(v)" :items="[
|
|
847
|
+
{ label: 'Hours', value: 'hour' },
|
|
848
|
+
{ label: 'Days', value: 'day' }
|
|
849
|
+
]" class="w-120" />
|
|
850
|
+
</div>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<!-- Validation error message -->
|
|
854
|
+
<div v-if="validateLeadDurationLocal()" class="mt-16 text-sm text-red-600">
|
|
855
|
+
{{ validateLeadDurationLocal() }}
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
861
|
+
<div class="flex flex-col">
|
|
862
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-4">
|
|
863
|
+
{{ t('order.operatingWindows') }}
|
|
864
|
+
</div>
|
|
865
|
+
<div class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary mb-12">
|
|
866
|
+
Set the hours your outlet is open for business. Reservations and walk-ins outside these
|
|
867
|
+
hours will not be allowed.
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
<!-- Operating hour setting -->
|
|
871
|
+
<div class="mb-32">
|
|
872
|
+
<div class="grid grid-cols-[1fr_1fr_3fr] items-start">
|
|
873
|
+
<div class="p-12 fm-typo-en-body-md-600 text-fm-color-typo-secondary bg-fm-color-neutral-gray-100">
|
|
874
|
+
Day
|
|
875
|
+
</div>
|
|
876
|
+
<div class="p-12 fm-typo-en-body-md-600 text-fm-color-typo-secondary bg-fm-color-neutral-gray-100">
|
|
877
|
+
Open / Closed
|
|
878
|
+
</div>
|
|
879
|
+
<div class="p-12 fm-typo-en-body-md-600 text-fm-color-typo-secondary bg-fm-color-neutral-gray-100">
|
|
880
|
+
Hours
|
|
881
|
+
</div>
|
|
882
|
+
<template v-for="day in [1, 2, 3, 4, 5, 6, 0] as const" :key="day">
|
|
883
|
+
<div class="fm-typo-en-body-md-400 py-12 px-12">
|
|
884
|
+
{{ moment().day(day).format('dddd') }}
|
|
885
|
+
</div>
|
|
886
|
+
<template v-if="rangeSetting.operatingHours[day as 0 | 1 | 2 | 3 | 4 | 5 | 6]">
|
|
887
|
+
<template v-for="hours in [rangeSetting.operatingHours[day as 0 | 1 | 2 | 3 | 4 | 5 | 6]]"
|
|
888
|
+
:key="`hours-${day}`">
|
|
889
|
+
<div class="px-12 py-8">
|
|
890
|
+
<FmSwitch label-placement="right" :model-value="hours.enable" @update:model-value="
|
|
891
|
+
(v) => {
|
|
892
|
+
rangeSetting.operatingHours[day].enable = v
|
|
893
|
+
rangeSetting.operatingHours[day].hours = v
|
|
894
|
+
? [
|
|
895
|
+
{
|
|
896
|
+
start: '00:00',
|
|
897
|
+
end: '23:59'
|
|
898
|
+
}
|
|
899
|
+
]
|
|
900
|
+
: []
|
|
901
|
+
}
|
|
902
|
+
" :label="hours.enable ? 'Open' : 'Closed'" />
|
|
903
|
+
</div>
|
|
904
|
+
<div class="px-12 self-center">
|
|
905
|
+
<div v-if="!hours.enable" class="">-</div>
|
|
906
|
+
<div v-else class="flex flex-col">
|
|
907
|
+
<div v-for="(hour, hi) in hours.hours" :key="hi" class="flex flex-col gap-4">
|
|
908
|
+
<div class="flex gap-4 items-center justify-between">
|
|
909
|
+
<div class="flex gap-12 items-center flex-1 justify-between py-8">
|
|
910
|
+
<CustomTimePicker v-model="hour.start" :min-time="hours.hours[hi - 1]?.end ?? '00:00'"
|
|
911
|
+
class="grow" />
|
|
912
|
+
<div class="text-center w-16 flex-shrink-0">to</div>
|
|
913
|
+
<CustomTimePicker v-model="hour.end" :min-time="hour.start" class="grow" />
|
|
914
|
+
</div>
|
|
915
|
+
<div class="mr-8 w-32 flex-shrink">
|
|
916
|
+
<FmButton variant="plain" icon="add" v-if="hi == 0" :disabled="hours.hours.length >= 2"
|
|
917
|
+
@click="hours.hours.push({ start: hour.end, end: '23:59' })" />
|
|
918
|
+
<FmButton variant="plain" icon="delete" v-if="hi > 0" @click="
|
|
919
|
+
deleteTimeRange(day as unknown as 0 | 1 | 2 | 3 | 4 | 5 | 6, hi)
|
|
920
|
+
" />
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
<div v-if="getTimeRangeError(day, hi)" class="text-sm text-red-600 ml-4">
|
|
924
|
+
{{ getTimeRangeError(day, hi) }}
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
</template>
|
|
930
|
+
</template>
|
|
931
|
+
</template>
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
|
|
935
|
+
<FmCard variant="outlined" class="grid grid-cols-[1fr_2fr]">
|
|
936
|
+
<div class="border-r py-[24px] px-16">
|
|
937
|
+
<div class="mb-24">
|
|
938
|
+
<div class="mb-8 fm-typo-en-body-lg-600">Time Interval</div>
|
|
939
|
+
<FmRadioGroup :model-value="rangeSetting.slotInterval"
|
|
940
|
+
@update:model-value="(v: number) => updateSlotInterval(v)">
|
|
941
|
+
<FmRadio label="15 min" :value="15" />
|
|
942
|
+
<FmRadio label="30 min (default)" :value="30">
|
|
943
|
+
<template #label>30 min <span class="text-fm-color-typo-secondary">(default)</span></template>
|
|
944
|
+
</FmRadio>
|
|
945
|
+
<FmRadio label="60 min" :value="60" />
|
|
946
|
+
</FmRadioGroup>
|
|
947
|
+
</div>
|
|
948
|
+
|
|
949
|
+
<div class="mb-24">
|
|
950
|
+
<div class="mb-8 fm-typo-en-body-lg-600">Dining Duration</div>
|
|
951
|
+
<FmRadioGroup :model-value="rangeSetting.bookingDuration"
|
|
952
|
+
@update:model-value="(v: number) => updateBookingDuration(v)">
|
|
953
|
+
<FmRadio label="60 min" :value="60" />
|
|
954
|
+
<FmRadio label="90 min" :value="90">
|
|
955
|
+
<template #label>90 min <span class="text-fm-color-typo-secondary">(default)</span></template>
|
|
956
|
+
</FmRadio>
|
|
957
|
+
|
|
958
|
+
<FmRadio label="120 min" :value="120" />
|
|
959
|
+
</FmRadioGroup>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
<div class="p-16 flex flex-col gap-16">
|
|
964
|
+
<div class="flex items-center justify-between w-full">
|
|
965
|
+
<div class="fm-typo-en-body-lg-600">Available reservation slots (preview)</div>
|
|
966
|
+
<FmSelect v-model="selectedPreviewDay" :items="[
|
|
967
|
+
{ label: 'Monday', value: 1 },
|
|
968
|
+
{ label: 'Tuesday', value: 2 },
|
|
969
|
+
{ label: 'Wednesday', value: 3 },
|
|
970
|
+
{ label: 'Thursday', value: 4 },
|
|
971
|
+
{ label: 'Friday', value: 5 },
|
|
972
|
+
{ label: 'Saturday', value: 6 },
|
|
973
|
+
{ label: 'Sunday', value: 0 }
|
|
974
|
+
]" />
|
|
975
|
+
</div>
|
|
976
|
+
|
|
977
|
+
<template v-if="availableSlots.morning.length > 0">
|
|
978
|
+
<div class="fm-typo-en-body-md-600">Morning</div>
|
|
979
|
+
<div class="grid grid-cols-5 gap-8">
|
|
980
|
+
<div v-for="slot in displayedSlots.morning" :key="slot.time"
|
|
981
|
+
class="border-1 rounded-md text-center p-8 transition-colors" :class="{
|
|
982
|
+
'bg-[#fafafa] text-fm-color-typo-primary': slot.available,
|
|
983
|
+
'bg-[#f0f0f0] text-fm-color-typo-disabled border-dashed': !slot.available
|
|
984
|
+
}">
|
|
985
|
+
{{ slot.time }}
|
|
986
|
+
</div>
|
|
987
|
+
<div v-if="hasMoreItems.morning && !expandedSegments.morning"
|
|
988
|
+
class="border-2 rounded-md text-center p-8 transition-colors cursor-pointer border-fm-color-primary text-fm-color-primary"
|
|
989
|
+
@click="toggleSegmentExpansion('morning')">
|
|
990
|
+
more
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
</template>
|
|
994
|
+
|
|
995
|
+
<template v-if="availableSlots.afternoon.length > 0">
|
|
996
|
+
<div class="fm-typo-en-body-md-600">Afternoon</div>
|
|
997
|
+
<div class="grid grid-cols-5 gap-8">
|
|
998
|
+
<div v-for="slot in displayedSlots.afternoon" :key="slot.time"
|
|
999
|
+
class="border-1 rounded-md text-center p-8 transition-colors" :class="{
|
|
1000
|
+
'bg-[#fafafa] text-fm-color-typo-primary': slot.available,
|
|
1001
|
+
'bg-[#f0f0f0] text-fm-color-typo-disabled border-dashed': !slot.available
|
|
1002
|
+
}">
|
|
1003
|
+
{{ slot.time }}
|
|
1004
|
+
</div>
|
|
1005
|
+
<div v-if="hasMoreItems.afternoon && !expandedSegments.afternoon"
|
|
1006
|
+
class="border-2 rounded-md text-center p-8 transition-colors cursor-pointer border-fm-color-primary text-fm-color-primary"
|
|
1007
|
+
@click="toggleSegmentExpansion('afternoon')">
|
|
1008
|
+
more
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
</template>
|
|
1012
|
+
|
|
1013
|
+
<template v-if="availableSlots.evening.length > 0">
|
|
1014
|
+
<div class="fm-typo-en-body-md-600">Evening</div>
|
|
1015
|
+
<div class="grid grid-cols-5 gap-8">
|
|
1016
|
+
<div v-for="slot in displayedSlots.evening" :key="slot.time"
|
|
1017
|
+
class="border-1 rounded-md text-center p-8 transition-colors" :class="{
|
|
1018
|
+
'bg-[#fafafa] text-fm-color-typo-primary': slot.available,
|
|
1019
|
+
'bg-[#f0f0f0] text-fm-color-typo-disabled border-dashed': !slot.available
|
|
1020
|
+
}">
|
|
1021
|
+
{{ slot.time }}
|
|
1022
|
+
</div>
|
|
1023
|
+
<div v-if="hasMoreItems.evening && !expandedSegments.evening"
|
|
1024
|
+
class="border-2 rounded-md text-center p-8 transition-colors cursor-pointer border-fm-color-primary text-fm-color-primary"
|
|
1025
|
+
@click="toggleSegmentExpansion('evening')">
|
|
1026
|
+
more
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
</template>
|
|
1030
|
+
|
|
1031
|
+
<div v-if="
|
|
1032
|
+
!rangeSetting.operatingHours[selectedPreviewDay as 0 | 1 | 2 | 3 | 4 | 5 | 6]
|
|
1033
|
+
?.enable
|
|
1034
|
+
" class="text-center text-fm-color-typo-secondary flex flex-col items-center gap-16 p-24">
|
|
1035
|
+
<img :src="notfound" class="aspect-square w-[150px]" />
|
|
1036
|
+
Uh-oh! This outlet is closed on the selected day. <br>
|
|
1037
|
+
Please select another day to view available time slots.
|
|
1038
|
+
</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
</FmCard>
|
|
1041
|
+
</div>
|
|
1042
|
+
|
|
1043
|
+
<div class="flex flex-col">
|
|
1044
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-4">
|
|
1045
|
+
{{ t('order.capacity') }}
|
|
1046
|
+
</div>
|
|
1047
|
+
<div class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary mb-24">
|
|
1048
|
+
Set the maximum number of guests allowed for each time slot.
|
|
1049
|
+
</div>
|
|
1050
|
+
|
|
1051
|
+
<!-- Table Capacity Settings -->
|
|
1052
|
+
<div>
|
|
1053
|
+
<div class="grid grid-cols-[4fr_4fr_4fr_1fr]">
|
|
1054
|
+
<div class="bg-fm-color-neutral-gray-100 p-12">Table Type</div>
|
|
1055
|
+
<div class="bg-fm-color-neutral-gray-100 p-12">Guest Range</div>
|
|
1056
|
+
<div class="bg-fm-color-neutral-gray-100 p-12">Available Table Count</div>
|
|
1057
|
+
<div class="bg-fm-color-neutral-gray-100 p-12"></div>
|
|
1058
|
+
</div>
|
|
1059
|
+
<div class="grid grid-cols-[4fr_4fr_4fr_1fr] items-center gap-8" v-for="tier in rangeSetting.capacityTiers"
|
|
1060
|
+
:key="tier._id">
|
|
1061
|
+
<div class="p-8">{{ tier.minPax }}{{ tier.maxPax ? `-${tier.maxPax}` : '+' }} pax</div>
|
|
1062
|
+
<div class="flex items-center gap-4">
|
|
1063
|
+
<FmStepperField v-model="tier.minPax" />
|
|
1064
|
+
<div>to</div>
|
|
1065
|
+
<FmStepperField :model-value="tier.maxPax ?? null" @update:model-value="(v) => (tier.maxPax = v)" />
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="flex items-center gap-8">
|
|
1068
|
+
<FmButton variant="tertiary" icon="remove" @click="tier.capacity--" :disabled="tier.capacity <= 1" />
|
|
1069
|
+
<div class="w-32 text-center">{{ tier.capacity }}</div>
|
|
1070
|
+
<FmButton variant="tertiary" icon="add" @click="tier.capacity++" />
|
|
1071
|
+
|
|
1072
|
+
<!-- <FmStepperField :model-value="tier.capacity ?? null" @update:model-value="(v) => (tier.capacity = v)" /> -->
|
|
1073
|
+
</div>
|
|
1074
|
+
<FmButton icon="delete" variant="plain" @click="
|
|
1075
|
+
rangeSetting.capacityTiers.splice(rangeSetting.capacityTiers.indexOf(tier), 1)
|
|
1076
|
+
" />
|
|
1077
|
+
</div>
|
|
1078
|
+
<div>
|
|
1079
|
+
<FmButton label="Add table type" icon="add" variant="plain" @click="
|
|
1080
|
+
rangeSetting.capacityTiers.push({
|
|
1081
|
+
_id: new Date().toISOString(),
|
|
1082
|
+
minPax: ([...rangeSetting.capacityTiers].pop()?.maxPax ?? 0) + 1,
|
|
1083
|
+
maxPax: null,
|
|
1084
|
+
capacity: 0
|
|
1085
|
+
})
|
|
1086
|
+
" />
|
|
1087
|
+
</div>
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
|
|
1091
|
+
<div>
|
|
1092
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-16">Preorder</div>
|
|
1093
|
+
<FmSwitch label-placement="right" :label="'Enable preorder'"
|
|
1094
|
+
:sublabel="'Enable this to allow guests to place preorders when making a reservation. This allows them to order food in advance.'"
|
|
1095
|
+
:model-value="rangeSetting.enablePreorder"
|
|
1096
|
+
@update:model-value="(v: boolean) => (rangeSetting.enablePreorder = v)" />
|
|
1097
|
+
</div>
|
|
1098
|
+
|
|
1099
|
+
<div class="flex flex-col">
|
|
1100
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-4">Guest Preferences</div>
|
|
1101
|
+
<div class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary mb-24">
|
|
1102
|
+
Allow guests to select preferences during reservation, such as seating preferences,
|
|
1103
|
+
allergies, or other notes.
|
|
1104
|
+
</div>
|
|
1105
|
+
|
|
1106
|
+
<FmSwitch class="mb-8" label="Enable preferences" label-placement="right"
|
|
1107
|
+
:model-value="rangeSetting.preferences.length > 0" @update:model-value="
|
|
1108
|
+
(v) => {
|
|
1109
|
+
if (v == true) {
|
|
1110
|
+
rangeSetting.preferences = [
|
|
1111
|
+
{
|
|
1112
|
+
name: '',
|
|
1113
|
+
type: 'radio',
|
|
1114
|
+
options: ['Option 1']
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
} else {
|
|
1118
|
+
rangeSetting.preferences = []
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
" />
|
|
1122
|
+
|
|
1123
|
+
<FmCard v-for="(preference, pIndex) in rangeSetting.preferences" class="p-16 mb-12" variant="outlined"
|
|
1124
|
+
:key="pIndex">
|
|
1125
|
+
<div class="fm-typo-en-body-lg-600 mb-8">Category title</div>
|
|
1126
|
+
<div class="grid grid-cols-[6fr_3fr_1fr] items-start gap-24 mb-24">
|
|
1127
|
+
<div>
|
|
1128
|
+
<FmTextField v-model="preference.name" />
|
|
1129
|
+
<div class="fm-typo-en-body-md-400 text-fm-color-typo-tertiary mt-8">
|
|
1130
|
+
This title will be shown to guests during reservation.
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
<CustomSelect v-model="preference.type" :items="[
|
|
1134
|
+
{ label: 'Single choice', value: 'radio', icon: 'radio' },
|
|
1135
|
+
{ label: 'Multiple choice', value: 'checkbox', icon: 'checkbox' }
|
|
1136
|
+
]" />
|
|
1137
|
+
<FmButton variant="tertiary" icon="delete" @click="removePreference(pIndex)" />
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
<div v-if="preference.type == 'checkbox' || preference.type == 'radio'">
|
|
1141
|
+
<div class="mb-8 fm-typo-en-body-md-600">Options</div>
|
|
1142
|
+
<div v-for="(option, oIndex) in preference.options" :key="oIndex"
|
|
1143
|
+
class="flex items-center gap-4 gap-y-8 mb-8">
|
|
1144
|
+
<div class="flex items-center w-full">
|
|
1145
|
+
<FmCheckbox v-if="preference.type == 'checkbox'" disabled :value="option" :model-value="false" readonly
|
|
1146
|
+
class="mr-8" />
|
|
1147
|
+
<FmRadio v-if="preference.type == 'radio'" disabled :value="option" :model-value="false" readonly />
|
|
1148
|
+
|
|
1149
|
+
<!-- Editable option label -->
|
|
1150
|
+
<input v-if="isEditingOption(pIndex, oIndex)" type="text"
|
|
1151
|
+
class="flex-1 outline-none border-b-2 border-fm-color-primary px-4 py-2" :value="option" @blur="
|
|
1152
|
+
(e) => saveOptionEdit(pIndex, oIndex, (e.target as HTMLInputElement).value)
|
|
1153
|
+
" @keyup.enter="
|
|
1154
|
+
(e) => saveOptionEdit(pIndex, oIndex, (e.target as HTMLInputElement).value)
|
|
1155
|
+
" ref="optionInput" />
|
|
1156
|
+
<div v-else-if="option != 'Other'"
|
|
1157
|
+
class="flex-1 cursor-pointer hover:bg-fm-color-neutral-gray-100 px-4 py-2 rounded transition-colors"
|
|
1158
|
+
@click="startEditingOption(pIndex, oIndex)" title="Click to edit">
|
|
1159
|
+
{{ option }}
|
|
1160
|
+
</div>
|
|
1161
|
+
<div v-else-if="option == 'Other'" class="flex-1 px-4 py-2 border-b border-fm-color-neutral-gray-200">
|
|
1162
|
+
Other:
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
<FmButton variant="plain" icon="close" :class="{ 'opacity-0 pointer-events-nonex': oIndex == 0 }"
|
|
1166
|
+
@click="oIndex != 0 && removePreferenceOption(preference, option)" />
|
|
1167
|
+
</div>
|
|
1168
|
+
<div class="flex items-center">
|
|
1169
|
+
<FmCheckbox v-if="preference.type == 'checkbox'" disabled :value="null" :model-value="false" readonly />
|
|
1170
|
+
<div v-if="preference.type == 'radio'" class="flex items-center">
|
|
1171
|
+
<FmRadio disabled :value="null" :model-value="false" readonly />
|
|
1172
|
+
</div>
|
|
1173
|
+
<div class="flex items-center w-full">
|
|
1174
|
+
<FmButton variant="tertiary" class="text-fm-color-typo-secondary" label="add option" @click="
|
|
1175
|
+
addPreferenceOption(preference, `Option ${preference.options.length + 1}`)
|
|
1176
|
+
">
|
|
1177
|
+
<template #default>
|
|
1178
|
+
<div>Add option</div>
|
|
1179
|
+
</template>
|
|
1180
|
+
</FmButton>
|
|
1181
|
+
<template v-if="!preference.options.includes('Other')">
|
|
1182
|
+
<div>or</div>
|
|
1183
|
+
<FmButton variant="plain" label='add "Other"' @click="addPreferenceOption(preference, 'Other')" />
|
|
1184
|
+
</template>
|
|
1185
|
+
</div>
|
|
1186
|
+
</div>
|
|
1187
|
+
</div>
|
|
1188
|
+
</FmCard>
|
|
1189
|
+
|
|
1190
|
+
<FmCard variant="outlined" class="border-dashed p-16" v-if="rangeSetting.preferences.length">
|
|
1191
|
+
<FmButton label="Add another preference category" icon="add" variant="plain"
|
|
1192
|
+
class="border-1 border-fm-color-primary rounded-lg" @click="addPreference" />
|
|
1193
|
+
</FmCard>
|
|
1194
|
+
</div>
|
|
1195
|
+
|
|
1196
|
+
<div class="flex flex-col">
|
|
1197
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-4">Guest Message</div>
|
|
1198
|
+
<div class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary mb-24">
|
|
1199
|
+
Add important information or notes guests should know before their visit.
|
|
1200
|
+
</div>
|
|
1201
|
+
|
|
1202
|
+
<FmSwitch class="mb-16" label="Enable guest message" label-placement="right" :model-value="rangeSetting.guestMessage !== null && rangeSetting.guestMessage !== undefined
|
|
1203
|
+
" @update:model-value="toggleGuestMessage" />
|
|
1204
|
+
|
|
1205
|
+
<div v-if="rangeSetting.guestMessage !== null && rangeSetting.guestMessage !== undefined">
|
|
1206
|
+
<div class="mb-8 fm-typo-en-body-md-600">Message</div>
|
|
1207
|
+
<FmTextarea v-model="rangeSetting.guestMessage" :maxLength="600"
|
|
1208
|
+
placeholder="Please take note of the following important details before making a reservation:"
|
|
1209
|
+
class="mb-4" />
|
|
1210
|
+
<div class="text-right text-fm-color-typo-tertiary fm-typo-en-body-sm-400">
|
|
1211
|
+
{{ rangeSetting.guestMessage?.length || 0 }} / 600 characters
|
|
1212
|
+
</div>
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
|
|
1216
|
+
<div class="flex flex-col">
|
|
1217
|
+
<div class="flex-grow fm-typo-en-title-sm-600 mb-4">Cancellation Policy</div>
|
|
1218
|
+
<div class="fm-typo-en-body-lg-400 text-fm-color-typo-secondary mb-24">
|
|
1219
|
+
Set the rules guests should follow when cancelling a reservation.
|
|
1220
|
+
</div>
|
|
1221
|
+
|
|
1222
|
+
<FmSwitch class="mb-16" label="Enable cancellation policy" label-placement="right" :model-value="rangeSetting.cancellationPolicy !== null &&
|
|
1223
|
+
rangeSetting.cancellationPolicy !== undefined
|
|
1224
|
+
" @update:model-value="toggleCancellationPolicy" />
|
|
1225
|
+
|
|
1226
|
+
<div v-if="
|
|
1227
|
+
rangeSetting.cancellationPolicy !== null &&
|
|
1228
|
+
rangeSetting.cancellationPolicy !== undefined
|
|
1229
|
+
">
|
|
1230
|
+
<div class="mb-8 fm-typo-en-body-md-600">Message</div>
|
|
1231
|
+
<FmTextarea v-model="rangeSetting.cancellationPolicy" :maxLength="200" placeholder="Cancellation Policy"
|
|
1232
|
+
class="mb-4" />
|
|
1233
|
+
<div class="text-right text-fm-color-typo-tertiary fm-typo-en-body-sm-400">
|
|
1234
|
+
{{ rangeSetting.cancellationPolicy?.length || 0 }} / 200 characters
|
|
1235
|
+
</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
</div>
|
|
1238
|
+
</template>
|
|
1239
|
+
|
|
1240
|
+
<!-- Save Button -->
|
|
1241
|
+
<div class="flex mt-5">
|
|
1242
|
+
<FmButton variant="primary" :label="t('order.saveAllChanges')" class="mr-auto" @click="updateReservationSetting"
|
|
1243
|
+
:loading="isSaving" />
|
|
1244
|
+
</div>
|
|
1245
|
+
</div>
|
|
1246
|
+
</template>
|