@feedmepos/mf-order-setting 0.0.54 → 0.0.56-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.tsbuildinfo +1 -0
  2. package/dist/{KioskDevicesView-CMKNjgWx.js → KioskDevicesView-CccsAZqK.js} +1 -1
  3. package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-B1sNvlUC.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-dF1jgi53.js} +2 -2
  4. package/dist/{KioskSettingView-BE_pMA-i.js → KioskSettingView-8GY7AT-N.js} +128 -126
  5. package/dist/{KioskView-U-Wg8oMC.js → KioskView-DmaCjLcw.js} +4 -4
  6. package/dist/{OrderSettingsView-BWzaITT6.js → OrderSettingsView-BZcU4t9L.js} +28631 -24307
  7. package/dist/{app-CFfgPAd8.js → app-EGmxrjDM.js} +392 -228
  8. package/dist/app.js +1 -1
  9. package/dist/{dayjs.min-CuRr-wlf.js → dayjs.min-lCwCAXUZ.js} +1 -1
  10. package/dist/frontend/mf-order/src/api/reservation/index.d.ts +8 -0
  11. package/dist/frontend/mf-order/src/app.d.ts +164 -0
  12. package/dist/frontend/mf-order/src/main.d.ts +164 -0
  13. package/dist/frontend/mf-order/src/views/order-settings/reservation/CopySettingsSheet.vue.d.ts +186 -0
  14. package/dist/frontend/mf-order/src/views/order-settings/reservation/CustomSelect.vue.d.ts +15 -0
  15. package/dist/frontend/mf-order/src/views/order-settings/reservation/CustomTimePicker.vue.d.ts +10 -0
  16. package/dist/frontend/mf-order/src/views/order-settings/reservation/ReservationSetting.vue.d.ts +2 -0
  17. package/dist/{index-Bj0bCGTm.js → index-CWrX79Jg.js} +8 -8
  18. package/dist/{menu.dto-DAh1J2ES.js → menu.dto-CgymySda.js} +47093 -44304
  19. package/dist/package/entity/index.d.ts +5 -0
  20. package/dist/package/entity/order-setting/order-setting.do.d.ts +861 -0
  21. package/dist/package/entity/order-setting/reservationV2/reservation.do.d.ts +1269 -0
  22. package/dist/package/entity/reservation/reservation.do.d.ts +101 -0
  23. package/dist/package/entity/reservation/reservation.dto.d.ts +325 -0
  24. package/dist/package/entity/reservation/reservation.enum.d.ts +3 -0
  25. package/dist/package/entity/reservation/reservation.utils.d.ts +152 -0
  26. package/dist/style.css +1 -0
  27. package/package.json +1 -1
  28. package/src/api/reservation/index.ts +28 -0
  29. package/src/assets/images/not-found.png +0 -0
  30. package/src/locales/en-US.json +56 -0
  31. package/src/locales/th-TH.json +54 -0
  32. package/src/locales/zh-CN.json +54 -0
  33. package/src/views/kiosk/settings/KioskSettingView.vue +16 -14
  34. package/src/views/order-settings/OrderSettingsView.vue +6 -1
  35. package/src/views/order-settings/reservation/CopySettingsSheet.vue +256 -0
  36. package/src/views/order-settings/reservation/CustomSelect.vue +99 -0
  37. package/src/views/order-settings/reservation/CustomTimePicker.vue +231 -0
  38. package/src/views/order-settings/reservation/ReservationSetting.vue +1311 -0
  39. package/tsconfig.app.json +8 -6
  40. package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +0 -1
@@ -246,6 +246,62 @@
246
246
  "current": "Current",
247
247
  "paxMin": "Pax Min",
248
248
  "paxMax": "Pax Max",
