Files
mgm-daylight-mcrm-cms/src/views/daylightAppSetting/room/Modal.vue
T
2026-10-26 09:29:53 +08:00

851 lines
21 KiB
Vue

<template>
<div
class="modal fade"
id="will_modal_users"
ref="modalRef"
tabindex="-1"
aria-hidden="true"
>
<!--begin::Modal dialog-->
<div class="modal-dialog modal-dialog-centered mw-650px">
<!--begin::Modal content-->
<div class="modal-content">
<!--begin::Modal header-->
<div class="modal-header" id="will_modal_users_header">
<!--begin::Modal title-->
<h2 class="fw-bold">{{ operoomTitle }}</h2>
<!--end::Modal title-->
<!--begin::Close-->
<div
id="will_modal_users_close"
data-bs-dismiss="modal"
class="btn btn-icon btn-sm btn-active-icon-primary"
>
<span class="svg-icon svg-icon-1">
<inline-svg
:src="
getAssetPath(
'media/icons/duotune/arrows/arr061.svg'
)
"
/>
</span>
</div>
<!--end::Close-->
</div>
<!--end::Modal header-->
<!--begin::Form-->
<el-form
@submit.prevent="submit()"
:model="data"
:rules="rules"
ref="formRef"
>
<!--begin::Modal body-->
<div class="modal-body py-10 px-lg-17">
<!--begin::Scroll-->
<div
class="scroll-y me-n7 pe-7"
id="will_modal_users_scroll"
data-kt-scroll="true"
data-kt-scroll-activate="{default: false, lg: true}"
data-kt-scroll-max-height="auto"
data-kt-scroll-dependencies="#will_modal_users_header"
data-kt-scroll-wrappers="#will_modal_users_scroll"
data-kt-scroll-offset="300px"
>
<!--begin::Input group-->
<div class="fv-row mb-7">
<label class="required fs-6 fw-semobold mb-2"
>Property</label
>
<el-form-item prop="propertyId">
<el-select
v-model="data.propertyId"
placeholder="Select"
:disabled="!enableEdit"
@change="handlePropertyChange"
>
<el-option
v-for="item in propertyList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
<div class="fv-row mb-7">
<label class="required fs-6 fw-semobold mb-2"
>Room Category</label
>
<el-form-item prop="roomCategory">
<el-select
v-model="data.roomCategory"
placeholder="Select"
:disabled="!enableEdit"
filterable
@change=""
>
<el-option
key="Standard Room"
label="Standard Room"
value="Standard Room"
/>
<el-option
key="Suite"
label="Suite"
value="Suite"
/>
</el-select>
</el-form-item>
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="required fs-6 fw-semobold mb-2"
>Room</label
>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="name">
<el-input
v-model="data.name"
type="text"
placeholder=""
:readonly="!enableEdit"
/>
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="required fs-6 fw-semobold mb-2"
>Room(Chinese)</label
>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="nameCN">
<el-input
v-model="data.nameCN"
type="text"
placeholder=""
:readonly="!enableEdit"
/>
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="required fs-6 fw-semobold mb-2"
>Email Name</label
>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="emailName">
<el-input
v-model="data.emailName"
type="text"
placeholder=""
:readonly="!enableEdit"
/>
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="fs-6 fw-semobold mb-2">Calendar Name</label>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="calendarName">
<el-input v-model="data.calendarName" type="text" placeholder="" :readonly="!enableEdit" />
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="fs-6 fw-semobold mb-2">Calendar Order</label>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="calendarOrder">
<el-input-number v-model="data.calendarOrder" :min="0" :disabled="!enableEdit" />
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="fs-6 fw-semobold mb-2">Show Calendar</label>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="showCalendar">
<el-select v-model="data.showCalendar" placeholder="Select" :disabled="!enableEdit" :teleported="false">
<el-option :key="1" label="Yes" :value="1" />
<el-option :key="0" label="No" :value="0" />
</el-select>
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="fs-6 fw-semobold mb-2">Default Show Calendar</label>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="defaultShowCalendar">
<el-select v-model="data.defaultShowCalendar" placeholder="Select" :disabled="!enableEdit" :teleported="false">
<el-option :key="1" label="Yes" :value="1" />
<el-option :key="0" label="No" :value="0" />
</el-select>
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="required fs-6 fw-semobold mb-2"
>Sharer</label
>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="sharerLimit">
<div
class="input-group bootstrap-touchspin bootstrap-touchspin-injected"
>
<el-input-number
v-model="data.sharerLimit"
:min="1"
:readonly="!enableEdit"
/>
</div>
</el-form-item>
<!--end::Input-->
</div>
<div class="fv-row mb-7">
<!--begin::Label-->
<label class="required fs-6 fw-semobold mb-2"
>Room Type</label
>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="roomTypeIds">
<el-select
v-model="data.roomTypeIds"
multiple
placeholder="Select"
:disabled="!enableEdit"
:teleported="false"
>
<el-option
v-for="item in roomTypeOptionsByProperty"
:key="item.id"
:label="item.code"
:value="item.id"
/>
</el-select>
</el-form-item>
<!--end::Input-->
</div>
<div class="row mb-7">
<div class="col-md-6 fv-row">
<label class="fs-5 fw-semobold mb-2"
>For Create Booking</label
>
<el-select
v-model="data.forCreateBooking"
placeholder="Select"
:disabled="!enableEdit"
:teleported="false"
placement="top"
:fallback-placements="['top']"
:popper-options="selectPopperOptions"
>
<el-option
:key="0"
label="No"
:value="0"
/>
<el-option
:key="1"
label="Yes"
:value="1"
/>
</el-select>
</div>
<div class="col-md-6 fv-row">
<label class="fs-5 fw-semobold mb-2"
>For Create Waitlist</label
>
<el-select
v-model="data.forCreateWaitlist"
placeholder="Select"
:disabled="!enableEdit"
:teleported="false"
placement="top"
:fallback-placements="['top']"
:popper-options="selectPopperOptions"
>
<el-option
:key="0"
label="No"
:value="0"
/>
<el-option
:key="1"
label="Yes"
:value="1"
/>
</el-select>
</div>
</div>
<div class="row g-9 mb-6" v-if="disabled">
<div class="col-md-6 fv-row">
<!--begin::Label-->
<label class="fs-6 fw-semobold mb-2"
>Last Updated At</label
>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="updatedAt">
<el-input
:value="formatTime(data.updatedAt)"
type="text"
placeholder=""
:disabled="disabled"
/>
</el-form-item>
<!--end::Input-->
</div>
<div class="col-md-6 fv-row">
<!--begin::Label-->
<label class="fs-6 fw-semobold mb-2"
>Created At</label
>
<!--end::Label-->
<!--begin::Input-->
<el-form-item prop="updatedAt">
<el-input
:value="formatTime(data.createdAt)"
type="text"
placeholder=""
:disabled="disabled"
/>
</el-form-item>
<!--end::Input-->
</div>
</div>
<!--end::Input group-->
<!--end::Billing form-->
</div>
<!--end::Scroll-->
</div>
<!--end::Modal body-->
<!--begin::Modal footer-->
<div class="modal-footer flex-center">
<!--begin::Button-->
<button
type="button"
data-bs-dismiss="modal"
class="btn btn-light me-3"
>
Close
</button>
<!--end::Button-->
<!--begin::Button-->
<button
:disabled="loading"
:data-kt-indicator="loading ? 'on' : null"
class="btn btn-lg btn-primary"
type="submit"
v-if="enableEdit"
>
<span v-if="!loading" class="indicator-label">
Save
<span class="svg-icon svg-icon-3 ms-2 me-0">
<inline-svg
:src="
getAssetPath(
'media/icons/duotune/arrows/arr064.svg'
)
"
/>
</span>
</span>
<span v-if="loading" class="indicator-progress">
Please wait...
<span
class="spinner-border spinner-border-sm align-middle ms-2"
></span>
</span>
</button>
<!--end::Button-->
</div>
<!--end::Modal footer-->
</el-form>
<!--end::Form-->
</div>
</div>
</div>
</template>
<script lang="ts">
import { getAssetPath } from "@/core/helpers/assets";
import { defineComponent, ref, computed } from "vue";
import { hideModal, showModal } from "@/core/helpers/dom";
import ApiService from "@/core/services/ApiService";
import type { ResponseData } from "@/core/data/AjaxData";
import Swal from "sweetalert2";
import { ElNotification } from "element-plus";
import moment from "moment";
export default defineComponent({
name: "user-modal-editor",
components: {},
setup() {
const roomTypeData = ref(new Map());
const roomTypeSelectOptions = computed(() =>
[...roomTypeData.value.values()].flat()
);
const formRef = ref<null | HTMLFormElement>(null);
const modalRef = ref<null | HTMLElement>(null);
const loading = ref<boolean>(false);
const operateType = ref("CREATE");
const propertyList = ref([]);
const actionCallback = ref(null);
const operoomTitle = computed(() => {
let titleLabel = "Create Room";
switch (operateType.value) {
case "CREATE":
titleLabel = "Create Room";
break;
case "VIEW":
titleLabel = "View Room";
break;
case "EDIT":
titleLabel = "Edit Room";
break;
case "DELETE":
titleLabel = "Remove Room";
break;
}
return titleLabel;
});
const getModalOption = () => {
let opt =
"CREATE" === operateType.value ||
"EDIT" === operateType.value ||
"COPY" === operateType.value
? { backdrop: "static" }
: {};
return opt;
};
const enableEdit = computed(() => {
let isEditable = false;
if (
operateType.value === "CREATE" ||
operateType.value === "EDIT"
) {
isEditable = true;
}
return isEditable;
});
const disabled = computed(() => {
let isEditable = false;
if (
operateType.value !== "CREATE" &&
operateType.value !== "COPY"
) {
isEditable = true;
}
return isEditable;
});
const formatTime = (pVal) => {
return pVal ? moment(pVal).format("YYYY-MM-DD HH:mm:ss") : "";
};
const data = ref({
id: null,
name: "",
propertyId: null,
sharerLimit: 1,
calendarOrder: 0
});
const selectPopperOptions = {
modifiers: [
{ name: "flip", enabled: false },
{ name: "preventOverflow", enabled: false },
],
};
const roomTypeOptionsByProperty = computed(() => {
const pid = data.value.propertyId;
if (pid == null) return [];
return roomTypeData.value.get(String(pid)) ?? [];
});
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
};
const normalizeRoomTypeOption = (item) => {
const optionId = item?.roomTypeId ?? item?.id;
if (optionId == null) return null;
return {
...item,
id: optionId,
propertyId: item?.propertyId ?? data.value.propertyId,
code:
item?.code ??
item?.roomTypeCode ??
item?.roomType?.code ??
item?.name ??
String(optionId),
};
};
const mergeRoomTypesToMap = (propertyId, roomTypes) => {
if (propertyId == null || !Array.isArray(roomTypes) || roomTypes.length === 0) {
return;
}
const key = String(propertyId);
const currentMap = roomTypeData.value;
const existingList = currentMap.get(key) ?? [];
const seen = new Set(existingList.map((item) => String(item.id)));
const merged = [...existingList];
roomTypes.forEach((item) => {
const normalized = normalizeRoomTypeOption(item);
if (!normalized) return;
const rid = String(normalized.id);
if (!seen.has(rid)) {
merged.push(normalized);
seen.add(rid);
}
});
currentMap.set(key, merged);
// 重新赋值,确保依赖该 Map 的 computed 触发更新
roomTypeData.value = new Map(currentMap);
};
const fetchRoomType = () => {
let reqParas = {
category: 'DAYLIGHT'
};
ApiService.processPostRequest(
"/app/room_type/list",
reqParas,
(resData: ResponseData) => {
const list = resData.data || [];
const map = new Map();
list.forEach((item) => {
const normalized = normalizeRoomTypeOption(item);
if (!normalized) return;
const pid = normalized.propertyId;
if (pid == null) return;
const key = String(pid);
const existing = map.get(key) ?? [];
existing.push(normalized);
map.set(key, existing);
});
roomTypeData.value = map;
},
(resData: ResponseData) => {},
(error) => {}
);
};
fetchRoomType();
const openCreate = (pPropertyId: number, fCallback) => {
resetForm();
operateType.value = "CREATE";
data.value = {
id: null,
name: "",
propertyId: null,
roomCategory: "",
forCreateBooking: 1,
forCreateWaitlist: 1,
sharerLimit: 1,
category: "DAYLIGHT"
};
actionCallback.value = fCallback;
showModal(modalRef.value, getModalOption());
};
const openView = (id: number) => {
operateType.value = "VIEW";
actionCallback.value = null;
resetForm();
fetchInfo(id, operateType.value);
};
const openEdit = (id: number, fCallback) => {
operateType.value = "EDIT";
actionCallback.value = fCallback;
resetForm();
fetchInfo(id, operateType.value);
};
const openDelete = (id: number, fCallback) => {
operateType.value = "DELETE";
Swal.fire({
text: "Are you sure to remove this room!",
icon: "warning",
showCancelButton: true,
buttonsStyling: false,
confirmButtonText: "Yes, remove it!",
cancelButtonText: "Cancel",
heightAuto: false,
customClass: {
confirmButton: "btn btn-primary",
cancelButton: "btn btn-light",
},
}).then((isConfirm) => {
if (isConfirm.value) {
actionCallback.value = fCallback;
deleteById(id);
}
});
};
const fetchPropertyList = () => {
propertyList.value = [];
let reqParas = {
category: "DAYLIGHT",
};
ApiService.processPostRequest(
"/app/property/list",
reqParas,
(resData: ResponseData) => {
propertyList.value = resData.data;
},
null,
null
);
};
fetchPropertyList();
const saveOrUpdateInfo = (dataObj: object) => {
if (dataObj) {
let postUrl =
dataObj.id == null
? "/app/room/create"
: "/app/room/update";
ApiService.processPostRequest(
postUrl,
dataObj,
(resData: ResponseData) => {
loading.value = false;
ElNotification({
position: "bottom-right",
title: "Success",
message: "Room has been successfully saved!",
type: "success",
});
hideModal(modalRef.value);
actionCallback.value && actionCallback.value();
},
(resData: ResponseData) => {
loading.value = false;
ElNotification({
position: "bottom-right",
title: "Fail",
message:
"Save data failed![" + resData.message + "]",
type: "error",
});
},
(error) => {
loading.value = false;
Swal.fire({
text: "Save data error![" + error + "]",
icon: "error",
buttonsStyling: false,
confirmButtonText: "Ok, got it!",
heightAuto: false,
customClass: {
confirmButton: "btn btn-primary",
},
});
}
);
} else {
ElNotification({
position: "bottom-right",
title: "Fail",
message: "Empty room info",
type: "error",
});
}
};
const deleteById = (id: number) => {
if (id > 0) {
let userObj = {
id: id,
};
let postUrl = "/app/room/delete";
ApiService.processPostRequest(
postUrl,
userObj,
(resData: ResponseData) => {
loading.value = false;
ElNotification({
position: "bottom-right",
title: "Success",
message: "Room has been successfully deleted!",
type: "success",
});
actionCallback.value && actionCallback.value();
},
(resData: ResponseData) => {
ElNotification({
position: "bottom-right",
title: "Fail",
message:
"Delete room failed![" + resData.message + "]",
type: "error",
});
},
(error) => {
Swal.fire({
text: "Delete room error![" + error + "]",
icon: "error",
buttonsStyling: false,
confirmButtonText: "Ok, got it!",
heightAuto: false,
customClass: {
confirmButton: "btn btn-primary",
},
});
}
);
}
};
const fetchInfo = (id: number, operoom: string) => {
let reqParas = {
searchId: id,
};
ApiService.processPostRequest(
"/app/room/get",
reqParas,
(resData: ResponseData) => {
let rdata = resData.data;
let roomTypeIds = [];
if (rdata.roomRoomType != null) {
mergeRoomTypesToMap(rdata.propertyId, rdata.roomRoomType);
for (let i = 0; i < rdata.roomRoomType.length; i++) {
const rtid =
rdata.roomRoomType[i].roomTypeId ??
rdata.roomRoomType[i].id;
if (rtid != null) roomTypeIds.push(rtid);
}
rdata.roomTypeIds = roomTypeIds;
}
data.value = rdata;
showModal(modalRef.value, getModalOption());
},
(resData: ResponseData) => {
ElNotification({
position: "bottom-right",
title: "Fail",
message:
"Read room data error![" + resData.message + "]",
type: "error",
});
hideModal(modalRef.value);
},
(error) => {}
);
};
const rules = ref({
propertyId: [
{
required: true,
message: "Property is required",
trigger: "change",
},
],
roomCategory: [
{
required: true,
message: "Room category is required",
trigger: "change",
},
],
name: [
{
required: true,
message: "Room name is required",
trigger: "change",
},
],
nameCN: [
{
required: true,
message: "Room name in Chinese is required",
trigger: "change",
},
],
sharerLimit: [
{
required: true,
message: "Sharer is required",
trigger: "change",
},
],
roomTypeIds: [
{
required: true,
message: "Room Type is required",
trigger: "change",
},
],
});
const submit = () => {
if (!formRef.value) {
return;
}
if (loading.value) return;
formRef.value.validate((valid: boolean) => {
if (valid) {
loading.value = true;
saveOrUpdateInfo(data.value);
} else {
return false;
}
});
};
const handlePropertyChange = (selectId) => {
// 切换 property 后,避免保留其他 property 的 room type 选择
data.value.roomTypeIds = [];
};
return {
data,
selectPopperOptions,
rules,
submit,
formRef,
loading,
modalRef,
getAssetPath,
openCreate,
openView,
openEdit,
openDelete,
operateType,
operoomTitle,
getModalOption,
enableEdit,
disabled,
roomTypeData,
roomTypeSelectOptions,
roomTypeOptionsByProperty,
propertyList,
formatTime,
handlePropertyChange,
};
},
});
</script>