This commit is contained in:
Viet An
2026-07-09 16:45:48 +07:00
parent 42124d4071
commit df4ed4bd4d
20 changed files with 726 additions and 964 deletions

View File

@@ -12,27 +12,3 @@
position: relative; position: relative;
width: 100%; width: 100%;
} }
// Tooltip Styles
.tooltiptext {
opacity: 0;
background-color: hsl(from var(--bulma-black-bis) h s l / 0.7);
color: var(--bulma-white-bis);
border-radius: 6px;
position: absolute;
margin-left: 0px;
z-index: 999;
bottom: 110%;
transition: opacity 0.2s;
padding: 0.4rem 0.6rem;
font-size: 13px;
pointer-events: none;
backdrop-filter: blur(2px);
}
.control:hover .tooltiptext {
opacity: 1;
position: absolute;
min-width: 300px;
z-index: 999;
}

View File

@@ -4,6 +4,28 @@
border: 1px solid red !important; border: 1px solid red !important;
} }
.tooltiptext {
opacity: 0;
background-color: hsl(from var(--bulma-black-bis) h s l / 0.8);
color: var(--bulma-white-bis);
border-radius: 4px;
position: absolute;
margin-left: 0px;
z-index: 999;
left: 0;
bottom: -32px;
min-width: max-content;
transition: opacity 0.2s;
padding: 0.3rem 0.6rem;
font-size: 12px;
pointer-events: none;
backdrop-filter: blur(2px);
}
*:hover > .tooltiptext {
opacity: 1;
}
// Font size loops // Font size loops
@for $i from 10 through 50 { @for $i from 10 through 50 {
.fs-#{$i} { .fs-#{$i} {

View File

@@ -14,7 +14,7 @@
<div <div
class="modal-card" class="modal-card"
:style="{ :style="{
width: $store.viewport <= 1 ? 'calc(100% - var(--bulma-modal-card-spacing))' : width, width: $store.viewport > 1 ? width : null,
height, height,
}" }"
> >

View File

@@ -52,7 +52,6 @@
<script setup> <script setup>
import CustomerForm from "~/components/customer/CustomerForm.vue"; import CustomerForm from "~/components/customer/CustomerForm.vue";
import CustomerView from "~/components/customer/CustomerView.vue"; import CustomerView from "~/components/customer/CustomerView.vue";
import Modal from "~/components/Modal.vue";
import ImageGallery from "~/components/media/ImageGallery.vue"; import ImageGallery from "~/components/media/ImageGallery.vue";
const { $dialog } = useNuxtApp(); const { $dialog } = useNuxtApp();

View File

@@ -1,44 +1,6 @@
<template> <template>
<div class="is-flex is-flex-direction-column max-h-full"> <div class="is-flex is-flex-direction-column max-h-full">
<div class="is-flex-shrink-0 toolbar field has-addons is-justify-content-center"> <div class="is-flex-shrink-0 toolbar field has-addons is-justify-content-center">
<!-- <div class="control">
<button
class="button is-light is-primary"
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })"
>
<span class="icon">
<Icon
name="mdi:sort-alphabetical-ascending"
:size="22"
:class="checkFilter() && 'has-text-grey-70'"
/>
</span>
</button>
<span
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
>Sắp xếp tăng dần</span
>
</div>
<div class="control">
<button
class="button is-light is-primary"
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })"
>
<span class="icon">
<Icon
name="mdi:sort-alphabetical-descending"
:size="22"
:class="checkFilter() && 'has-text-grey-70'"
/>
</span>
</button>
<span
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 25px"
>Sắp xếp giảm dần</span
>
</div> -->
<div class="control"> <div class="control">
<button <button
class="button" class="button"
@@ -51,11 +13,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Chuyển cột sang trái</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Chuyển cột sang trái</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -69,11 +27,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Chuyển cột sang phải</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Chuyển cột sang phải</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -87,11 +41,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Tăng độ rộng cột</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Tăng độ rộng cột</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -105,11 +55,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Giảm độ rộng cột</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Giảm độ rộng cột</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -123,11 +69,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Ẩn cột</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Ẩn cột</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -141,11 +83,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Xóa cột</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Xóa cột</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -159,11 +97,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Sao chép cột</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Sao chép cột</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -177,11 +111,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext">Danh sách cột</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Danh sách cột</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -195,11 +125,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext left-auto right-0">Tạo cột mới</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: 0"
>Tạo cột mới</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -213,11 +139,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext left-auto right-0">Tùy chọn bảng</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; right: 0"
>Tùy chọn bảng</span
>
</div> </div>
<div class="control"> <div class="control">
<button <button
@@ -231,12 +153,7 @@
/> />
</span> </span>
</button> </button>
<p <p class="tooltiptext left-auto right-0">Lưu thiết lập</p>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; right: 0"
>
Lưu thiết lập
</p>
</div> </div>
</div> </div>
<div class="is-flex-shrink-0 tabs"> <div class="is-flex-shrink-0 tabs">

View File

@@ -87,12 +87,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext top-0">Nhập dữ liệu</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
>
Nhập dữ liệu
</span>
</p> </p>
<p <p
v-if="enableAdd && $getEditRights()" v-if="enableAdd && $getEditRights()"
@@ -109,12 +104,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext left-auto right-0">Thêm mới</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
>
Thêm mới
</span>
</p> </p>
<p class="control"> <p class="control">
<button <button
@@ -128,12 +118,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext left-auto right-0">Xuất Excel</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
>
Xuất Excel
</span>
</p> </p>
<p class="control"> <p class="control">
<button <button
@@ -147,12 +132,7 @@
/> />
</span> </span>
</button> </button>
<span <span class="tooltiptext left-auto right-0">Làm mới</span>
class="tooltiptext"
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
>
Làm mới
</span>
</p> </p>
<Icon <Icon
v-if="loading" v-if="loading"

View File

@@ -1,6 +1,4 @@
<script setup> <script setup>
import Modal from "~/components/Modal.vue";
const props = defineProps({ const props = defineProps({
product: Object, product: Object,
}); });

View File

@@ -1,6 +1,4 @@
<script setup> <script setup>
import Modal from "~/components/Modal.vue";
const props = defineProps({ const props = defineProps({
variant: Object, variant: Object,
}); });

View File