249
+ "reservation": "Reservation",
250
+ "reservationStatus": "Reservation Status",
251
+ "reservationSettings": "Reservation Settings",
252
+ "reservationRanges": "Reservation Ranges",
253
+ "reservationAvailability": "Reservation Availability",
254
+ "addRange": "Add Range",
255
+ "range": "Range",
256
+ "rangeName": "Range Name",
257
+ "priority": "Priority",
258
+ "priorityDescription": "Higher number = higher priority when matching slots",
259
+ "capacity": "Capacity",
260
+ "capacityDescription": "Maximum concurrent reservations for this range",
261
+ "minPax": "Min Pax",
262
+ "maxPax": "Max Pax",
263
+ "slotInterval": "Slot Interval",
264
+ "slotIntervalDescription": "Time between each available booking slot (e.g., 30 mins)",
265
+ "bookingDuration": "Booking Duration",
266
+ "bookingDurationDescription": "Duration of each reservation (e.g., 60 mins)",
267
+ "minLeadDays": "Min Lead Days",
268
+ "minLeadDaysDescription": "Minimum days in advance customers must book (0 = same day)",
269
+ "maxLeadDays": "Max Lead Days",
270
+ "maxLeadDaysDescription": "Maximum days in advance customers can book",
271
+ "operatingWindows": "Operating Windows",
272
+ "addWindow": "Add Window",
273
+ "startTime": "Start Time",
274
+ "endTime": "End Time",
275
+ "customPresetRemarks": "Custom Preset Remarks",
276
+ "addRemark": "Add Remark",
277
+ "noRangesConfigured": "No reservation ranges configured. Click 'Add Range' to get started.",
278
+ "addFirstRange": "Add your first reservation range to get started",
279
+ "globalSettings": "Global Settings",
280
+ "paxRange": "Pax Range",
281
+ "noOperatingHours": "No operating hours",
282
+ "saveAllChanges": "Save All Changes",
283
+ "confirmDeleteRange": "Are you sure you want to delete {name}?",
284
+ "settingUpdated": "Settings updated successfully",
285
+ "newReservationRange": "New Reservation Range",
286
+ "basicInformation": "Basic Information",
287
+ "guestRequirements": "Guest Requirements",
288
+ "bookingConfiguration": "Booking Configuration",
289
+ "rangeNamePlaceholder": "e.g., VIP Room, Main Hall",
290
+ "presetRemarkPlaceholder": "e.g., Window seat, Quiet area",
291
+ "saveRange": "Save Range",
292
+ "draftHoldTimeMinutes": "Draft Hold Time (Minutes)",
293
+ "draftHoldTimeDescription": "How long to hold a draft reservation before it expires",
294
+ "posCanOverbook": "POS Can Overbook",
295
+ "posCanOverbookDescription": "Allow POS to create reservations even when capacity is full",
296
+ "notificationSettings": "Notification Settings",
297
+ "smsEnabled": "Enable SMS Notifications",
298
+ "smsEnabledDescription": "Send SMS notifications to customers for reservation updates",
299
+ "emailEnabled": "Enable Email Notifications",
300
+ "emailEnabledDescription": "Send email notifications to customers for reservation updates",
301
+ "notifyOnConfirm": "Notify on Confirmation",
302
+ "notifyOnConfirmDescription": "Send notifications when a reservation is confirmed",
303
+ "notifyOnCancel": "Notify on Cancellation",
304
+ "notifyOnCancelDescription": "Send notifications when a reservation is cancelled",
249
305
  "qrPay": "QR Pay",
250
306
  "terminalScanPay": "Terminal Scan Pay",
251
307
  "terminalQrPay": "Terminal QR Pay",
@@ -243,6 +243,60 @@
243
243
  "current": "ปัจจุบัน",
244
244
  "paxMin": "จำนวนผู้โดยสารขั้นต่ำ",
245
245
  "paxMax": "จำนวนผู้โดยสารสูงสุด",
