472 lines
14 KiB
Vue
472 lines
14 KiB
Vue
<template>
|
|
<div class="container is-fluid px-0" style="overflow: hidden">
|
|
<!-- 95px is height of nav + breadcrumb -->
|
|
<div class="columns m-0" style="height: calc(100vh - 95px)">
|
|
<!-- Form Section -->
|
|
<div class="column is-5 pb-5" style="
|
|
overflow-y: scroll;
|
|
border-right: 1px solid #dbdbdb;
|
|
">
|
|
<label class="label">
|
|
<span class="icon-text">
|
|
<span>Chọn mẫu</span>
|
|
</span>
|
|
</label>
|
|
<div class="level mb-4 mt-2 pr-4">
|
|
<!-- Template Selector -->
|
|
|
|
<div class="level-left" style="flex: 1">
|
|
<div class="level-item" style="flex: 1">
|
|
<div class="field" style="width: 100%">
|
|
<div class="control">
|
|
<div class="select is-fullwidth">
|
|
<select v-model="selectedValue" @change="handleTemplateChange">
|
|
<option value="defaultTemplate">Mặc định</option>
|
|
<option
|
|
v-for="template in dataTemplate"
|
|
:key="template.id"
|
|
:value="template.name"
|
|
>
|
|
{{ template.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div v-if="$getEditRights()" class="level-right">
|
|
<div class="level-item">
|
|
<div class="buttons">
|
|
<button class="button is-light" @click="handleOpenModal">
|
|
<span class="icon">
|
|
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 18 }"></SvgIcon>
|
|
</span>
|
|
</button>
|
|
<button
|
|
v-if="selectedTemplateId"
|
|
class="button is-danger is-outlined"
|
|
@click="showDeleteDialog = true"
|
|
:disabled="loading"
|
|
>
|
|
<span class="icon">
|
|
<SvgIcon v-bind="{ name: 'trash.svg', type: 'white', size: 18 }"></SvgIcon>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabbed Interface -->
|
|
<div class="tabs mb-3">
|
|
<ul>
|
|
<li :class="{ 'is-active': activeTab === 'content' }">
|
|
<a @click="activeTab = 'content'">Content</a>
|
|
</li>
|
|
<template v-if="$getEditRights()">
|
|
<li :class="{ 'is-active': activeTab === 'mappings' }">
|
|
<a @click="activeTab = 'mappings'">Mappings</a>
|
|
</li>
|
|
<li :class="{ 'is-active': activeTab === 'automation' }">
|
|
<a @click="activeTab = 'automation'">Automation</a>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Form Component based on tab -->
|
|
<div v-show="activeTab === 'content'">
|
|
<component
|
|
:is="currentFormComponent"
|
|
:key="formKey"
|
|
:initial-data="emailFormData"
|
|
@data-change="handleChangeData"
|
|
/>
|
|
</div>
|
|
<div v-if="activeTab === 'mappings'">
|
|
<MappingConfigurator :mappings="formData.mappings" @update:mappings="updateMappings" />
|
|
</div>
|
|
<div v-if="activeTab === 'automation'">
|
|
<JobConfigurator :template-id="selectedTemplateId" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Section -->
|
|
<div class="column is-7 p-0" style="overflow-y: scroll">
|
|
<component :is="currentTemplateComponent" v-bind="templateProps" style="height: 100%" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div v-if="showDeleteDialog" class="modal is-active">
|
|
<div class="modal-background" @click="showDeleteDialog = false"></div>
|
|
<div class="modal-card">
|
|
<header class="modal-card-head p-4">
|
|
<p class="modal-card-title">Xác nhận xóa mẫu email</p>
|
|
<button class="delete" @click="showDeleteDialog = false"></button>
|
|
</header>
|
|
<section class="modal-card-body p-4">
|
|
<p class="mb-4">Bạn có chắc chắn muốn xóa mẫu email này không? Hành động này không thể hoàn tác.</p>
|
|
<p class="has-text-weight-bold has-text-danger">Mẫu: {{ dataTemplateSelected?.name }}</p>
|
|
</section>
|
|
<footer class="modal-card-foot p-4">
|
|
<button class="button mr-2" @click="showDeleteDialog = false" :disabled="loading">Hủy</button>
|
|
<button
|
|
class="button is-danger has-text-white"
|
|
@click="handleDeleteTemplate"
|
|
:disabled="loading"
|
|
:class="{ 'is-loading': loading }"
|
|
>
|
|
{{ loading ? "Đang xóa..." : "Xóa" }}
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useNuxtApp } from "nuxt/app";
|
|
import { ref, computed, onMounted, watch, markRaw } from "vue";
|
|
import EmailForm1 from "./forms/EmailForm1.vue";
|
|
import Template1 from "@/lib/email/templates/Template1.vue";
|
|
import Modal from "@/components/Modal.vue";
|
|
import MappingConfigurator from "@/components/marketing/email/MappingConfigurator.vue";
|
|
import JobConfigurator from "@/components/marketing/email/JobConfigurator.vue";
|
|
|
|
const nuxtApp = useNuxtApp();
|
|
const $snackbar = nuxtApp.$snackbar as (message?: string) => void;
|
|
const $getdata = nuxtApp.$getdata as (name: string) => Promise<DataTemplate[]>;
|
|
const $deleteapi = nuxtApp.$deleteapi as (name: string, id: any) => Promise<DataTemplate[]>;
|
|
const $getEditRights = nuxtApp.$getEditRights as () => boolean;
|
|
const showmodal = ref<any>();
|
|
const activeTab = ref("content");
|
|
const defaultTemplate = 'defaultTemplate';
|
|
|
|
// Types
|
|
interface FormContent {
|
|
receiver: string;
|
|
subject: string;
|
|
content: string; // This is the email body
|
|
imageUrl: string | null;
|
|
linkUrl: string[];
|
|
textLinkUrl: string[];
|
|
keyword: (string | { keyword: string; value: string })[];
|
|
html: string;
|
|
}
|
|
|
|
interface FormData {
|
|
name?: string;
|
|
id?: number;
|
|
template: string;
|
|
content: FormContent;
|
|
mappings: any[];
|
|
}
|
|
|
|
interface DataTemplate {
|
|
id: number;
|
|
name: string;
|
|
content: Record<string, any> | string;
|
|
mappings?: any[];
|
|
}
|
|
|
|
// Reactive state
|
|
const formData = ref<FormData>({
|
|
name: "",
|
|
id: undefined,
|
|
template: defaultTemplate,
|
|
content: {
|
|
receiver: "",
|
|
subject: "",
|
|
content: "",
|
|
imageUrl: null,
|
|
linkUrl: [""],
|
|
textLinkUrl: [""],
|
|
keyword: [{ keyword: "", value: "" }],
|
|
html: "",
|
|
},
|
|
mappings: [],
|
|
});
|
|
|
|
const dataTemplate = ref<DataTemplate[] | null>(null);
|
|
const dataTemplateSelected = ref<DataTemplate | null>(null);
|
|
const originalLoadedTemplate = ref<DataTemplate | null>(null); // For diffing on save
|
|
const editMode = ref(false);
|
|
const formKey = ref(0);
|
|
const selectedValue = ref(defaultTemplate);
|
|
const loading = ref(false);
|
|
const selectedTemplateId = ref<number | null>(null);
|
|
const showDeleteDialog = ref(false);
|
|
|
|
// Computed properties
|
|
const emailFormData = computed(() => ({
|
|
id: formData.value.id,
|
|
name: formData.value.name || "",
|
|
template: formData.value.template,
|
|
content: formData.value.content,
|
|
}));
|
|
|
|
const templateProps = computed(() => ({
|
|
content: {
|
|
subject: formData.value.content.subject || "Thông báo mới",
|
|
message: formData.value.content.content || "Bạn có một thông báo mới.",
|
|
imageUrl: formData.value.content.imageUrl || null,
|
|
linkUrl: formData.value.content.linkUrl || [""],
|
|
textLinkUrl: formData.value.content.textLinkUrl || [""],
|
|
keyword: Array.isArray(formData.value.content.keyword)
|
|
? formData.value.content.keyword.map((k) => (typeof k === "string" ? { keyword: k, value: "" } : k))
|
|
: [{ keyword: "", value: "" }],
|
|
},
|
|
previewMode: true,
|
|
}));
|
|
|
|
const currentFormComponent = computed(() => {
|
|
return markRaw(EmailForm1);
|
|
});
|
|
|
|
const currentTemplateComponent = computed(() => {
|
|
return markRaw(Template1);
|
|
});
|
|
|
|
// Methods
|
|
const handleChangeData = (data: Partial<FormData>) => {
|
|
formData.value.name = data.name || formData.value.name;
|
|
formData.value.id = data.id || formData.value.id;
|
|
if (data.content) {
|
|
formData.value.content = { ...formData.value.content, ...data.content };
|
|
}
|
|
};
|
|
|
|
const updateMappings = (newMappings: any[]) => {
|
|
formData.value.mappings = newMappings;
|
|
};
|
|
|
|
const resetToDefault = () => {
|
|
formData.value = {
|
|
name: "",
|
|
id: undefined,
|
|
template: defaultTemplate,
|
|
content: {
|
|
receiver: "",
|
|
subject: "",
|
|
content: "",
|
|
imageUrl: null,
|
|
linkUrl: [""],
|
|
textLinkUrl: [""],
|
|
keyword: [{ keyword: "", value: "" }],
|
|
html: "",
|
|
},
|
|
mappings: [],
|
|
};
|
|
dataTemplateSelected.value = null;
|
|
originalLoadedTemplate.value = null;
|
|
editMode.value = false;
|
|
formKey.value++;
|
|
};
|
|
|
|
const handleTemplateChange = async () => {
|
|
const templateValue = selectedValue.value;
|
|
|
|
if (templateValue === defaultTemplate) {
|
|
selectedTemplateId.value = null;
|
|
resetToDefault();
|
|
$snackbar("Template reset to default");
|
|
return;
|
|
}
|
|
|
|
const selectedTemplate = dataTemplate.value?.find((t) => t.name === templateValue);
|
|
|
|
if (selectedTemplate) {
|
|
originalLoadedTemplate.value = JSON.parse(JSON.stringify(selectedTemplate)); // Deep copy for diffing
|
|
selectedTemplateId.value = selectedTemplate.id;
|
|
dataTemplateSelected.value = selectedTemplate;
|
|
editMode.value = true;
|
|
|
|
const tplContent = selectedTemplate.content;
|
|
let emailBody = "";
|
|
let subject = "";
|
|
let receiver = "";
|
|
let imageUrl = null;
|
|
let linkUrl = [""];
|
|
let textLinkUrl = [""];
|
|
let keyword: any[] = [{ keyword: "", value: "" }];
|
|
let mappings: any[] = [];
|
|
|
|
if (typeof tplContent === "string") {
|
|
emailBody = tplContent;
|
|
subject = selectedTemplate.name;
|
|
} else if (typeof tplContent === "object" && tplContent !== null) {
|
|
emailBody = tplContent.content || ""; // Always use content
|
|
subject = tplContent.subject || "";
|
|
receiver = tplContent.receiver || "";
|
|
imageUrl = tplContent.imageUrl || null;
|
|
linkUrl = Array.isArray(tplContent.linkUrl) ? tplContent.linkUrl : [""];
|
|
textLinkUrl = Array.isArray(tplContent.textLinkUrl) ? tplContent.textLinkUrl : [""];
|
|
keyword = Array.isArray(tplContent.keyword) ? tplContent.keyword : [{ keyword: "", value: "" }];
|
|
mappings = tplContent.mappings || [];
|
|
}
|
|
|
|
formData.value = {
|
|
id: selectedTemplate.id,
|
|
name: selectedTemplate.name || "",
|
|
template: defaultTemplate,
|
|
content: {
|
|
receiver,
|
|
subject,
|
|
content: emailBody,
|
|
imageUrl,
|
|
linkUrl,
|
|
textLinkUrl,
|
|
keyword,
|
|
html: "", // Will be generated by preview
|
|
},
|
|
mappings,
|
|
};
|
|
|
|
formKey.value++;
|
|
$snackbar(`Template loaded: ${selectedTemplate.name}`);
|
|
}
|
|
};
|
|
|
|
const handleDeleteTemplate = async () => {
|
|
if (!selectedTemplateId.value || !dataTemplateSelected.value?.name) return;
|
|
|
|
try {
|
|
loading.value = true;
|
|
await $deleteapi('emailtemplate', selectedTemplateId.value);
|
|
$snackbar(`Template deleted: ${dataTemplateSelected.value.name}`);
|
|
await fetchTemplates();
|
|
selectedValue.value = defaultTemplate;
|
|
resetToDefault();
|
|
} catch (error) {
|
|
console.error("Error deleting template:", error);
|
|
$snackbar("Deleting template failed");
|
|
} finally {
|
|
loading.value = false;
|
|
showDeleteDialog.value = false;
|
|
}
|
|
};
|
|
|
|
const fetchTemplates = async () => {
|
|
try {
|
|
dataTemplate.value = await $getdata('emailtemplate');
|
|
} catch (error) {
|
|
$snackbar('Error: Failed to fetch templates');
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const handleOpenModal = () => {
|
|
let dataForModal;
|
|
|
|
if (editMode.value && originalLoadedTemplate.value) {
|
|
// EDIT MODE: Calculate a patch of changed data
|
|
const patchPayload: { id: number; name?: string; content?: Record<string, any> } = {
|
|
id: formData.value.id!,
|
|
};
|
|
|
|
// 1. Check if name has changed
|
|
if (formData.value.name !== originalLoadedTemplate.value.name) {
|
|
patchPayload.name = formData.value.name;
|
|
}
|
|
|
|
// 2. Reconstruct the content object, preserving all original fields
|
|
const originalContentObject = typeof originalLoadedTemplate.value.content === 'object'
|
|
? JSON.parse(JSON.stringify(originalLoadedTemplate.value.content))
|
|
: {};
|
|
|
|
const newContentObject = {
|
|
...originalContentObject,
|
|
subject: formData.value.content.subject,
|
|
content: formData.value.content.content,
|
|
receiver: formData.value.content.receiver,
|
|
imageUrl: formData.value.content.imageUrl,
|
|
linkUrl: formData.value.content.linkUrl,
|
|
textLinkUrl: formData.value.content.textLinkUrl,
|
|
keyword: formData.value.content.keyword,
|
|
mappings: formData.value.mappings,
|
|
};
|
|
|
|
// 3. Only include the 'content' field in the patch if it has actually changed
|
|
if (JSON.stringify(newContentObject) !== JSON.stringify(originalLoadedTemplate.value.content)) {
|
|
patchPayload.content = newContentObject;
|
|
}
|
|
|
|
dataForModal = patchPayload;
|
|
|
|
} else {
|
|
// CREATE MODE: Build the full object
|
|
const contentToSave = {
|
|
subject: formData.value.content.subject,
|
|
content: formData.value.content.content,
|
|
receiver: formData.value.content.receiver,
|
|
imageUrl: formData.value.content.imageUrl,
|
|
linkUrl: formData.value.content.linkUrl,
|
|
textLinkUrl: formData.value.content.textLinkUrl,
|
|
keyword: formData.value.content.keyword,
|
|
mappings: formData.value.mappings,
|
|
};
|
|
dataForModal = {
|
|
name: formData.value.name,
|
|
content: contentToSave,
|
|
};
|
|
}
|
|
|
|
showmodal.value = {
|
|
component: "marketing/email/dataTemplate/SaveListTemplate",
|
|
title: "Lưu mẫu email",
|
|
width: "auto",
|
|
height: "200px",
|
|
vbind: {
|
|
data: dataForModal,
|
|
editMode: editMode.value,
|
|
onSuccess: fetchTemplates,
|
|
},
|
|
};
|
|
};
|
|
|
|
// Watchers
|
|
watch(selectedValue, (newValue, oldValue) => {
|
|
if (newValue === defaultTemplate && oldValue !== defaultTemplate) {
|
|
resetToDefault();
|
|
}
|
|
});
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
fetchTemplates();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Custom Scrollbar */
|
|
.column::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.column::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.column::-webkit-scrollbar-thumb {
|
|
background: #888;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.column::-webkit-scrollbar-thumb:hover {
|
|
background: #555;
|
|
}
|
|
|
|
/* Firefox */
|
|
.column {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #888 #f1f1f1;
|
|
}
|
|
</style>
|