Files
web/app/components/marketing/email/Email.vue
2026-03-02 09:45:33 +07:00

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 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>