246
+ "reservation": "การจอง",
247
+ "reservationSettings": "การตั้งค่าการจอง",
248
+ "reservationRanges": "ช่วงการจอง",
249
+ "addRange": "เพิ่มช่วง",
250
+ "range": "ช่วง",
251
+ "rangeName": "ชื่อช่วง",
252
+ "priority": "ลำดับความสำคัญ",
253
+ "priorityDescription": "ตัวเลขที่สูงกว่า = ลำดับความสำคัญสูงกว่าเมื่อจับคู่ช่วงเวลา",
254
+ "capacity": "ความจุ",
255
+ "capacityDescription": "จำนวนการจองพร้อมกันสูงสุดสำหรับช่วงนี้",
256
+ "minPax": "จำนวนผู้โดยสารขั้นต่ำ",
257
+ "maxPax": "จำนวนผู้โดยสารสูงสุด",
258
+ "slotInterval": "ช่วงเวลา",
259
+ "slotIntervalDescription": "เวลาระหว่างช่วงการจองที่ว่าง (เช่น 30 นาที)",
260
+ "bookingDuration": "ระยะเวลาการจอง",
261
+ "bookingDurationDescription": "ระยะเวลาของการจองแต่ละครั้ง (เช่น 60 นาที)",
262
+ "minLeadDays": "จำนวนวันล่วงหน้าขั้นต่ำ",
263
+ "minLeadDaysDescription": "จำนวนวันขั้นต่ำที่ลูกค้าต้องจองล่วงหน้า (0 = วันเดียวกัน)",
264
+ "maxLeadDays": "จำนวนวันล่วงหน้าสูงสุด",
265
+ "maxLeadDaysDescription": "จำนวนวันสูงสุดที่ลูกค้าสามารถจองล่วงหน้าได้",
266
+ "operatingWindows": "ช่วงเวลาทำการ",
267
+ "addWindow": "เพิ่มช่วงเวลา",
268
+ "startTime": "เวลาเริ่มต้น",
269
+ "endTime": "เวลาสิ้นสุด",
270
+ "customPresetRemarks": "หมายเหตุที่กำหนดไว้ล่วงหน้า",
271
+ "addRemark": "เพิ่มหมายเหตุ",
272
+ "noRangesConfigured": "ยังไม่มีการกำหนดช่วงการจอง คลิก 'เพิ่มช่วง' เพื่อเริ่มต้น",
273
+ "addFirstRange": "เพิ่มช่วงการจองแรกของคุณเพื่อเริ่มต้น",
274
+ "globalSettings": "การตั้งค่าทั่วไป",
275
+ "paxRange": "ช่วงจำนวนผู้โดยสาร",
276
+ "noOperatingHours": "ไม่มีเวลาทำการ",
277
+ "saveAllChanges": "บันทึกการเปลี่ยนแปลงทั้งหมด",
278
+ "confirmDeleteRange": "คุณแน่ใจหรือไม่ว่าต้องการลบ {name}?",
279
+ "settingUpdated": "อัปเดตการตั้งค่าสำเร็จ",
280
+ "newReservationRange": "ช่วงการจองใหม่",
281
+ "basicInformation": "ข้อมูลพื้นฐาน",
282
+ "guestRequirements": "ความต้องการของแขก",
283
+ "bookingConfiguration": "การกำหนดค่าการจอง",
284
+ "rangeNamePlaceholder": "เช่น ห้อง VIP, ห้องโถง",
285
+ "presetRemarkPlaceholder": "เช่น ที่นั่งริมหน้าต่าง, พื้นที่เงียบ",
286
+ "saveRange": "บันทึกช่วง",
287
+ "draftHoldTimeMinutes": "เวลาเก็บร่าง (นาที)",
288
+ "draftHoldTimeDescription": "ระยะเวลาในการเก็บการจองแบบร่างก่อนที่จะหมดอายุ",
289
+ "posCanOverbook": "POS สามารถจองเกินได้",
290
+ "posCanOverbookDescription": "อนุญาตให้ POS สร้างการจองแม้ว่าความจุเต็มแล้ว",
291
+ "notificationSettings": "การตั้งค่าการแจ้งเตือน",
292
+ "smsEnabled": "เปิดใช้งานการแจ้งเตือนผ่าน SMS",
293
+ "smsEnabledDescription": "ส่งการแจ้งเตือนผ่าน SMS ให้กับลูกค้าสำหรับการอัพเดทการจอง",
294
+ "emailEnabled": "เปิดใช้งานการแจ้งเตือนผ่านอีเมล",
295
+ "emailEnabledDescription": "ส่งการแจ้งเตือนผ่านอีเมลให้กับลูกค้าสำหรับการอัพเดทการจอง",
296
+ "notifyOnConfirm": "แจ้งเตือนเมื่อยืนยัน",
297
+ "notifyOnConfirmDescription": "ส่งการแจ้งเตือนเมื่อการจองได้รับการยืนยัน",
298
+ "notifyOnCancel": "แจ้งเตือนเมื่อยกเลิก",
299
+ "notifyOnCancelDescription": "ส่งการแจ้งเตือนเมื่อการจองถูกยกเลิก",
246
300
  "qrPay": "ชำระเงินด้วย QR",
