changes
This commit is contained in:
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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} {
|
||||||
|
|||||||
@@ -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,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Modal from "~/components/Modal.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
product: Object,
|
product: Object,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Modal from "~/components/Modal.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
variant: Object,
|
variant: Object,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Modal from "~/components/Modal.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
variant: Object,
|
variant: Object,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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/",
|
||||||
|
|||||||
Reference in New Issue
Block a user