@@ -3,141 +3,114 @@
class="container is-fluid px-0" class="container is-fluid px-0"
style="overflow: hidden" style="overflow: hidden"
> >
<!-- 95px is height of nav + breadcrumb --> <div class="fixed-grid has-12-cols">
<div <div class="grid">
class="columns m-0" <!-- Form Section -->
style="height: calc(100vh - 95px)" <div
> class="cell is-col-span-5"
<!-- Form Section --> style="overflow-y: scroll"
<div >
class="column is-5 pb-5" <label class="label">Chọn mẫu</label>
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 --> <!-- Template Selector -->
<div class="field has-addons">
<div <div class="control has-icons-left is-expanded">
class="level-left" <span class="icon is-small is-left">
style="flex: 1" <Icon
> name="tabler:template"
<div :size="20"
class="level-item" />
style="flex: 1" </span>
> <div class="select is-fullwidth">
<div <select
class="field" v-model="selectedValue"
style="width: 100%" @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 class="control">
<button
v-if="selectedTemplateId"
class="button is-danger is-light"
@click="showDeleteDialog = true"
:disabled="loading"
> >
<div class="control"> <span class="icon">
<div class="select is-fullwidth"> <Icon
<select name="material-symbols:delete-outline-rounded"
v-model="selectedValue" :size="20"
@change="handleTemplateChange" />
> </span>
<option value="defaultTemplate">Mặc định</option> </button>
<option </div>
v-for="template in dataTemplate" <div class="control">
:key="template.id" <button
:value="template.name" class="button is-primary is-light"
> @click="handleOpenModal"
{{ template.name }} >
</option> <span class="icon">
</select> <Icon
</div> name="material-symbols:save-outline-rounded"
</div> :size="20"
</div> />
</span>
</button>
</div> </div>
</div> </div>
<!-- Tabs -->
<!-- Action Buttons --> <div class="tabs">
<div <ul>
v-if="$getEditRights()" <li
class="level-right" v-for="tab in ['content', 'mappings', 'automation']"
> :key="tab"
<div class="level-item"> :class="[activeTab === tab && 'is-active']"
<div class="buttons"> >
<button <a @click="activeTab = tab">{{ capitalize(tab) }}</a>
class="button is-light" </li>
@click="handleOpenModal" </ul>
> </div>
<span class="icon"> <!-- Form Component based on tab -->
<Icon <div class="card">
name="material-symbols:save-outline-rounded" <div class="card-content p-4">
:size="18" <div v-show="activeTab === 'content'">
class="has-text-primary" <component
/> :is="currentFormComponent"
</span> :key="formKey"
</button> :initial-data="emailFormData"
<button @data-change="handleChangeData"
v-if="selectedTemplateId" />
class="button is-danger is-outlined"
@click="showDeleteDialog = true"
:disabled="loading"
>
<span class="icon">
<Icon
name="material-symbols:delete-outline-rounded"
:size="18"
/>
</span>
</button>
</div> </div>
<MappingConfigurator
v-if="activeTab === 'mappings'"
:mappings="formData.mappings"
@update:mappings="updateMappings"
/>
<JobConfigurator
v-if="activeTab === 'automation'"
:template-id="selectedTemplateId"
/>
</div> </div>
</div> </div>
</div> </div>
<!-- Preview Section -->
<!-- Tabbed Interface --> <div
<div class="tabs mb-3"> class="cell is-col-span-7"
<ul> style="overflow-y: scroll"
<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 <component
:is="currentFormComponent" :is="currentTemplateComponent"
:key="formKey" v-bind="templateProps"
:initial-data="emailFormData" style="height: 100%"
@data-change="handleChangeData"
/> />
</div> </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>
</div> </div>
@@ -184,16 +157,16 @@
</div> </div>
<Modal <Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal" v-if="showmodal"
></Modal> v-bind="showmodal"
@close="showmodal = undefined"
/>
</template> </template>
<script setup> <script setup>
import { capitalize } from "es-toolkit";
import EmailForm1 from "./forms/EmailForm1.vue"; import EmailForm1 from "./forms/EmailForm1.vue";
import Template1 from "~/components/marketing/email/Template1.vue"; import Template1 from "~/components/marketing/email/Template1.vue";
import Modal from "~/components/Modal.vue";
import MappingConfigurator from "~/components/marketing/email/MappingConfigurator.vue"; import MappingConfigurator from "~/components/marketing/email/MappingConfigurator.vue";
import JobConfigurator from "~/components/marketing/email/JobConfigurator.vue"; import JobConfigurator from "~/components/marketing/email/JobConfigurator.vue";
@@ -380,7 +353,7 @@ const handleDeleteTemplate = async () => {
const fetchTemplates = async () => { const fetchTemplates = async () => {
try { try {
dataTemplate.value = await $getdata("emailtemplate"); dataTemplate.value = await $getdata("Email_Template");
} catch (error) { } catch (error) {
$snackbar("Error: Failed to fetch templates"); $snackbar("Error: Failed to fetch templates");
console.error(error); console.error(error);

View File

@@ -1,102 +1,91 @@
<template> <template>
<div class="box"> <div>
<div class="level"> <div class="block is-flex is-gap-1 is-align-items-center is-justify-content-space-between">
<div class="level-left"> <p class="fs-17 font-semibold">Automation Jobs</p>
<h5 class="title is-5">Automation Jobs</h5> <button
</div> class="button is-primary"
<div class="level-right"> @click="openNewJobForm"
<button >
class="button is-primary" <span class="icon">
@click="openNewJobForm" <Icon
> name="material-symbols:add-rounded"
<span class="icon"> :size="18"
<Icon />
name="material-symbols:add-rounded" </span>
:size="18" <span>Add New Job</span>
/> </button>
</span>
<span>Add New Job</span>
</button>
</div>
</div> </div>
<p
<div
v-if="isLoading" v-if="isLoading"
class="has-text-centered" class="has-text-centered has-text-grey py-4"
> >
<p>Loading jobs...</p> Loading jobs...
</div> </p>
<p
<div
v-else-if="jobs.length === 0" v-else-if="jobs.length === 0"
class="has-text-centered" class="has-text-centered has-text-grey py-4"
> >
<p>No automation jobs found for this template.</p> No automation jobs found for this template.
</div> </p>
<div v-else> <div v-else>
<div <div
v-for="job in jobs" v-for="job in jobs"
:key="job.id" :key="job.id"
class="mb-4 p-3 border" class="mb-4 p-3 rounded"
style="border: 1px solid var(--bulma-grey-90)"
> >
<div class="level"> <div class="is-flex is-gap-1 is-justify-content-space-between is-align-items-center">
<div class="level-left"> <div class="is-flex-shrink-1">
<div> <p class="font-semibold mb-1">{{ job.name }}</p>
<p class="has-text-weight-bold">{{ job.name }}</p> <p class="is-size-7">Model: {{ job.model_name }}</p>
<p class="is-size-7">Model: {{ job.model_name }}</p> <p class="is-size-7">
<p class="is-size-7"> Triggers:
Triggers: <span
<span v-if="job.trigger_on_create"
v-if="job.trigger_on_create" class="tag is-info is-light mr-1"
class="tag is-info is-light mr-1" >On Create</span
>On Create</span >
> <span
<span v-if="job.trigger_on_update"
v-if="job.trigger_on_update" class="tag is-warning is-light"
class="tag is-warning is-light" >On Update</span
>On Update</span >
> </p>
</p>
</div>
</div> </div>
<div class="level-right"> <div class="field has-addons">
<div class="field has-addons"> <div class="control">
<div class="control"> <button
<button :class="['button is-small', job.active && 'is-primary']"
class="button is-small" @click="toggleJobStatus(job)"
:class="{ 'is-success': job.active, 'is-light': !job.active }" >
@click="toggleJobStatus(job)" {{ job.active ? "Active" : "Inactive" }}
> </button>
{{ job.active ? "Active" : "Inactive" }} </div>
</button> <div class="control">
</div> <button
<div class="control"> class="button is-small"
<button @click="openEditJobForm(job)"
class="button is-small" >
@click="openEditJobForm(job)" <span class="icon">
> <Icon
<span class="icon"> name="material-symbols:edit-outline-rounded"
<Icon :size="18"
name="material-symbols:edit-outline-rounded" />
:size="18" </span>
/> </button>
</span> </div>
</button> <div class="control">
</div> <button
<div class="control"> class="button is-danger is-small"
<button @click="confirmDelete(job)"
class="button is-danger is-small" >
@click="confirmDelete(job)" <span class="icon">
> <Icon
<span class="icon"> name="material-symbols:delete-outline-rounded"
<Icon :size="18"
name="material-symbols:delete-outline-rounded" />
:size="18" </span>
/> </button>
</span>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -197,7 +186,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup>
import { apiUrl, putApiUrl } from "~/components/marketing/email/Email.utils"; import { apiUrl, putApiUrl } from "~/components/marketing/email/Email.utils";
const props = defineProps({ const props = defineProps({
@@ -207,10 +196,9 @@ const props = defineProps({
}, },
}); });
const nuxtApp = useNuxtApp(); const { $getdata, $snackbar } = useNuxtApp();
const $snackbar = nuxtApp.$snackbar as (message: string) => void;
const jobs = ref<any[]>([]); const jobs = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
const isSaving = ref(false); const isSaving = ref(false);
const showForm = ref(false); const showForm = ref(false);
@@ -238,7 +226,11 @@ const fetchJobs = async () => {
const response = await $fetch(`${apiUrl}/Email_Job/`, { const response = await $fetch(`${apiUrl}/Email_Job/`, {
params: { template_id: props.templateId }, params: { template_id: props.templateId },
}); });
jobs.value = response.rows || []; jobs.value = await $getdata("Email_Job", {
filter: {
template_id: props.templateId,
},
});
} catch (error) { } catch (error) {
console.error("Error fetching email jobs:", error); console.error("Error fetching email jobs:", error);
$snackbar(`Error fetching jobs`); $snackbar(`Error fetching jobs`);
@@ -259,7 +251,7 @@ const openNewJobForm = () => {
showForm.value = true; showForm.value = true;
}; };
const openEditJobForm = (job: any) => { const openEditJobForm = (job) => {
isEditing.value = true; isEditing.value = true;
jobForm.value = { ...job }; jobForm.value = { ...job };
showForm.value = true; showForm.value = true;
@@ -283,19 +275,19 @@ const saveJob = async () => {
body: data, body: data,
}); });
if (response) { if (response) {
$snackbar(`Job saved successfully`); $snackbar("Job saved successfully", "Success");
await fetchJobs(); await fetchJobs();
closeForm(); closeForm();
} }
} catch (error) { } catch (error) {
console.error("Error saving job:", error); console.error("Error saving job:", error);
$snackbar(`Error saving job`); $snackbar("Error saving job", "Error");
} finally { } finally {
isSaving.value = false; isSaving.value = false;
} }
}; };
const toggleJobStatus = async (job: any) => { const toggleJobStatus = async (job) => {
const updatedJob = { ...job, active: !job.active }; const updatedJob = { ...job, active: !job.active };
try { try {
await $fetch(`${putApiUrl}/Email_Job/${job.id}`, { await $fetch(`${putApiUrl}/Email_Job/${job.id}`, {
@@ -310,13 +302,13 @@ const toggleJobStatus = async (job: any) => {
} }
}; };
const confirmDelete = (job: any) => { const confirmDelete = (job) => {
if (confirm(`Are you sure you want to delete the job "${job.name}"?`)) { if (confirm(`Are you sure you want to delete the job "${job.name}"?`)) {
deleteJob(job.id); deleteJob(job.id);
} }
}; };
const deleteJob = async (jobId: number) => { const deleteJob = async (jobId) => {
try { try {
await $fetch(`${putApiUrl}/Email_Job/${jobId}`, { await $fetch(`${putApiUrl}/Email_Job/${jobId}`, {
method: "DELETE", method: "DELETE",
@@ -333,13 +325,6 @@ onMounted(fetchJobs);
</script> </script>
<style scoped> <style scoped>
.box {
padding: 1.5rem;
}
.border {
border: 1px solid #dbdbdb;
border-radius: 4px;
}
.modal-card-foot { .modal-card-foot {
justify-content: flex-end; justify-content: flex-end;
} }

View File

@@ -1,161 +1,151 @@
<template> <template>
<div class="box"> <div>
<div <div
v-for="(mapping, mapIndex) in localMappings" v-for="(mapping, mapIndex) in localMappings"
:key="mapIndex" :key="mapIndex"
class="mb-5 p-4 border" class="card"
style="border: 1px solid var(--bulma-grey-90)"
> >
<div class="level"> <div class="card-content p-4">
<div class="level-left"> <div class="block is-flex is-gap-1 is-justify-content-space-between is-align-items-center">
<h5 class="title is-5">Mapping {{ mapIndex + 1 }}: {{ mapping.alias }}</h5> <h5 class="fs-17 font-semibold">Mapping {{ mapIndex + 1 }}: {{ mapping.alias }}</h5>
</div>
<div class="level-right">
<button
class="button is-danger is-small"
@click="removeMapping(mapIndex)"
>
<span class="icon is-small">
<Icon name="material-symbols:delete-outline-rounded" />
</span>
</button>
</div>
</div>
<div class="columns is-multiline">
<div class="column is-6">
<div class="field">
<label class="label">Alias</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.alias"
@input="update"
/>
</div>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Model</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.model"
@input="update"
placeholder="e.g., app.Transaction"
/>
</div>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Lookup Field</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.lookup_field"
@input="update"
placeholder="e.g., id"
/>
</div>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Lookup Value From</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.lookup_value_from"
@input="update"
placeholder="e.g., transaction_id or transaction.customer.id"
/>
</div>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Type</label>
<div class="control">
<div class="select is-fullwidth">
<select
v-model="mapping.type"
@change="update"
>
<option value="object">Object</option>
<option value="list">List</option>
</select>
</div>
</div>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Is Trigger Object?</label>
<div class="control">
<label class="checkbox">
<input
type="checkbox"
v-model="mapping.is_trigger_object"
@change="update"
/>
Yes
</label>
</div>
</div>
</div>
</div>
<hr />
<h6 class="title is-6">Fields</h6>
<div
v-for="(field, fieldIndex) in mapping.fields"
:key="fieldIndex"
class="field has-addons"
>
<div class="control">
<input
class="input"
type="text"
v-model="field.placeholder"
@input="update"
placeholder="[placeholder]"
/>
</div>
<div class="control is-expanded">
<input
class="input"
type="text"
v-model="field.source"
@input="update"
placeholder="source.field.name"
/>
</div>
<div class="control">
<button <button
class="button is-danger is-light" class="button is-danger is-light"
@click="removeField(mapIndex, fieldIndex as number)" @click="removeMapping(mapIndex)"
> >
<span class="icon"> <span class="icon">
<Icon <Icon
name="material-symbols:delete-outline-rounded" name="material-symbols:delete-outline-rounded"
:size="20" :size="18"
/> />
</span> </span>
</button> </button>
</div> </div>
<div class="fixed-grid">
<div class="grid">
<div class="cell">
<label class="label">Alias</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.alias"
@input="update"
/>
</div>
</div>
<div class="cell">
<label class="label">Model</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.model"
@input="update"
placeholder="e.g., app.Transaction"
/>
</div>
</div>
<div class="cell">
<label class="label">Lookup Field</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.lookup_field"
@input="update"
placeholder="e.g., id"
/>
</div>
</div>
<div class="cell">
<label class="label">Lookup Value From</label>
<div class="control">
<input
class="input"
type="text"
v-model="mapping.lookup_value_from"
@input="update"
placeholder="e.g., transaction_id or transaction.customer.id"
/>
</div>
</div>
<div class="cell">
<label class="label">Type</label>
<div class="control">
<div class="select is-fullwidth">
<select
v-model="mapping.type"
@change="update"
>
<option value="object">Object</option>
<option value="list">List</option>
</select>
</div>
</div>
</div>
<div class="cell">
<label class="label">Is Trigger Object?</label>
<div class="control">
<label class="checkbox">
<input
type="checkbox"
v-model="mapping.is_trigger_object"
@change="update"
/>
Yes
</label>
</div>
</div>
</div>
</div>
<hr />
<p class="label">Fields</p>
<div
v-for="(field, fieldIndex) in mapping.fields"
:key="fieldIndex"
class="field has-addons"
>
<div class="control">
<input
class="input"
type="text"
v-model="field.placeholder"
@input="update"
placeholder="[placeholder]"
/>
</div>
<div class="control is-expanded">
<input
class="input"
type="text"
v-model="field.source"
@input="update"
placeholder="source.field.name"
/>
</div>
<div class="control">
<button
class="button is-danger is-light"
@click="removeField(mapIndex, fieldIndex as number)"
>
<span class="icon">
<Icon
name="material-symbols:delete-outline-rounded"
:size="20"
/>
</span>
</button>
</div>
</div>
<button
class="button is-small is-light"
@click="addField(mapIndex)"
>
<span class="icon is-small">+</span>
<span>Add Field</span>
</button>
</div> </div>
<button
class="button is-small is-light"
@click="addField(mapIndex)"
>
<span class="icon is-small">+</span>
<span>Add Field</span>
</button>
</div> </div>
<button <button
@@ -174,8 +164,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from "vue";
const props = defineProps({ const props = defineProps({
mappings: { mappings: {
type: Array, type: Array,
@@ -260,13 +248,3 @@ const update = () => {
emit("update:mappings", reformatMappingsForEmit()); emit("update:mappings", reformatMappingsForEmit());
}; };
</script> </script>
<style scoped>
.box {
padding: 1.5rem;
}
.border {
border: 1px solid #dbdbdb;
border-radius: 4px;
}
</style>

View File

@@ -4,14 +4,7 @@
:is="'style'" :is="'style'"
v-html="styles" v-html="styles"
/> />
<div <div class="container py-6 min-h-full">
class="container py-6"
:style="{
minHeight: '100%',
backgroundColor: 'hsl(0, 0%, 97%)',
fontFamily: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif',
}"
>
<table <table
role="presentation" role="presentation"
cellspacing="0" cellspacing="0"
@@ -37,8 +30,7 @@
width: '100%', width: '100%',
maxWidth: '680px', maxWidth: '680px',
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
borderRadius: '16px', border: '1px solid #dedede',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
overflow: 'hidden', overflow: 'hidden',
}" }"
> >
@@ -102,22 +94,21 @@
overflow: 'hidden', overflow: 'hidden',
}" }"
> >
<div :style="{ padding: '15px' }"> <div
class="message-content"
:style="{
padding: '16px',
fontSize: '16px',
margin: '0',
color: '#374151',
lineHeight: '1.6',
wordBreak: 'break-word',
}"
>
<div <div
class="message-content" class="content"
:style="{ v-html="processedMessage"
fontSize: '16px', ></div>
margin: '0',
color: '#374151',
lineHeight: '1.6',
wordBreak: 'break-word',
}"
>
<div
class="content"
v-html="processedMessage"
/>
</div>
</div> </div>
<div <div
@@ -169,7 +160,6 @@
<div <div
class="company-info" class="company-info"
:style="{ :style="{
background: 'linear-gradient(to right, #000000, #0f9b0f)',
color: '#ffffff', color: '#ffffff',
borderRadius: '12px', borderRadius: '12px',
}" }"
@@ -233,7 +223,6 @@
<script setup> <script setup>
import { imageUrl } from "~/components/marketing/email/Email.utils"; import { imageUrl } from "~/components/marketing/email/Email.utils";
import { computed } from "vue";
const props = defineProps({ const props = defineProps({
content: Object, content: Object,
@@ -248,7 +237,7 @@ const hasValidImage = computed(() => {
return props.content.imageUrl && props.content.imageUrl.trim() !== ""; return props.content.imageUrl && props.content.imageUrl.trim() !== "";
}); });
const replaceKeywords = (contentstring) => { const replaceKeywords = (content) => {
if (!content) return ""; if (!content) return "";
let replacedContent = content; let replacedContent = content;
@@ -415,10 +404,6 @@ const styles = `
.message-text { .message-text {
font-size: 12px !important; font-size: 12px !important;
} }
.logo-container {
width: 150px !important;
height: 45px !important;
}
.company-name { .company-name {
font-size: 12px !important; font-size: 12px !important;
color: #ffffff !important; color: #ffffff !important;

View File

@@ -1,67 +1,56 @@
<template> <template>
<div class="box"> <div class="table-container">
<div class="table-container"> <table class="table is-fullwidth is-hoverable">
<table class="table is-bordered is-fullwidth is-hoverable"> <thead>
<thead> <tr>
<tr> <th>List Name</th>
<th>List Name</th> <th>Email</th>
<th>Email</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <tr
<template v-if="loading"> v-if="loading"
<tr v-for="index in 5"
v-for="index in 5" :key="index"
:key="index" >
> <td>
<td> <div class="skeleton-lines">
<div <div></div>
class="skeleton-loader" </div>
style="width: 150px; height: 20px" </td>
></div> <td>
</td> <div class="skeleton-lines">
<td> <div></div>
<div </div>
class="skeleton-loader" </td>
style="width: 250px; height: 20px" </tr>
></div> <tr v-else-if="dataEmail.length === 0">
</td> <td
</tr> colspan="2"
</template> class="has-text-centered py-6"
<template v-else-if="dataEmail.length === 0"> >
<tr> <div class="has-text-grey">
<td <p class="has-text-weight-medium">No saved lists found</p>
colspan="2" <p class="is-size-7 mt-1">Create a new list to get started</p>
class="has-text-centered py-6" </div>
> </td>
<div class="has-text-grey"> </tr>
<p class="has-text-weight-medium">No saved lists found</p> <tr
<p class="is-size-7 mt-2">Create a new list to get started</p> v-else
</div> v-for="item in dataEmail"
</td> :key="item.id"
</tr> :class="['is-clickable', selectedEmails === item.email ? 'is-selected' : '']"
</template> @click="handleRowClick(item.email, item.name, item.id)"
<template v-else> >
<tr <td class="has-text-weight-medium">{{ item.name }}</td>
v-for="item in dataEmail" <td class="has-text-grey-30">{{ item.email }}</td>
:key="item.id" </tr>
:class="['is-clickable', selectedEmails === item.email ? 'is-selected' : '']" </tbody>
@click="handleRowClick(item.email, item.name, item.id)" </table>
>
<td class="has-text-weight-medium">{{ item.name }}</td>
<td class="has-text-grey-30">{{ item.email }}</td>
</tr>
</template>
</tbody>
</table>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue";
import { useNuxtApp } from "nuxt/app";
interface EmailItem { interface EmailItem {
id: string; id: string;
name: string; name: string;
@@ -112,10 +101,6 @@ const handleRowClick = (email: string, name: string, id: string) => {
</script> </script>
<style scoped> <style scoped>
.is-clickable {
cursor: pointer;
}
.is-clickable:hover { .is-clickable:hover {
background-color: hsl(219, 70%, 96%) !important; background-color: hsl(219, 70%, 96%) !important;
} }
@@ -123,20 +108,4 @@ const handleRowClick = (email: string, name: string, id: string) => {
.is-selected { .is-selected {
background-color: hsl(219, 70%, 90%) !important; background-color: hsl(219, 70%, 90%) !important;
} }
.skeleton-loader {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 2s ease-in-out infinite;
border-radius: 4px;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style> </style>

View File

@@ -1,329 +1,322 @@
<template> <template>
<div class="pr-4"> <form @submit.prevent="handleSendEmail">
<form @submit.prevent="handleSendEmail"> <!-- Receiver Section -->
<!-- Receiver Section --> <div class="field">
<div class="field"> <label class="label">
<label class="label"> <span class="icon-text">
<span class="icon-text"> <span>Người nhận {{ selectedName ? `(Đang mở: ${selectedName})` : "" }}<b class="has-text-danger">*</b></span>
<span </span>
>Người nhận {{ selectedName ? `(Đang mở: ${selectedName})` : "" </label>
}}<b class="ml-1 has-text-danger">*</b></span <div class="field has-addons">
> <div class="control is-expanded">
</span>
</label>
<div class="field is-flex">
<div class="control is-expanded pr-3 is-flex-grow-1">
<input
ref="emailTextareaRef"
class="input"
name="receiver"
id="receiver"
style="height: 100%"
v-model="formData.content.receiver"
placeholder="Nhập địa chỉ email người nhận, ngăn cách bằng dấu chấm phẩy (;)"
@input="handleInputChange('receiver', $event)"
/>
</div>
<div class="control">
<div class="buttons is-flex is-flex-wrap-nowrap">
<button
v-if="$getEditRights()"
type="button"
class="button is-light"
@click="handleOpenModal('save-list')"
style="border-radius: 8px; width: 100%"
>
<span>Lưu</span>
</button>
<button
type="button"
class="button is-info"
@click="handleOpenModal('open-list')"
style="border-radius: 8px; width: 100%"
>
<span>Mở</span>
</button>
</div>
</div>
</div>
</div>
<!-- Subject Section -->
<div class="field">
<label class="label">
<span class="icon-text">
<span>Tiêu đề<b class="ml-1 has-text-danger">*</b></span>
</span>
</label>
<div class="control">
<input <input
type="text" ref="emailTextareaRef"
id="subject"
name="subject"
class="input" class="input"
v-model="formData.content.subject" name="receiver"
placeholder="Nhập tiêu đề email..." id="receiver"
required v-model="formData.content.receiver"
@input="handleInputChange('subject', $event)" @input="handleInputChange('receiver', $event)"
placeholder="Nhập địa chỉ email người nhận, ngăn cách bằng dấu chấm phẩy (;)"
title="Nhập địa chỉ email người nhận, ngăn cách bằng dấu chấm phẩy (;)"
/> />
</div> </div>
</div>
<!-- Content Section -->
<div class="field">
<label class="label">
<span class="icon-text">
<span>Nội dung</span>
</span>
</label>
<div class="control"> <div class="control">
<div <button
class="box" v-if="$getEditRights()"
style="padding: 0" @click="handleOpenModal('save-list')"
class="button is-primary is-light"
type="button"
> >
<Editor <span class="icon">
:key="quillKey" <Icon
:text="formData.content.content" name="material-symbols:save-outline-rounded"
pagename="pagedataemail" :size="20"
:height="'300px'" />
@content="handleQuillChange" </span>
/> </button>
</div> <span class="tooltiptext left-auto right-0">Lưu</span>
</div> </div>
</div>
<!-- Image URL Section -->
<div class="field">
<label class="label">
<span class="icon-text">
<span>Đường dẫn hình ảnh</span>
</span>
</label>
<div class="control"> <div class="control">
<input <button
type="url" @click="handleOpenModal('open-list')"
id="imageUrl" class="button is-info is-light"
name="imageUrl" type="button"
class="input" >
placeholder="https://hinhanh.com/image.jpg" <span class="icon">
@input="handleInputChange('imageUrl', $event)" <Icon
/> name="material-symbols:list-alt-outline-rounded"
:size="20"
/>
</span>
</button>
<span class="tooltiptext left-auto right-0">Xem danh sách </span>
</div> </div>
</div> </div>
</div>
<!-- Link URLs Section --> <!-- Subject Section -->
<div class="field"> <div class="field">
<div class="level"> <label class="label">
<div class="level-left"> <span class="icon-text">
<div class="level-item"> <span>Tiêu đề <b class="has-text-danger">*</b></span>
<label class="label mb-0"> </span>
<span class="icon-text"> </label>
<span>Đường dẫn liên kết</span> <div class="control">
</span> <input
</label> type="text"
</div> id="subject"
</div> name="subject"
<div class="level-right"> class="input"
<div class="level-item"> v-model="formData.content.subject"
<button placeholder="Nhập tiêu đề email"
type="button" required
@click="addLinkUrl" @input="handleInputChange('subject', $event)"
class="button is-normal" />
> </div>
<span class="icon"> </div>
<Icon name="material-symbols:add-rounded" />
</span> <!-- Content Section -->
<span>Thêm liên kết</span> <div class="field">
</button> <label class="label">
</div> <span class="icon-text">
<span>Nội dung</span>
</span>
</label>
<div class="control">
<Editor
:key="quillKey"
:text="formData.content.content"
style="height: 300px"
@content="handleQuillChange"
/>
</div>
</div>
<!-- Image URL Section -->
<div class="field">
<label class="label">
<span class="icon-text">
<span>Đường dẫn hình ảnh</span>
</span>
</label>
<div class="control">
<input
type="url"
id="imageUrl"
name="imageUrl"
class="input"
placeholder="https://hinhanh.com/image.jpg"
@input="handleInputChange('imageUrl', $event)"
/>
</div>
</div>
<!-- Link URLs Section -->
<div class="field">
<div class="level">
<div class="level-left">
<div class="level-item">
<label class="label mb-0">
<span class="icon-text">
<span>Đường dẫn liên kết</span>
</span>
</label>
</div> </div>
</div> </div>
<div <div class="level-right">
v-for="(link, index) in formData.content.linkUrl" <div class="level-item">
:key="index"
class="field columns has-addons mb-2"
:class="`${formData.content.linkUrl.length > 1 ? 'pr-3' : ''}`"
>
<div class="control column is-6 pr-0 is-expanded">
<input
type="url"
class="input"
style="border-radius: 8px"
v-model="formData.content.linkUrl[index]"
:placeholder="`Liên kết ${index + 1}: https://example.com`"
@input="props.onDataChange(formData)"
/>
</div>
<div class="control column is-6 is-expanded">
<input
type="text"
class="input"
style="border-radius: 8px"
v-model="formData.content.textLinkUrl[index]"
:placeholder="`Tên ${index + 1}`"
@input="props.onDataChange(formData)"
/>
</div>
<div
class="control column is-1 p-0"
v-if="formData.content.linkUrl.length > 1"
style="align-self: center; width: 40px"
>
<button <button
@click="addLinkUrl"
class="button is-primary is-light"
type="button" type="button"
@click="removeLinkUrl(index)"
class="button is-danger has-text-white"
style="border-radius: 8px"
> >
<span class="icon"> <span class="icon">
<Icon name="material-symbols:delete-outline-rounded" /> <Icon
name="material-symbols:add-rounded"
:size="18"
/>
</span> </span>
<span>Thêm liên kết</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Keywords Section -->
<div <div
v-if="detectedKeywords.length > 0" v-for="(link, index) in formData.content.linkUrl"
class="field" :key="index"
class="field columns has-addons mb-2"
:class="`${formData.content.linkUrl.length > 1 ? 'pr-3' : ''}`"
> >
<label class="label"> <div class="control column is-6 pr-0 is-expanded">
<span class="icon-text"> <input
<span>Từ khóa</span> type="url"
</span> class="input"
</label> style="border-radius: 8px"
<div class="box"> v-model="formData.content.linkUrl[index]"
<div :placeholder="`Liên kết ${index + 1}: https://example.com`"
v-for="(keyword, index) in detectedKeywords" @input="props.onDataChange(formData)"
:key="index" />
class="field has-addons mb-2 columns"
>
<div class="control column is-4 pr-0 pb-0">
<span
style="width: 100%"
class="button is-static is-small"
>{{ keyword }}</span
>
</div>
<div class="control is-expanded column is-8 pb-0">
<input
type="text"
class="input is-small"
:value="getKeywordValue(keyword)"
:placeholder="`Nhập giá trị cho ${keyword}`"
@input="handleKeywordValueChange(keyword, ($event.target as HTMLInputElement).value)"
/>
</div>
</div>
</div> </div>
</div> <div class="control column is-6 is-expanded">
<input
<!-- Submit Button --> type="text"
<div class="field"> class="input"
<div class="control"> style="border-radius: 8px"
v-model="formData.content.textLinkUrl[index]"
:placeholder="`Tên ${index + 1}`"
@input="props.onDataChange(formData)"
/>
</div>
<div
class="control column is-1 p-0"
v-if="formData.content.linkUrl.length > 1"
style="align-self: center; width: 40px"
>
<button <button
class="button is-primary has-text-white" type="button"
:class="{ 'is-loading': loading }" @click="removeLinkUrl(index)"
:disabled="loading" class="button is-danger has-text-white"
style="border-radius: 8px"
> >
<span v-if="loading">Đang gửi</span> <span class="icon">
<template v-else> <Icon name="material-symbols:delete-outline-rounded" />
<span class="icon"> </span>
<Icon name="material-symbols:send-rounded" />
</span>
<span>Gửi email</span>
</template>
</button> </button>
</div> </div>
</div> </div>
</form> </div>
</div>
<!-- Keywords Section -->
<div
v-if="detectedKeywords.length > 0"
class="field"
>
<label class="label">
<span class="icon-text">
<span>Từ khóa</span>
</span>
</label>
<div class="box">
<div
v-for="(keyword, index) in detectedKeywords"
:key="index"
class="field has-addons mb-2 columns"
>
<div class="control column is-4 pr-0 pb-0">
<span
style="width: 100%"
class="button is-static is-small"
>{{ keyword }}</span
>
</div>
<div class="control is-expanded column is-8 pb-0">
<input
type="text"
class="input is-small"
:value="getKeywordValue(keyword)"
:placeholder="`Nhập giá trị cho ${keyword}`"
@input="handleKeywordValueChange(keyword, $event.target.value)"
/>
</div>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="field">
<div class="control is-flex is-justify-content-flex-end">
<button :class="['button is-primary has-text-white', loading && 'is-loading']">
<span v-if="loading">Đang gửi</span>
<template v-else>
<span class="icon">
<Icon name="material-symbols:send-rounded" />
</span>
<span>Gửi email</span>
</template>
</button>
</div>
</div>
</form>
<Modal <Modal
@dataevent="handleRowClick" @dataevent="handleRowClick"
@close="showmodal = undefined" @close="showmodal = undefined"
v-bind="showmodal" v-bind="showmodal"
v-if="showmodal" v-if="showmodal"
></Modal> />
</template> </template>
<script setup lang="ts"> <script setup>
import { render } from "@vue-email/render"; import { render } from "@vue-email/render";
import Modal from "~/components/Modal.vue";
import Template1 from "~/components/marketing/email/Template1.vue"; import Template1 from "~/components/marketing/email/Template1.vue";
import Editor from "~/components/common/Editor.vue"; import Editor from "~/components/common/Editor.vue";
const nuxtApp = useNuxtApp(); const { $getdata, $getEditRights, $snackbar } = useNuxtApp();
const $snackbar = nuxtApp.$snackbar as (message?: string) => void;
const $getpath = nuxtApp.$getpath as (message?: string) => void;
const $getEditRights = nuxtApp.$getEditRights as () => boolean;
const apiUrl = `${$getpath()}data`;
const showmodal = ref(); const showmodal = ref();
// Types // Types
interface KeywordItem { // interface KeywordItem {
keyword: string; // keyword: string;
value: string; // value: string;
} // }
interface FormContent { // interface FormContent {
receiver: string; // receiver: string;
subject: string; // subject: string;
content: string; // content: string;
imageUrl: string | null; // imageUrl: string | null;
linkUrl: string[]; // linkUrl: string[];
textLinkUrl: string[]; // textLinkUrl: string[];
keyword: KeywordItem[]; // keyword: KeywordItem[];
html: string; // html: string;
} // }
interface FormData { // interface FormData {
id?: number; // id?: number;
name: string; // name: string;
template: string; // template: string;
content: FormContent; // content: FormContent;
} // }
type ModalType = "none" | "save-list" | "open-list"; // type ModalType = "none" | "save-list" | "open-list";
interface Props { // interface Props {
onDataChange: (data: FormData) => void; // onDataChange: (data: FormData) => void;
initialData?: FormData; // initialData?: FormData;
} // }
// Props const props = defineProps({
const props = withDefaults(defineProps<Props>(), { onDataChange: Function,
initialData: () => ({ initialData: {
id: undefined, type: Object,
name: "", default: () => ({
template: "template1", id: undefined,
content: { name: "",
receiver: "", template: "template1",
subject: "", content: {
content: "", receiver: "",
imageUrl: null, subject: "",
linkUrl: [""], content: "",
textLinkUrl: [""], imageUrl: null,
keyword: [{ keyword: "", value: "" }], linkUrl: [""],
html: "", textLinkUrl: [""],
}, keyword: [{ keyword: "", value: "" }],
}), html: "",
},
}),
},
}); });
const selectedName = ref(""); const selectedName = ref("");
const selectedId = ref(""); const selectedId = ref("");
// Refs const emailTextareaRef = ref(null);
const emailTextareaRef = ref<HTMLTextAreaElement | null>(null);
const loading = ref(false); const loading = ref(false);
const modalType = ref<ModalType>("none"); const modalType = ref("none");
const formData = ref<FormData>(props.initialData); const formData = ref(props.initialData);
const quillKey = ref(0); const quillKey = ref(0);
const dataEmails = ref([]); const dataEmails = ref([]);
const detectedKeywords = ref<string[]>([]); const detectedKeywords = ref([]);
const handleRowClick = (data: { email: string; name: string; id: string }) => { const handleRowClick = (data /* : { email: string; name: string; id: string } */) => {
formData.value = { formData.value = {
...formData.value, ...formData.value,
content: { content: {
@@ -341,12 +334,12 @@ const handleRowClick = (data: { email: string; name: string; id: string }) => {
}; };
// Methods // Methods
const extractKeywords = (content: string): string[] => { const extractKeywords = (content) => {
if (!content) return []; if (!content) return [];
const regex = /\{\{([^}]+)\}\}/g; const regex = /\{\{([^}]+)\}\}/g;
const matches: string[] = []; const matches = [];
let match: RegExpExecArray | null = null; let match = null;
while ((match = regex.exec(content)) !== null) { while ((match = regex.exec(content)) !== null) {
if (match[1]) { if (match[1]) {
@@ -357,14 +350,13 @@ const extractKeywords = (content: string): string[] => {
return [...new Set(matches)]; return [...new Set(matches)];
}; };
const getKeywordValue = (keyword: string): string => { const getKeywordValue = (keyword) => {
const found = formData.value.content.keyword.find((kw) => kw.keyword === keyword); const found = formData.value.content.keyword.find((kw) => kw.keyword === keyword);
return found ? found.value : ""; return found ? found.value : "";
}; };
const handleInputChange = (field: keyof FormContent | "name" | "template", eventOrValue: Event | string) => { const handleInputChange = (field /* : keyof FormContent | "name" | "template" */, eventOrValue) => {
const value = const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
typeof eventOrValue === "string" ? eventOrValue : ((eventOrValue.target as HTMLInputElement)?.value ?? "");
if (field === "receiver" && value.trim() === "") { if (field === "receiver" && value.trim() === "") {
selectedName.value = ""; selectedName.value = "";
} }
@@ -380,7 +372,7 @@ const handleInputChange = (field: keyof FormContent | "name" | "template", event
props.onDataChange(formData.value); props.onDataChange(formData.value);
}; };
const handleQuillChange = (content: string) => { const handleQuillChange = (content) => {
formData.value = { formData.value = {
...formData.value, ...formData.value,
content: { content: {
@@ -391,9 +383,8 @@ const handleQuillChange = (content: string) => {
props.onDataChange(formData.value); props.onDataChange(formData.value);
}; };
const handleLinkUrlChange = (index: number, eventOrValue: Event | string) => { const handleLinkUrlChange = (index, eventOrValue) => {
const value = const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
typeof eventOrValue === "string" ? eventOrValue : ((eventOrValue.target as HTMLInputElement)?.value ?? "");
const newLinkUrl = [...formData.value.content.linkUrl]; const newLinkUrl = [...formData.value.content.linkUrl];
newLinkUrl[index] = value; newLinkUrl[index] = value;
formData.value = { formData.value = {
@@ -403,9 +394,8 @@ const handleLinkUrlChange = (index: number, eventOrValue: Event | string) => {
props.onDataChange(formData.value); props.onDataChange(formData.value);
}; };
const handleTextLinkUrlChange = (index: number, eventOrValue: Event | string) => { const handleTextLinkUrlChange = (index, eventOrValue) => {
const value = const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
typeof eventOrValue === "string" ? eventOrValue : ((eventOrValue.target as HTMLInputElement)?.value ?? "");
const newTextLinkUrl = [...formData.value.content.textLinkUrl]; const newTextLinkUrl = [...formData.value.content.textLinkUrl];
newTextLinkUrl[index] = value; newTextLinkUrl[index] = value;
formData.value = { formData.value = {
@@ -415,9 +405,8 @@ const handleTextLinkUrlChange = (index: number, eventOrValue: Event | string) =>
props.onDataChange(formData.value); props.onDataChange(formData.value);
}; };
const handleKeywordValueChange = (keyword: string, eventOrValue: Event | string) => { const handleKeywordValueChange = (keyword, eventOrValue) => {
const value = const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
typeof eventOrValue === "string" ? eventOrValue : ((eventOrValue.target as HTMLInputElement)?.value ?? "");
const keywordIndex = formData.value.content.keyword.findIndex((kw) => kw.keyword === keyword); const keywordIndex = formData.value.content.keyword.findIndex((kw) => kw.keyword === keyword);
if (keywordIndex !== -1) { if (keywordIndex !== -1) {
const newKeywords = [...formData.value.content.keyword]; const newKeywords = [...formData.value.content.keyword];
@@ -445,7 +434,7 @@ const addLinkUrl = () => {
props.onDataChange(formData.value); props.onDataChange(formData.value);
}; };
const removeLinkUrl = (index: number) => { const removeLinkUrl = (index) => {
if (formData.value.content.linkUrl.length > 1) { if (formData.value.content.linkUrl.length > 1) {
const newLinkUrl = formData.value.content.linkUrl.filter((_, i) => i !== index); const newLinkUrl = formData.value.content.linkUrl.filter((_, i) => i !== index);
const newTextLinkUrl = formData.value.content.textLinkUrl.filter((_, i) => i !== index); const newTextLinkUrl = formData.value.content.textLinkUrl.filter((_, i) => i !== index);
@@ -563,10 +552,10 @@ const handleSendEmail = async () => {
} }
}; };
const handleOpenModal = async (type: ModalType) => { const handleOpenModal = async (type) => {
modalType.value = type; modalType.value = type;
await getData(); await getEmailList();
console.log(modalType.value);
if (type === "save-list") { if (type === "save-list") {
showmodal.value = { showmodal.value = {
component: "marketing/email/dataGmail/SaveListGmail", component: "marketing/email/dataGmail/SaveListGmail",
@@ -584,7 +573,7 @@ const handleOpenModal = async (type: ModalType) => {
showmodal.value = { showmodal.value = {
component: "marketing/email/dataGmail/OpenListGmail", component: "marketing/email/dataGmail/OpenListGmail",
title: "Danh sách email", title: "Danh sách email",
width: "95%", width: "min(700px, 75%)",
height: "auto", height: "auto",
vbind: { vbind: {
dataEmail: dataEmails.value, dataEmail: dataEmails.value,
@@ -600,24 +589,13 @@ const handleCloseModal = () => {
showmodal.value = undefined; showmodal.value = undefined;
}; };
const getData = async () => { async function getEmailList() {
loading.value = true; loading.value = true;
const emailListUrl = `${apiUrl}/Email_List/`; dataEmails.value = await $getdata("Email_List");
loading.value = false;
}
try { const handleEditList = (id, name, emails) => {
const response = await $fetch(emailListUrl);
dataEmails.value = response.rows;
console.log(response);
console.log(dataEmails.value);
} catch (error) {
console.error("Error fetching email list:", error);
throw error;
} finally {
loading.value = false;
}
};
const handleEditList = (id: string, name: string, emails: string) => {
modalType.value = "save-list"; modalType.value = "save-list";
}; };

View File

@@ -116,8 +116,8 @@ function doChange() {
function getFiles(files) { function getFiles(files) {
emit("files", files); emit("files", files);
// setTimeout(() => { setTimeout(() => {
// showmodal.value = undefined; showmodal.value = undefined;
// }, 3000); }, 3000);
} }
</script> </script>

View File

@@ -42,8 +42,6 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import Modal from "~/components/Modal.vue";
const { $getpath } = useNuxtApp(); const { $getpath } = useNuxtApp();
const showModal = ref(); const showModal = ref();

View File

@@ -1,6 +1,4 @@
<script setup> <script setup>
import Modal from "~/components/Modal.vue";
const props = defineProps({ const props = defineProps({
variant: Object, variant: Object,
}); });

View File

@@ -4,7 +4,6 @@ import CartItem from "~/components/pos/CartItem.vue";
import CartTabs from "~/components/pos/CartTabs.vue"; import CartTabs from "~/components/pos/CartTabs.vue";
import useActiveCart from "~/components/pos/composables/useActiveCart"; import useActiveCart from "~/components/pos/composables/useActiveCart";
import useOrderInfo from "~/components/pos/composables/useOrderInfo"; import useOrderInfo from "~/components/pos/composables/useOrderInfo";
import SearchBox from "~/components/SearchBox.vue";
const store = useStore(); const store = useStore();
const { $insertapi } = useNuxtApp(); const { $insertapi } = useNuxtApp();

View File

@@ -20,11 +20,8 @@
</template> </template>
<script setup> <script setup>
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import { throttle } from "es-toolkit"; import { throttle } from "es-toolkit";
import SnackBar from "~/components/snackbar/SnackBar.vue"; import SnackBar from "~/components/snackbar/SnackBar.vue";
import Modal from "~/components/Modal.vue";
const route = useRoute(); const route = useRoute();
const { $getdata, $requestLogin, $store } = useNuxtApp(); const { $getdata, $requestLogin, $store } = useNuxtApp();

View File

@@ -451,6 +451,18 @@ export default /** @type {const} */ ([
url_detail: "data-detail/Email_Template/", url_detail: "data-detail/Email_Template/",
params: {}, params: {},
}, },
{
name: "Email_List",
url: "data/Email_List/",
url_detail: "data-detail/Email_List/",
params: {},
},
{
name: "Email_Job",
url: "data/Email_Job/",
url_detail: "data-detail/Email_Job/",
params: {},
},
{ {
name: "issuedplace", name: "issuedplace",
url: "data/Issued_Place/", url: "data/Issued_Place/",