247
301
  "terminalScanPay": "ชำระเงินด้วยการสแกนเทอร์มินัล",
248
302
  "terminalQrPay": "ชำระเงินด้วย QR เทอร์มินัล",
@@ -247,6 +247,60 @@
247
247
  "current": "当前",
248
248
  "paxMin": "最少人数",
249
249
  "paxMax": "最多人数",
250
+ "reservation": "预订",
251
+ "reservationSettings": "预订设置",
252
+ "reservationRanges": "预订范围",
253
+ "addRange": "添加范围",
254
+ "range": "范围",
255
+ "rangeName": "范围名称",
256
+ "priority": "优先级",
257
+ "priorityDescription": "数字越大,匹配时段时优先级越高",
258
+ "capacity": "容量",
259
+ "capacityDescription": "此范围的最大并发预订数",
260
+ "minPax": "最少人数",
261
+ "maxPax": "最多人数",
262
+ "slotInterval": "时段间隔",
263
+ "slotIntervalDescription": "每个可预订时段之间的时间(例如30分钟)",
264
+ "bookingDuration": "预订时长",
265
+ "bookingDurationDescription": "每次预订的持续时间(例如60分钟)",
266
+ "minLeadDays": "最少提前天数",
267
+ "minLeadDaysDescription": "顾客必须提前预订的最少天数(0 = 当天)",
268
+ "maxLeadDays": "最多提前天数",
269
+ "maxLeadDaysDescription": "顾客可以提前预订的最多天数",
270
+ "operatingWindows": "营业时段",
271
+ "addWindow": "添加时段",
272
+ "startTime": "开始时间",
273
+ "endTime": "结束时间",
274
+ "customPresetRemarks": "自定义预设备注",
275
+ "addRemark": "添加备注",
276
+ "noRangesConfigured": "未配置预订范围。点击\"添加范围\"开始",
277
+ "addFirstRange": "添加您的第一个预订范围以开始",
278
+ "globalSettings": "全局设置",
279
+ "paxRange": "人数范围",
280
+ "noOperatingHours": "无营业时间",
281
+ "saveAllChanges": "保存所有更改",
282
+ "confirmDeleteRange": "您确定要删除{name}吗?",
283
+ "settingUpdated": "设置已成功更新",
284
+ "newReservationRange": "新预订范围",
285
+ "basicInformation": "基本信息",
286
+ "guestRequirements": "客人要求",
287
+ "bookingConfiguration": "预订配置",
288
+ "rangeNamePlaceholder": "例如:VIP包厢、大厅",
289
+ "presetRemarkPlaceholder": "例如:靠窗座位、安静区域",
290
+ "saveRange": "保存范围",
291
+ "draftHoldTimeMinutes": "草稿保留时间(分钟)",
292
+ "draftHoldTimeDescription": "在草稿预订过期前保留的时间",
293
+ "posCanOverbook": "POS可以超额预订",
294
+ "posCanOverbookDescription": "允许POS在容量已满时创建预订",
295
+ "notificationSettings": "通知设置",
296
+ "smsEnabled": "启用短信通知",
297
+ "smsEnabledDescription": "向客户发送预订更新的短信通知",
298
+ "emailEnabled": "启用电子邮件通知",
299
+ "emailEnabledDescription": "向客户发送预订更新的电子邮件通知",
300
+ "notifyOnConfirm": "确认时通知",
301
+ "notifyOnConfirmDescription": "当预订被确认时发送通知",
302
+ "notifyOnCancel": "取消时通知",
303
+ "notifyOnCancelDescription": "当预订被取消时发送通知",
250
304
  "qrPay": "二维码支付",
251
305
  "terminalScanPay": "终端扫描支付",
252
306
  "terminalQrPay": "终端二维码支付",
@@ -156,6 +156,7 @@
156
156
  v-if="!getCoverImagePreview('coverImageLandscape')"
157
157
  accept="image/*"
158
158
  :max-file-size="MAX_IMAGE_FILE_SIZE"
