This commit is contained in:
Viet An
2026-06-10 13:46:11 +07:00
parent 7025fd8cd5
commit 22c284f1ef
15 changed files with 31 additions and 268 deletions

View File

@@ -183,7 +183,7 @@
></Modal> ></Modal>
</template> </template>
<script setup lang="ts"> <script setup>
import { useNuxtApp } from "nuxt/app"; import { useNuxtApp } from "nuxt/app";
import { ref, computed, onMounted, watch, markRaw } from "vue"; import { ref, computed, onMounted, watch, markRaw } from "vue";
import EmailForm1 from "./forms/EmailForm1.vue"; import EmailForm1 from "./forms/EmailForm1.vue";
@@ -193,43 +193,12 @@ import MappingConfigurator from "~/components/marketing/email/MappingConfigurato
import JobConfigurator from "~/components/marketing/email/JobConfigurator.vue"; import JobConfigurator from "~/components/marketing/email/JobConfigurator.vue";
const nuxtApp = useNuxtApp(); const nuxtApp = useNuxtApp();
const $snackbar = nuxtApp.$snackbar as (message?: string) => void; const { $getdata, $snackbar, $deleteapi, $getEditRights } = useNuxtApp();
const $getdata = nuxtApp.$getdata as (name: string) => Promise<DataTemplate[]>; const showmodal = ref();
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 activeTab = ref("content");
const defaultTemplate = "defaultTemplate"; const defaultTemplate = "defaultTemplate";
// Types const formData = ref({
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: "", name: "",
id: undefined, id: undefined,
template: defaultTemplate, template: defaultTemplate,
@@ -246,14 +215,14 @@ const formData = ref<FormData>({
mappings: [], mappings: [],
}); });
const dataTemplate = ref<DataTemplate[] | null>(null); const dataTemplate = ref(null);
const dataTemplateSelected = ref<DataTemplate | null>(null); const dataTemplateSelected = ref(null);
const originalLoadedTemplate = ref<DataTemplate | null>(null); // For diffing on save const originalLoadedTemplate = ref(null); // For diffing on save
const editMode = ref(false); const editMode = ref(false);
const formKey = ref(0); const formKey = ref(0);
const selectedValue = ref(defaultTemplate); const selectedValue = ref(defaultTemplate);
const loading = ref(false); const loading = ref(false);
const selectedTemplateId = ref<number | null>(null); const selectedTemplateId = ref(null);
const showDeleteDialog = ref(false); const showDeleteDialog = ref(false);
// Computed properties // Computed properties
@@ -287,7 +256,7 @@ const currentTemplateComponent = computed(() => {
}); });
// Methods // Methods
const handleChangeData = (data: Partial<FormData>) => { const handleChangeData = (data) => {
formData.value.name = data.name || formData.value.name; formData.value.name = data.name || formData.value.name;
formData.value.id = data.id || formData.value.id; formData.value.id = data.id || formData.value.id;
if (data.content) { if (data.content) {
@@ -295,7 +264,7 @@ const handleChangeData = (data: Partial<FormData>) => {
} }
}; };
const updateMappings = (newMappings: any[]) => { const updateMappings = (newMappings) => {
formData.value.mappings = newMappings; formData.value.mappings = newMappings;
}; };
@@ -347,8 +316,8 @@ const handleTemplateChange = async () => {
let imageUrl = null; let imageUrl = null;
let linkUrl = [""]; let linkUrl = [""];
let textLinkUrl = [""]; let textLinkUrl = [""];
let keyword: any[] = [{ keyword: "", value: "" }]; let keyword = [{ keyword: "", value: "" }];
let mappings: any[] = []; let mappings = [];
if (typeof tplContent === "string") { if (typeof tplContent === "string") {
emailBody = tplContent; emailBody = tplContent;
@@ -419,12 +388,8 @@ const handleOpenModal = () => {
if (editMode.value && originalLoadedTemplate.value) { if (editMode.value && originalLoadedTemplate.value) {
// EDIT MODE: Calculate a patch of changed data // EDIT MODE: Calculate a patch of changed data
const patchPayload: { const patchPayload = {
id: number; id: formData.value.id,
name?: string;
content?: Record<string, any>;
} = {
id: formData.value.id!,
}; };
// 1. Check if name has changed // 1. Check if name has changed

View File

@@ -234,31 +234,13 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup>
import { imageUrl } from "~/components/marketing/email/Email.utils"; import { imageUrl } from "~/components/marketing/email/Email.utils";
import { computed } from "vue"; import { computed } from "vue";
interface KeywordItem { const props = defineProps({
keyword: string; content: Object,
value: string; previewMode: Boolean,
}
interface Template1Content {
subject?: string;
message?: string;
imageUrl?: string | null;
linkUrl?: string[];
textLinkUrl?: string[];
keyword?: KeywordItem[];
}
interface Props {
content: Template1Content;
previewMode?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
previewMode: false,
}); });
const hasValidLinks = computed(() => { const hasValidLinks = computed(() => {
@@ -269,7 +251,7 @@ const hasValidImage = computed(() => {
return props.content.imageUrl && props.content.imageUrl.trim() !== ""; return props.content.imageUrl && props.content.imageUrl.trim() !== "";
}); });
const replaceKeywords = (content: string): string => { const replaceKeywords = (contentstring) => {
if (!content) return ""; if (!content) return "";
let replacedContent = content; let replacedContent = content;
@@ -293,7 +275,7 @@ const validLinks = computed(() => {
return props.content.linkUrl.filter((link) => link && link.trim() !== ""); return props.content.linkUrl.filter((link) => link && link.trim() !== "");
}); });
const getLinkText = (index: number): string => { const getLinkText = (index) => {
return props.content.textLinkUrl && props.content.textLinkUrl[index] && props.content.textLinkUrl[index].trim() !== "" return props.content.textLinkUrl && props.content.textLinkUrl[index] && props.content.textLinkUrl[index].trim() !== ""
? props.content.textLinkUrl[index] ? props.content.textLinkUrl[index]
: `Link ${index + 1}`; : `Link ${index + 1}`;
@@ -311,7 +293,7 @@ const styles = `
} }
.content-padding, .company-padding { .content-padding, .company-padding {
padding: 0px 25px 20px; padding: 0px 25px 20px;
} }
.message-content p { .message-content p {
@@ -407,16 +389,16 @@ const styles = `
width: 100% !important; width: 100% !important;
max-width: 100% !important; max-width: 100% !important;
} }
.greeting-padding{ .greeting-padding{
padding: 20px 0 0 0 !important; padding: 20px 0 0 0 !important;
} }
.content-padding { .content-padding {
padding: 20px 0px !important; padding: 20px 0px !important;
} }
.company-padding { .company-padding {
padding: 0px 0px 20px 0px !important; padding: 0px 0px 20px 0px !important;
} }
.company-info { .company-info {
padding: 20px 10px !important; padding: 20px 10px !important;

View File

@@ -248,8 +248,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import axios from "axios";
import { apiUrl } from "~/components/marketing/email/Email.utils";
import type dayjs from "dayjs"; import type dayjs from "dayjs";
interface EmailSent { interface EmailSent {
@@ -263,6 +261,7 @@ interface EmailSent {
} }
const nuxtApp = useNuxtApp(); const nuxtApp = useNuxtApp();
const $getdata = nuxtApp.$getdata as any;
const $snackbar = nuxtApp.$snackbar as (message?: string) => void; const $snackbar = nuxtApp.$snackbar as (message?: string) => void;
const $dayjs = nuxtApp.$dayjs as (date?: string | Date) => ReturnType<typeof dayjs>; const $dayjs = nuxtApp.$dayjs as (date?: string | Date) => ReturnType<typeof dayjs>;
@@ -311,10 +310,7 @@ const stripHtml = (html: string, maxLength: number = 1000): string => {
const getDataEmailsSent = async () => { const getDataEmailsSent = async () => {
loading.value = true; loading.value = true;
try { try {
const response = await axios.get(`${apiUrl}/Email_Sent/?sort=-id`); emailsSent.value = await $getdata("Email_Sent", { params: { sort: "-id" } });
if (response.status === 200) {
emailsSent.value = response.data.rows || response.data || [];
}
} catch (error) { } catch (error) {
console.error("Error fetching sent emails:", error); console.error("Error fetching sent emails:", error);
$snackbar("Failed to load sent emails"); $snackbar("Failed to load sent emails");

View File

@@ -198,15 +198,17 @@ const handleSave = async () => {
}; };
const handleCreateNew = async () => { const handleCreateNew = async () => {
const postTemplateUrl = `${apiUrl}/Email_Template/`;
try { try {
loadingCreate.value = true; loadingCreate.value = true;
const newTemplateData = { const newTemplateData = {
...props.data, ...props.data,
name: templateName.value, name: templateName.value,
}; };
const response = await axios.post(postTemplateUrl, newTemplateData); const res = await $insertapi("Email_Template", {
if (response.status === 201 || response.status === 200) { data: newTemplateData,
notify: false,
});
if (res !== "error") {
$snackbar("Template created successfully"); $snackbar("Template created successfully");
if (props.onClose) props.onClose(); if (props.onClose) props.onClose();
emit("close"); emit("close");

View File

@@ -1,29 +0,0 @@
<template>
<a
class="has-text-link"
@click="open"
>{{ row["code"] }}</a
>
</template>
<script setup>
import { useStore } from "~/stores/index";
const store = useStore();
const emit = defineEmits(["clickevent"]);
const props = defineProps({
row: Object,
});
function open() {
emit("clickevent", {
name: "dataevent",
data: {
modal: {
title: store.lang === "en" ? "Application" : "Đơn vay",
height: "500px",
width: "65%",
component: "application/ApplicationView",
vbind: { row: props.row },
},
},
});
}
</script>

View File

@@ -1,29 +0,0 @@
<template>
<a
class="has-text-link"
@click="open"
>{{ row["customer__code"] }}</a
>
</template>
<script setup>
import { useStore } from "~/stores/index";
const store = useStore();
const emit = defineEmits(["clickevent"]);
const props = defineProps({
row: Object,
});
function open() {
emit("clickevent", {
name: "dataevent",
data: {
modal: {
title: store.lang === "en" ? "Customer" : "Khách hàng",
height: "500px",
width: "60%",
component: "customer/CustomerView",
vbind: { row: props.row },
},
},
});
}
</script>

View File

@@ -23,8 +23,6 @@ import CountWithAdd from "~/components/common/CountWithAdd.vue";
// menu // menu
import MenuAction from "~/components/menu/MenuAction.vue"; import MenuAction from "~/components/menu/MenuAction.vue";
import MenuApp from "~/components/menu/MenuApp.vue";
import MenuCust from "~/components/menu/MenuCust.vue";
import MenuPhone from "~/components/menu/MenuPhone.vue"; import MenuPhone from "~/components/menu/MenuPhone.vue";
import MenuParam from "~/components/menu/MenuParam.vue"; import MenuParam from "~/components/menu/MenuParam.vue";
import MenuAdd from "~/components/menu/MenuAdd.vue"; import MenuAdd from "~/components/menu/MenuAdd.vue";
@@ -107,8 +105,6 @@ const components = {
MenuParam, MenuParam,
FormatNumber, FormatNumber,
FormatDate, FormatDate,
MenuApp,
MenuCust,
MenuAdd, MenuAdd,
MenuNote, MenuNote,
ImageLayout, ImageLayout,

View File

@@ -1,8 +0,0 @@
export interface EmailContextType {
selectedEmails: string;
selectedName: string;
selectedId: string;
setSelectedEmails: (emails: string) => void;
setSelectedName: (name: string) => void;
setSelectedId: (id: string) => void;
}

View File

@@ -1,32 +0,0 @@
export interface FormData {
id: number | undefined;
name: string;
template: string;
content: {
receiver: string;
subject: string;
content: string;
imageUrl: string | null;
linkUrl: string[];
textLinkUrl: string[];
keyword: Array<string | { keyword: string; value: string }>;
html: string;
};
// emails: string;
// subject: string;
// message: string;
// template: string;
// company?: string;
// phone?: string;
// imageUrl?: string | null;
// linkUrl?: string[];
// keyWords?: string[] | { keyword: string; value: string }[];
// textLinkUrl?: string[];
}
export interface EmailFormProps {
onDataChange: (data: FormData) => void;
initialData?: FormData;
}
export type ModalType = "none" | "save-list" | "open-list" | "save-template" | "open-template";

View File

@@ -1,11 +0,0 @@
export interface EmailSent {
id: string;
receiver: string;
subject: string;
content: string;
status: number;
create_time: string;
update_time: string;
}
export type EmailSentStatus = "pending" | "success" | "error" | "schedule";

View File

@@ -1,7 +0,0 @@
export interface ModalProps {
active: boolean;
onClose: () => void;
height?: string | number;
width?: string | number;
children: React.ReactNode;
}

View File

@@ -1,7 +0,0 @@
export interface openListGmailProps {
emails: string;
dataEmail: { id: string; email: string; name: string }[];
onClose: () => void;
onEdit?: (id: string, name: string, emails: string) => void;
loading?: boolean;
}

View File

@@ -1,12 +0,0 @@
export interface SaveListGmailProps {
emails: string;
name?: string;
id?: string;
onClose?: () => void;
onSuccess?: () => Promise<void>;
}
export interface DataEmail {
email: string;
name: string;
}

View File

@@ -1,32 +0,0 @@
export interface saveListTemplateProps {
name?: string;
content?: {
receiver: string;
subject: string;
content: string;
imageUrl: string | null;
linkUrl: string[] | string;
textLinkUrl: string[] | string;
keyword: Array<string | { keyword: string; value: string }>;
html: string;
};
editMode?: boolean;
id?: number;
onClose?: () => void;
onSuccess?: () => Promise<void>;
}
export interface DataTemplate {
id: number;
name: string;
content: {
receiver: string;
subject: string;
content: string;
imageUrl: string | null;
linkUrl: string[] | string;
textLinkUrl: string[] | string;
keyword: Array<string | { keyword: string; value: string }>;
html: string;
};
}

View File

@@ -1,11 +0,0 @@
export interface Template1Props {
content: {
subject?: string;
message?: string;
imageUrl?: string | null;
linkUrl?: string[];
textLinkUrl?: string[];
keyword: Array<string | { keyword: string; value: string }>;
};
previewMode?: boolean;
}