159
+ content-class="w-full max-w-[400px] aspect-video"
159
160
  @file-upload="uploadCoverImage('coverImageLandscape', $event)"
160
161
  @file-rejected="handleCoverImageRejected('coverImageLandscape', $event)"
161
162
  />
@@ -163,13 +164,13 @@
163
164
  <img
164
165
  :src="getCoverImagePreview('coverImageLandscape')!"
165
166
  :alt="t('order.coverImageLandscape')"
166
- class="w-full max-w-[360px] max-h-[200px] object-cover rounded-md border border-fm-color-neutral-gray-200"
167
- />
168
- <FmButton
169
- variant="tertiary"
170
- class="mr-auto"
171
- :label="t('common.delete')"
172
- @click="clearCoverImage('coverImageLandscape')"
167
+ class="max-w-[400px] aspect-video object-cover rounded-md border border-fm-color-neutral-gray-200"
168
+ />
169
+ <FmButton
170
+ variant="tertiary"
171
+ class="mr-auto"
172
+ :label="t('common.delete')"
173
+ @click="clearCoverImage('coverImageLandscape')"
173
174
  />
174
175
  </div>
175
176
  </div>
@@ -183,6 +184,7 @@
183
184
  v-if="!getCoverImagePreview('coverImagePortrait')"
184
185
  accept="image/*"
185
186
  :max-file-size="MAX_IMAGE_FILE_SIZE"
187
+ content-class="w-[240px] aspect-[9/16]"
186
188
  @file-upload="uploadCoverImage('coverImagePortrait', $event)"
187
189
  @file-rejected="handleCoverImageRejected('coverImagePortrait', $event)"
188
190
  />
@@ -190,13 +192,13 @@
190
192
  <img
191
193
  :src="getCoverImagePreview('coverImagePortrait')!"
192
194
  :alt="t('order.coverImagePortrait')"
193
- class="w-full max-w-[360px] max-h-[200px] object-cover rounded-md border border-fm-color-neutral-gray-200"
194
- />
195
- <FmButton
196
- variant="tertiary"
197
- class="mr-auto"
198
- :label="t('common.delete')"
199
- @click="clearCoverImage('coverImagePortrait')"
195
+ class="max-w-[200px] aspect-[9/16] object-cover rounded-md border border-fm-color-neutral-gray-200"
196
+ />
197
+ <FmButton
198
+ variant="tertiary"
199
+ class="mr-auto"
200
+ :label="t('common.delete')"
201
+ @click="clearCoverImage('coverImagePortrait')"
200
202
  />
201
203
  </div>
202
204
  </div>
@@ -27,6 +27,7 @@ import SmsSetting from './sms/SmsSetting.vue'
27
27
 
28
28
  import GeneralSetting from './general/GeneralSetting.vue'
29
29
  import QueueSetting from './queue/QueueSetting.vue'
30
+ import ReservationSetting from './reservation/ReservationSetting.vue'
30
31
  import { useI18n, useCoreStore } from '@feedmepos/mf-common'
31
32
  import { useMenuStore } from '@/stores/menu/menu'
32
33
  import { useSnackbarFunctions } from '@/components/snackbar'
@@ -45,6 +46,7 @@ type OrderSettingMenuItemValue =
45
46
  // | 'discountRule'
46
47
  | 'general'
47
48
  | 'queue'
49
+ | 'reservation'
48
50
  const selectedOrderSetting = ref<OrderSettingMenuItemValue>('delivery')
49
51
 
50
52
  const settingItem = computed<FmTabProps[]>(() => [
@@ -56,7 +58,8 @@ const settingItem = computed<FmTabProps[]>(() => [
56
58
  // { label: t('order.discountRule.title'), value: 'discountRule' },
57
59
  { label: t('order.sms'), value: 'sms' },
58
60
  { label: t('order.general'), value: 'general' },
59
- { label: t('order.queue'), value: 'queue' }
61
+ { label: t('order.queue'), value: 'queue' },
62
+ { label: t('order.reservation'), value: 'reservation' }
60
63
  ])
61
64
 
62
65
  const currentComponent = computed(() => {
@@ -79,6 +82,8 @@ const currentComponent = computed(() => {
79
82
  return GeneralSetting
80
83
  case 'queue':
81
84
  return QueueSetting
85
+ case 'reservation':
86
+ return ReservationSetting
82
87
  }
83
88
  })
84
89
 
@@ -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,99 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { components } from '@feedmepos/ui-library';
4
+
5
+ interface SelectItem {
6
+ label: string
7
+ value: string
8
+ icon?: 'radio' | 'checkbox'
9
+ }
10
+
11
+ const props = defineProps<{
12
+ modelValue: string
13
+ items: SelectItem[]
14
+ }>()
15
+
16
+ const emit = defineEmits<{
17
+ (e: 'update:modelValue', value: string): void
18
+ }>()
19
+
20
+ const isOpen = ref(false)
21
+ const selectRef = ref<HTMLDivElement | null>(null)
22
+ const menuRef = ref<typeof components['FmMenu'] | null>(null);
23
+
24
+ const selectedItem = computed(() => {
25
+ const found = props.items.find((item) => item.value === props.modelValue)
26
+ return found || props.items[0] || { label: 'Select...', value: '', icon: undefined }
27
+ })
28
+
29
+ function selectItem(item: SelectItem) {
30
+ emit('update:modelValue', item.value)
31
+ menuRef.value?.toggleMenu();
32
+ }
33
+
34
+ // Close dropdown when clicking outside
35
+ function handleClickOutside(event: MouseEvent) {
36
+ if (selectRef.value && !selectRef.value.contains(event.target as Node)) {
37
+ menuRef.value?.toggleMenu();
38
+ }
39
+ }
40
+
41
+ // Setup click outside listener when component mounts/unmounts
42
+ import { onMounted, onUnmounted } from 'vue'
43
+
44
+ onMounted(() => {
45
+ document.addEventListener('click', handleClickOutside)
46
+ })
47
+
48
+ onUnmounted(() => {
49
+ document.removeEventListener('click', handleClickOutside)
50
+ })
51
+ </script>
52
+
53
+ <template>
54
+ <div class="relative w-full" ref="selectRef">
55
+ <!-- Use FmMenu for dropdown behavior -->
56
+ <FmMenu ref="menuRef" @menu-changed="(v) => isOpen = v">
57
+ <template #menu-button>
58
+ <FmField>
59
+ <div class="flex items-center gap-4 w-full">
60
+ <FmCheckbox v-if="selectedItem.icon == 'checkbox'" :value="true" :model-value="true"
61
+ @update:model-value="(v) => v" />
62
+ <div v-if="selectedItem.icon == 'radio'" class="flex items-center">
63
+ <FmRadio :value="true" :model-value="true" @update:model-value="(v) => v" />
64
+ </div>
65
+ <!-- Label -->
66
+ <span class="text-gray-900 flex-1">
67
+ {{ selectedItem.label }}
68
+ </span>
69
+ <FmIcon name="keyboard_arrow_down" class="text-gray-500 transition-transform duration-200 ml-2"
70
+ :class="{ 'rotate-180': isOpen }" />
71
+ </div>
72
+ </FmField>
73
+ </template>
74
+
75
+ <!-- Dropdown Menu Items -->
76
+ <template #default>
77
+ <div :style="{ width: `${selectRef?.clientWidth}px` }" class="rounded-md flex flex-col gap-4">
78
+ <div v-for="item in items" :key="item.value" @click="selectItem(item)"
79
+ class="cursor-pointer transition-colors duration-150 flex items-center p-8 rounded-sm hover:bg-orange-100">
80
+ <FmCheckbox v-if="item.icon == 'checkbox'" :value="true" :model-value="true" @update:model-value="(v) => v"
81
+ class="mr-8" />
82
+ <div v-if="item.icon == 'radio'" class="flex items-center">
83
+ <FmRadio :value="true" :model-value="true" @update:model-value="(v) => v" />
84
+ </div>
85
+ <span class="text-gray-900 flex-1"> {{ item.label }} </span>
86
+ </div>
87
+ </div>
88
+ </template>
89
+ </FmMenu>
90
+ </div>
91
+ </template>
92
+
93
+ <style scoped>
94
+ /* Match FmTextField styling */
95
+ .fm-text-field-input {
96
+ font-size: 1.125rem;
97
+ line-height: 1.75rem;
98
+ }
99
+ </style>