changes
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
**/*.min.js
|
||||||
|
my-bulma-project.css
|
||||||
|
my-bulma-project.css.map
|
||||||
24
.prettierrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"objectWrap": "preserve",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"semi": true,
|
||||||
|
"experimentalOperatorPosition": "end",
|
||||||
|
"experimentalTernaries": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleAttributePerLine": true,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"insertPragma": false,
|
||||||
|
"printWidth": 120,
|
||||||
|
"requirePragma": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"embeddedLanguageFormatting": "auto"
|
||||||
|
}
|
||||||
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
105
app/components/LogIn.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(["close"]);
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const userCreds = {
|
||||||
|
email: "user@gmail.com",
|
||||||
|
password: "99171123",
|
||||||
|
};
|
||||||
|
const adminCreds = {
|
||||||
|
email: "admin@gmail.com",
|
||||||
|
password: "1234567890",
|
||||||
|
};
|
||||||
|
|
||||||
|
const formValues = ref(adminCreds);
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
if (formValues.value.email.includes("admin")) store.login = adminRec;
|
||||||
|
else store.login = userRec;
|
||||||
|
window.location.assign("/");
|
||||||
|
}
|
||||||
|
const adminRec = {
|
||||||
|
id: 1,
|
||||||
|
avatar: null,
|
||||||
|
username: "admin",
|
||||||
|
fullname: "Admin",
|
||||||
|
type: 1,
|
||||||
|
type__code: "admin",
|
||||||
|
type__name: "Quản trị hệ thống",
|
||||||
|
is_admin: true,
|
||||||
|
token: "3j3ki2rvy",
|
||||||
|
};
|
||||||
|
const userRec = {
|
||||||
|
id: 2,
|
||||||
|
avatar: null,
|
||||||
|
username: "user",
|
||||||
|
fullname: "User",
|
||||||
|
type: 2,
|
||||||
|
type__code: "user",
|
||||||
|
type__name: "Nhân viên",
|
||||||
|
is_admin: false,
|
||||||
|
token: "b2z0as13x",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="buttons is-centered is-flex-wrap-nowrap pb-3">
|
||||||
|
<button
|
||||||
|
@click="formValues = userCreds"
|
||||||
|
class="button fs-14 is-fullwidth"
|
||||||
|
:class="formValues.email === userCreds.email && 'is-primary is-light has-text-weight-bold'"
|
||||||
|
>
|
||||||
|
Nhân viên
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="formValues = adminCreds"
|
||||||
|
class="button fs-14 is-fullwidth"
|
||||||
|
:class="formValues.email === adminCreds.email && 'is-primary is-light has-text-weight-bold'"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form>
|
||||||
|
<div class="field">
|
||||||
|
<p class="control has-icons-left has-icons-right">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
v-model="formValues.email"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<SvgIcon v-bind="{ name: 'email.svg', type: 'grey', size: 18 }" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<p class="control has-icons-left">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
placeholder="Mật khẩu"
|
||||||
|
v-model="formValues.password"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<SvgIcon v-bind="{ name: 'password.svg', type: 'grey', size: 18 }" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field is-grouped is-grouped-centered">
|
||||||
|
<p class="control">
|
||||||
|
<button
|
||||||
|
@click="login"
|
||||||
|
type="button"
|
||||||
|
class="button is-success"
|
||||||
|
>
|
||||||
|
Đăng nhập
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="navbar is-fixed-top has-shadow px-3" role="navigation">
|
<nav
|
||||||
|
class="navbar is-fixed-top has-shadow px-3"
|
||||||
|
role="navigation"
|
||||||
|
>
|
||||||
<div class="navbar-brand mr-5">
|
<div class="navbar-brand mr-5">
|
||||||
<span class="navbar-item">
|
<span class="navbar-item">
|
||||||
<SvgIcon v-bind="{ name: 'dot.svg', size: 18, type: 'primary' }" />
|
<SvgIcon v-bind="{ name: 'dot.svg', size: 18, type: 'primary' }" />
|
||||||
<span class="fsb-20 has-text-primary">HRM</span>
|
<span class="fsb-18 has-text-primary">HRM</span>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
@@ -20,19 +23,39 @@
|
|||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-menu" id="navMenu">
|
<div
|
||||||
<div class="navbar-start" style="min-width: 650px">
|
class="navbar-menu"
|
||||||
<template v-for="(v, i) in leftmenu" :key="i" :id="v.code">
|
id="navMenu"
|
||||||
<a class="navbar-item px-2" v-if="!v.submenu" @click="changeTab(v)">
|
>
|
||||||
|
<div
|
||||||
|
class="navbar-start"
|
||||||
|
style="min-width: 650px"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(v, i) in leftmenu"
|
||||||
|
:key="i"
|
||||||
|
:id="v.code"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="navbar-item px-2"
|
||||||
|
v-if="!v.submenu"
|
||||||
|
@click="changeTab(v)"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
:class="`fs-15 ${currentTab.code === v.code ? 'activetab' : ''}`"
|
:class="`fs-15 ${currentTab.code === v.code ? 'activetab' : ''}`"
|
||||||
style="font-weight: 600; padding: 3px 4px"
|
style="font-weight: 500; padding: 3px 5px"
|
||||||
>
|
>
|
||||||
{{ v[lang] }}
|
{{ v[lang] }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable" v-else>
|
<div
|
||||||
<a class="navbar-item px-2" @click="changeTab(v)">
|
class="navbar-item has-dropdown is-hoverable"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="navbar-item px-2"
|
||||||
|
@click="changeTab(v)"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
:class="`icon-text ${currentTab.code === v.code ? 'activetab' : ''}`"
|
:class="`icon-text ${currentTab.code === v.code ? 'activetab' : ''}`"
|
||||||
style="padding: 3px 4px"
|
style="padding: 3px 4px"
|
||||||
@@ -62,144 +85,88 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<a class="navbar-item" @click="changeTab(tabConfig)" v-if="tabConfig">
|
<a
|
||||||
<SvgIcon
|
v-if="!$store.login"
|
||||||
v-bind="{ name: 'configuration.svg', type: 'findata', size: 24 }"
|
@click="openLogInModal"
|
||||||
></SvgIcon>
|
class="navbar-item fs-14 px-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="px-2 py-1"
|
||||||
|
style="border-radius: 6px; border: 1px solid #204853"
|
||||||
|
>Đăng nhập</span
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" @click="openProfile()" v-if="avatar">
|
<a
|
||||||
|
class="navbar-item"
|
||||||
|
@click="changeTab(tabConfig)"
|
||||||
|
v-if="tabConfig"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'configuration.svg', type: 'findata', size: 24 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="navbar-item"
|
||||||
|
@click="openProfile()"
|
||||||
|
v-if="avatar && $store.login"
|
||||||
|
>
|
||||||
<Avatarbox v-bind="avatar"></Avatarbox>
|
<Avatarbox v-bind="avatar"></Avatarbox>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal
|
||||||
|
v-if="showModal"
|
||||||
|
v-bind="showModal"
|
||||||
|
@close="showModal = undefined"
|
||||||
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { watch } from 'vue';
|
import { watch } from "vue";
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const emit = defineEmits(['changetab', 'langChanged']);
|
const emit = defineEmits(["changetab", "langChanged"]);
|
||||||
const { $find, $filter, $findIndex, $store } = useNuxtApp();
|
const { $find, $filter, $findIndex, $store } = useNuxtApp();
|
||||||
const lang = ref($store.lang);
|
const lang = ref($store.lang);
|
||||||
// let menu = $filter($store.common, { category: 'topmenu' });
|
|
||||||
// console.log('menu', menu);
|
|
||||||
const menu = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
category: 'topmenu',
|
|
||||||
classify: 'left',
|
|
||||||
code: 'dashboard',
|
|
||||||
vi: 'Dashboard',
|
|
||||||
link: null,
|
|
||||||
detail: {
|
|
||||||
base: 'Dashboard',
|
|
||||||
component: 'DashboardMaster',
|
|
||||||
},
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
category: 'topmenu',
|
|
||||||
classify: 'left',
|
|
||||||
code: 'hrm',
|
|
||||||
vi: 'Quản lý nhân sự',
|
|
||||||
link: null,
|
|
||||||
detail: {
|
|
||||||
base: 'HRM',
|
|
||||||
component: 'HRMMaster',
|
|
||||||
},
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
category: 'topmenu',
|
|
||||||
classify: 'left',
|
|
||||||
code: 'rollcall',
|
|
||||||
vi: 'Chấm công',
|
|
||||||
link: null,
|
|
||||||
detail: {
|
|
||||||
base: 'Rollcall',
|
|
||||||
component: 'RollcallMaster',
|
|
||||||
},
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
category: 'topmenu',
|
|
||||||
classify: 'left',
|
|
||||||
code: 'salary',
|
|
||||||
vi: 'Tính lương',
|
|
||||||
link: null,
|
|
||||||
detail: {
|
|
||||||
base: 'Salary',
|
|
||||||
component: 'SalaryMaster',
|
|
||||||
},
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
category: 'topmenu',
|
|
||||||
classify: 'left',
|
|
||||||
code: 'kpi_efficiency',
|
|
||||||
vi: 'KPI & Hiệu suất',
|
|
||||||
link: null,
|
|
||||||
detail: {
|
|
||||||
base: 'Kpi',
|
|
||||||
component: 'KpiMaster',
|
|
||||||
},
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
category: 'topmenu',
|
|
||||||
classify: 'left',
|
|
||||||
code: 'employee',
|
|
||||||
vi: 'Cổng nhân viên',
|
|
||||||
link: null,
|
|
||||||
detail: {
|
|
||||||
base: 'Employee',
|
|
||||||
component: 'EmployeeMaster',
|
|
||||||
},
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if ($store.rights.length > 0) {
|
|
||||||
menu = menu.filter((v) => $findIndex($store.rights, { setting: v.id }) >= 0);
|
|
||||||
}
|
|
||||||
// if (menu.length === 0) {
|
|
||||||
// $snackbar(
|
|
||||||
// $store.lang === 'vi'
|
|
||||||
// ? 'Bạn không có quyền truy cập'
|
|
||||||
// : 'You do not have permission to access.',
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// menu.map((v) => {
|
|
||||||
// let arr = $filter($store.common, { category: 'submenu', classify: v.code });
|
|
||||||
// if ($store.rights.length > 0) {
|
// if ($store.rights.length > 0) {
|
||||||
// arr = arr.filter((x) => $findIndex($store.rights, { setting: x.id }) >= 0);
|
// menu = menu.filter((v) => $findIndex($store.rights, { setting: v.id }) >= 0);
|
||||||
// }
|
// }
|
||||||
// v.submenu = arr.length > 0 ? arr : null;
|
|
||||||
// });
|
const showModal = ref();
|
||||||
const leftmenu = $filter(menu, { category: 'topmenu', classify: 'left' });
|
function openLogInModal() {
|
||||||
let currentTab = ref(leftmenu.length > 0 ? leftmenu[0] : undefined);
|
showModal.value = {
|
||||||
|
component: "LogIn",
|
||||||
|
width: "400px",
|
||||||
|
height: "176px",
|
||||||
|
title: "Đăng nhập",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let leftmenu = ref(
|
||||||
|
!$store.login
|
||||||
|
? $store.menu.slice(0, -1)
|
||||||
|
: $store.login.fullname === "User"
|
||||||
|
? $store.menu.filter((m) => m.view)
|
||||||
|
: $store.menu,
|
||||||
|
);
|
||||||
|
let currentTab = ref(leftmenu.value.length > 0 ? leftmenu.value[0] : undefined);
|
||||||
const subTab = ref();
|
const subTab = ref();
|
||||||
const tabConfig = $find(menu, { code: 'configuration' });
|
const tabConfig = $find($store.menu, { code: "configuration" });
|
||||||
const avatar = ref();
|
const avatar = ref();
|
||||||
const isAdmin = ref();
|
const isAdmin = ref();
|
||||||
const handleClick = function () {
|
const handleClick = function () {
|
||||||
const target = document.getElementById('burger');
|
const target = document.getElementById("burger");
|
||||||
target.classList.toggle('is-active');
|
target.classList.toggle("is-active");
|
||||||
const target1 = document.getElementById('navMenu');
|
const target1 = document.getElementById("navMenu");
|
||||||
target1.classList.toggle('is-active');
|
target1.classList.toggle("is-active");
|
||||||
};
|
};
|
||||||
const closeMenu = function () {
|
const closeMenu = function () {
|
||||||
if (!document) return;
|
if (!document) return;
|
||||||
const target = document.getElementById('burger');
|
const target = document.getElementById("burger");
|
||||||
const target1 = document.getElementById('navMenu');
|
const target1 = document.getElementById("navMenu");
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
if (target.classList.contains('is-active')) {
|
if (target.classList.contains("is-active")) {
|
||||||
target.classList.remove('is-active');
|
target.classList.remove("is-active");
|
||||||
target1.classList.remove('is-active');
|
target1.classList.remove("is-active");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function changeTab(tab, subtab) {
|
function changeTab(tab, subtab) {
|
||||||
@@ -208,35 +175,31 @@ function changeTab(tab, subtab) {
|
|||||||
}
|
}
|
||||||
currentTab.value = tab;
|
currentTab.value = tab;
|
||||||
subTab.value = subtab;
|
subTab.value = subtab;
|
||||||
emit('changetab', tab, subtab);
|
emit("changetab", tab, subtab);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
let query = subtab
|
let query = subtab ? { tab: tab.code, subtab: subtab.code } : { tab: tab.code };
|
||||||
? { tab: tab.code, subtab: subtab.code }
|
|
||||||
: { tab: tab.code };
|
|
||||||
router.push({ query: query });
|
router.push({ query: query });
|
||||||
}
|
}
|
||||||
function openProfile() {
|
function openProfile() {
|
||||||
let modal = {
|
let modal = {
|
||||||
component: 'user/Profile',
|
component: "user/Profile",
|
||||||
width: '1100px',
|
width: "1100px",
|
||||||
height: '360px',
|
height: "360px",
|
||||||
title: $store.lang === 'vi' ? 'Thông tin cá nhân' : '"User profile"',
|
title: $store.lang === "vi" ? "Thông tin cá nhân" : '"User profile"',
|
||||||
};
|
};
|
||||||
$store.commit('showmodal', modal);
|
$store.commit("showmodal", modal);
|
||||||
}
|
}
|
||||||
let found = route.query.tab
|
let found = route.query.tab ? $find($store.menu, { code: route.query.tab }) : undefined;
|
||||||
? $find(menu, { code: route.query.tab })
|
|
||||||
: undefined;
|
|
||||||
if (found || currentTab.value) changeTab(found || currentTab.value);
|
if (found || currentTab.value) changeTab(found || currentTab.value);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!$store.login) return;
|
if (!$store.login) return;
|
||||||
avatar.value = {
|
avatar.value = {
|
||||||
image: null,
|
image: null,
|
||||||
text: $store.login.fullname.substring(0, 1).toUpperCase(),
|
text: $store.login.fullname.substring(0, 1).toUpperCase(),
|
||||||
size: 'two',
|
size: "two",
|
||||||
type: 'findata',
|
type: "findata",
|
||||||
};
|
};
|
||||||
isAdmin.value = $store.login.type__code === 'admin';
|
isAdmin.value = $store.login.type__code === "admin";
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
() => $store.login,
|
() => $store.login,
|
||||||
@@ -245,10 +208,10 @@ watch(
|
|||||||
avatar.value = {
|
avatar.value = {
|
||||||
image: null,
|
image: null,
|
||||||
text: $store.login.fullname.substring(0, 1).toUpperCase(),
|
text: $store.login.fullname.substring(0, 1).toUpperCase(),
|
||||||
size: 'two',
|
size: "two",
|
||||||
type: 'findata',
|
type: "findata",
|
||||||
};
|
};
|
||||||
isAdmin.value = $store.login.type__code === 'admin';
|
isAdmin.value = $store.login.type__code === "admin";
|
||||||
lang.value = $store.lang;
|
lang.value = $store.lang;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
@click="$emit('justclick')"
|
@click="$emit('justclick')"
|
||||||
:style="image ? 'border: none' : ''"
|
:style="image ? 'border: none' : ''"
|
||||||
>
|
>
|
||||||
<figure class="image" v-if="image">
|
<figure
|
||||||
<img class="is-rounded" :src="`${$path()}download?name=${image}`" />
|
class="image"
|
||||||
|
v-if="image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="is-rounded"
|
||||||
|
:src="`${$path()}download?name=${image}`"
|
||||||
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span>{{ text }}</span>
|
<span>{{ text }}</span>
|
||||||
@@ -14,19 +20,18 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['text', 'image', 'type', 'size'],
|
props: ["text", "image", "type", "size"],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.cbox-findata-two {
|
.cbox-findata-two {
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: bold;
|
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1px solid #e4e4e4;
|
border: 1px solid #d3d3d3;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,393 +1,3 @@
|
|||||||
<template>
|
<template>hey</template>
|
||||||
<ClientOnly>
|
|
||||||
<div ref="el"></div>
|
|
||||||
</ClientOnly>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup></script>
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
|
||||||
import 'ckeditor5/ckeditor5.css';
|
|
||||||
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';
|
|
||||||
|
|
||||||
const el = ref(null);
|
|
||||||
let editor = null;
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
|
||||||
const content = ref(props.modelValue || '');
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!process.client) return;
|
|
||||||
|
|
||||||
const {
|
|
||||||
ClassicEditor,
|
|
||||||
AccessibilityHelp,
|
|
||||||
Autoformat,
|
|
||||||
AutoImage,
|
|
||||||
AutoLink,
|
|
||||||
Autosave,
|
|
||||||
BalloonToolbar,
|
|
||||||
BlockQuote,
|
|
||||||
BlockToolbar,
|
|
||||||
Bold,
|
|
||||||
CKBox,
|
|
||||||
CKBoxImageEdit,
|
|
||||||
CloudServices,
|
|
||||||
Essentials,
|
|
||||||
FontBackgroundColor,
|
|
||||||
FontColor,
|
|
||||||
FontFamily,
|
|
||||||
FontSize,
|
|
||||||
Heading,
|
|
||||||
Highlight,
|
|
||||||
ImageBlock,
|
|
||||||
ImageCaption,
|
|
||||||
ImageInline,
|
|
||||||
ImageInsert,
|
|
||||||
ImageInsertViaUrl,
|
|
||||||
ImageResize,
|
|
||||||
ImageStyle,
|
|
||||||
ImageTextAlternative,
|
|
||||||
ImageToolbar,
|
|
||||||
ImageUpload,
|
|
||||||
Indent,
|
|
||||||
IndentBlock,
|
|
||||||
Italic,
|
|
||||||
Link,
|
|
||||||
LinkImage,
|
|
||||||
List,
|
|
||||||
ListProperties,
|
|
||||||
MediaEmbed,
|
|
||||||
Mention,
|
|
||||||
PageBreak,
|
|
||||||
Paragraph,
|
|
||||||
PasteFromOffice,
|
|
||||||
PictureEditing,
|
|
||||||
RemoveFormat,
|
|
||||||
SelectAll,
|
|
||||||
SpecialCharacters,
|
|
||||||
SpecialCharactersArrows,
|
|
||||||
SpecialCharactersCurrency,
|
|
||||||
SpecialCharactersEssentials,
|
|
||||||
SpecialCharactersLatin,
|
|
||||||
SpecialCharactersMathematical,
|
|
||||||
SpecialCharactersText,
|
|
||||||
Subscript,
|
|
||||||
Superscript,
|
|
||||||
Table,
|
|
||||||
TableCaption,
|
|
||||||
TableCellProperties,
|
|
||||||
GeneralHtmlSupport,
|
|
||||||
TableColumnResize,
|
|
||||||
TableProperties,
|
|
||||||
TableToolbar,
|
|
||||||
TextTransformation,
|
|
||||||
TodoList,
|
|
||||||
Underline,
|
|
||||||
Alignment,
|
|
||||||
Undo,
|
|
||||||
} = await import('ckeditor5');
|
|
||||||
const { ExportPdf, ExportWord, ImportWord } = await import('ckeditor5-premium-features');
|
|
||||||
|
|
||||||
editor = await ClassicEditor.create(el.value, {
|
|
||||||
// licenseKey: 'GPL',
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
AccessibilityHelp,
|
|
||||||
Autoformat,
|
|
||||||
AutoImage,
|
|
||||||
AutoLink,
|
|
||||||
Autosave,
|
|
||||||
BalloonToolbar,
|
|
||||||
BlockQuote,
|
|
||||||
BlockToolbar,
|
|
||||||
Bold,
|
|
||||||
CKBox,
|
|
||||||
CKBoxImageEdit,
|
|
||||||
CloudServices,
|
|
||||||
Essentials,
|
|
||||||
ExportPdf,
|
|
||||||
ExportWord,
|
|
||||||
FontBackgroundColor,
|
|
||||||
FontColor,
|
|
||||||
FontFamily,
|
|
||||||
FontSize,
|
|
||||||
Heading,
|
|
||||||
Highlight,
|
|
||||||
ImageBlock,
|
|
||||||
ImageCaption,
|
|
||||||
ImageInline,
|
|
||||||
ImageInsert,
|
|
||||||
ImageInsertViaUrl,
|
|
||||||
ImageResize,
|
|
||||||
ImageStyle,
|
|
||||||
ImageTextAlternative,
|
|
||||||
ImageToolbar,
|
|
||||||
ImageUpload,
|
|
||||||
ImportWord,
|
|
||||||
Indent,
|
|
||||||
IndentBlock,
|
|
||||||
Italic,
|
|
||||||
Link,
|
|
||||||
LinkImage,
|
|
||||||
List,
|
|
||||||
ListProperties,
|
|
||||||
MediaEmbed,
|
|
||||||
Mention,
|
|
||||||
PageBreak,
|
|
||||||
Paragraph,
|
|
||||||
PasteFromOffice,
|
|
||||||
PictureEditing,
|
|
||||||
RemoveFormat,
|
|
||||||
SelectAll,
|
|
||||||
SpecialCharacters,
|
|
||||||
SpecialCharactersArrows,
|
|
||||||
SpecialCharactersCurrency,
|
|
||||||
SpecialCharactersEssentials,
|
|
||||||
SpecialCharactersLatin,
|
|
||||||
SpecialCharactersMathematical,
|
|
||||||
SpecialCharactersText,
|
|
||||||
Subscript,
|
|
||||||
Superscript,
|
|
||||||
Table,
|
|
||||||
TableCaption,
|
|
||||||
TableCellProperties,
|
|
||||||
TableColumnResize,
|
|
||||||
TableProperties,
|
|
||||||
TableToolbar,
|
|
||||||
TextTransformation,
|
|
||||||
TodoList,
|
|
||||||
Underline,
|
|
||||||
Alignment,
|
|
||||||
GeneralHtmlSupport,
|
|
||||||
Undo,
|
|
||||||
],
|
|
||||||
|
|
||||||
toolbar: [
|
|
||||||
'undo',
|
|
||||||
'redo',
|
|
||||||
'|',
|
|
||||||
'importWord',
|
|
||||||
'exportWord',
|
|
||||||
'exportPdf',
|
|
||||||
'|',
|
|
||||||
'heading',
|
|
||||||
'|',
|
|
||||||
'fontSize',
|
|
||||||
'fontFamily',
|
|
||||||
'fontColor',
|
|
||||||
'fontBackgroundColor',
|
|
||||||
'|',
|
|
||||||
'bold',
|
|
||||||
'italic',
|
|
||||||
'underline',
|
|
||||||
'subscript',
|
|
||||||
'superscript',
|
|
||||||
'removeFormat',
|
|
||||||
'|',
|
|
||||||
'alignment', // 👈 THÊM
|
|
||||||
'|',
|
|
||||||
'specialCharacters',
|
|
||||||
'pageBreak',
|
|
||||||
'link',
|
|
||||||
'insertImage',
|
|
||||||
'insertImageViaUrl',
|
|
||||||
'ckbox',
|
|
||||||
'mediaEmbed',
|
|
||||||
'insertTable',
|
|
||||||
'highlight',
|
|
||||||
'blockQuote',
|
|
||||||
'|',
|
|
||||||
'bulletedList',
|
|
||||||
'numberedList',
|
|
||||||
'todoList',
|
|
||||||
'outdent',
|
|
||||||
'indent',
|
|
||||||
],
|
|
||||||
exportPdf: {
|
|
||||||
stylesheets: [
|
|
||||||
'https://cdn.ckeditor.com/ckeditor5/43.2.0/ckeditor5.css',
|
|
||||||
'https://cdn.ckeditor.com/ckeditor5-premium-features/43.2.0/ckeditor5-premium-features.css',
|
|
||||||
],
|
|
||||||
fileName: 'export-pdf-demo.pdf',
|
|
||||||
converterOptions: {
|
|
||||||
format: 'Tabloid',
|
|
||||||
margin_top: '20mm',
|
|
||||||
margin_bottom: '20mm',
|
|
||||||
margin_right: '24mm',
|
|
||||||
margin_left: '24mm',
|
|
||||||
page_orientation: 'portrait',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exportWord: {
|
|
||||||
stylesheets: [
|
|
||||||
'https://cdn.ckeditor.com/ckeditor5/43.2.0/ckeditor5.css',
|
|
||||||
'https://cdn.ckeditor.com/ckeditor5-premium-features/43.2.0/ckeditor5-premium-features.css',
|
|
||||||
],
|
|
||||||
fileName: 'export-word-demo.docx',
|
|
||||||
converterOptions: {
|
|
||||||
document: {
|
|
||||||
orientation: 'portrait',
|
|
||||||
size: 'Tabloid',
|
|
||||||
margins: {
|
|
||||||
top: '20mm',
|
|
||||||
bottom: '20mm',
|
|
||||||
right: '24mm',
|
|
||||||
left: '24mm',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
balloonToolbar: ['bold', 'italic','fontFamily','fontSize', '|', 'alignment', 'link', 'insertImage', '|', 'bulletedList', 'numberedList'],
|
|
||||||
blockToolbar: [
|
|
||||||
'fontSize',
|
|
||||||
'fontColor',
|
|
||||||
'fontBackgroundColor',
|
|
||||||
'|',
|
|
||||||
'bold',
|
|
||||||
'italic',
|
|
||||||
'|',
|
|
||||||
'link',
|
|
||||||
'insertImage',
|
|
||||||
'insertTable',
|
|
||||||
'|',
|
|
||||||
'bulletedList',
|
|
||||||
'numberedList',
|
|
||||||
'outdent',
|
|
||||||
'indent',
|
|
||||||
],
|
|
||||||
fontFamily: {
|
|
||||||
supportAllValues: true,
|
|
||||||
},
|
|
||||||
fontSize: {
|
|
||||||
options: [10, 12,13, 14, 'default', 18, 20, 22],
|
|
||||||
supportAllValues: true,
|
|
||||||
},
|
|
||||||
heading: {
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
model: 'paragraph',
|
|
||||||
title: 'Paragraph',
|
|
||||||
class: 'ck-heading_paragraph',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'heading1',
|
|
||||||
view: 'h1',
|
|
||||||
title: 'Heading 1',
|
|
||||||
class: 'ck-heading_heading1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'heading2',
|
|
||||||
view: 'h2',
|
|
||||||
title: 'Heading 2',
|
|
||||||
class: 'ck-heading_heading2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'heading3',
|
|
||||||
view: 'h3',
|
|
||||||
title: 'Heading 3',
|
|
||||||
class: 'ck-heading_heading3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'heading4',
|
|
||||||
view: 'h4',
|
|
||||||
title: 'Heading 4',
|
|
||||||
class: 'ck-heading_heading4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'heading5',
|
|
||||||
view: 'h5',
|
|
||||||
title: 'Heading 5',
|
|
||||||
class: 'ck-heading_heading5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'heading6',
|
|
||||||
view: 'h6',
|
|
||||||
title: 'Heading 6',
|
|
||||||
class: 'ck-heading_heading6',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
toolbar: [
|
|
||||||
'toggleImageCaption',
|
|
||||||
'imageTextAlternative',
|
|
||||||
'|',
|
|
||||||
'imageStyle:inline',
|
|
||||||
'imageStyle:wrapText',
|
|
||||||
'imageStyle:breakText',
|
|
||||||
'|',
|
|
||||||
'resizeImage',
|
|
||||||
'|',
|
|
||||||
'ckboxImageEdit',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
addTargetToExternalLinks: true,
|
|
||||||
defaultProtocol: 'https://',
|
|
||||||
decorators: {
|
|
||||||
toggleDownloadable: {
|
|
||||||
mode: 'manual',
|
|
||||||
label: 'Downloadable',
|
|
||||||
attributes: {
|
|
||||||
download: 'file',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
properties: {
|
|
||||||
styles: true,
|
|
||||||
startIndex: true,
|
|
||||||
reversed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mention: {
|
|
||||||
feeds: [
|
|
||||||
{
|
|
||||||
marker: '@',
|
|
||||||
feed: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
menuBar: {
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties'],
|
|
||||||
},
|
|
||||||
alignment: {
|
|
||||||
options: ['left', 'center', 'right', 'justify'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// set data ban đầu
|
|
||||||
editor.setData(props.modelValue || '');
|
|
||||||
|
|
||||||
// khi thay đổi → emit ra ngoài
|
|
||||||
editor.model.document.on('change:data', () => {
|
|
||||||
const data = editor.getData();
|
|
||||||
emit('update:modelValue', data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(val) => {
|
|
||||||
if (editor && val !== editor.getData()) {
|
|
||||||
editor.setData(val || '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
onBeforeUnmount(async () => {
|
|
||||||
if (editor) {
|
|
||||||
await editor.destroy();
|
|
||||||
editor = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,347 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container"></div>
|
||||||
<div class="level mb-4">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item">
|
|
||||||
<div>
|
|
||||||
<h2 class="title is-4">Sent Emails</h2>
|
|
||||||
<p class="subtitle is-6">
|
|
||||||
{{ loading ? "Loading..." : `${emailsSent.length} email(s) sent` }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="level-right">
|
|
||||||
<div class="level-item">
|
|
||||||
<button
|
|
||||||
class="button is-light"
|
|
||||||
@click="getDataEmailsSent"
|
|
||||||
:disabled="loading"
|
|
||||||
:class="{ 'is-loading': loading }"
|
|
||||||
>
|
|
||||||
<span class="icon">
|
|
||||||
<RefreshCw :size="16" />
|
|
||||||
</span>
|
|
||||||
<span>Refresh</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<table class="table is-fullwidth is-striped is-hoverable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Emails</th>
|
|
||||||
<th>Subject</th>
|
|
||||||
<th>Content</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Sent At</th>
|
|
||||||
<th>Updated At</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template v-if="loading">
|
|
||||||
<tr v-for="index in 5" :key="index">
|
|
||||||
<td><div class="skeleton-line"></div></td>
|
|
||||||
<td><div class="skeleton-line"></div></td>
|
|
||||||
<td><div class="skeleton-line"></div></td>
|
|
||||||
<td><div class="skeleton-line"></div></td>
|
|
||||||
<td><div class="skeleton-line"></div></td>
|
|
||||||
<td><div class="skeleton-line"></div></td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="emailsSent.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="6">
|
|
||||||
<div class="has-text-centered py-6">
|
|
||||||
<Mail :size="64" class="has-text-grey-light mb-4" />
|
|
||||||
<p class="title is-5">No emails sent yet</p>
|
|
||||||
<p class="subtitle is-6">Sent emails will appear here</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<tr v-for="email in emailsSent" :key="email.id" class="is-clickable" @click="handleViewDetail(email)">
|
|
||||||
<td>
|
|
||||||
<span>{{ truncateText(email.receiver, 40) }}</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ truncateText(email.subject, 20) }}</td>
|
|
||||||
<td>
|
|
||||||
<div v-html="truncateText(stripHtml(extractMessageContent(email.content), 1000), 40)"></div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span v-if="email.status === 1" class="tag is-warning">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<SvgIcon v-bind="{ name: 'loading.svg', type: 'white', size: 12 }" />
|
|
||||||
</span>
|
|
||||||
<span>Pending</span>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="email.status === 2" class="tag is-success">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<CheckCircle2 :size="12" />
|
|
||||||
</span>
|
|
||||||
<span>Sent</span>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="email.status === 3" class="tag is-danger">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<XCircle :size="12" />
|
|
||||||
</span>
|
|
||||||
<span>Failed</span>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="email.status === 4" class="tag is-info">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<Clock :size="12" />
|
|
||||||
</span>
|
|
||||||
<span>Scheduled</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>
|
|
||||||
<p class="is-size-7">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<Calendar :size="12" />
|
|
||||||
</span>
|
|
||||||
{{ formatDate(email.create_time) }}
|
|
||||||
</p>
|
|
||||||
<p class="is-size-7 has-text-grey">{{ getRelativeTime(email.create_time) }}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div>
|
|
||||||
<p class="is-size-7">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<Clock :size="12" />
|
|
||||||
</span>
|
|
||||||
{{ formatDate(email.update_time || "") }}
|
|
||||||
</p>
|
|
||||||
<p class="is-size-7 has-text-grey">{{ getRelativeTime(email.update_time || "") || "---" }}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Detail Modal -->
|
|
||||||
<div v-if="showDetailModal && selectedEmail" class="modal is-active">
|
|
||||||
<div class="modal-background" @click="showDetailModal = false"></div>
|
|
||||||
<div class="modal-card" style="width: 90%; max-width: 1200px">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">Email Details</p>
|
|
||||||
<button class="delete" @click="showDetailModal = false"></button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">
|
|
||||||
Recipients ({{ selectedEmail.receiver.split(";").filter((e) => e.trim()).length }})
|
|
||||||
</label>
|
|
||||||
<div class="box" style="max-height: 150px; overflow-y: auto">
|
|
||||||
<p style="white-space: pre-wrap">{{ selectedEmail.receiver }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Subject</label>
|
|
||||||
<p>{{ selectedEmail.subject }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Content</label>
|
|
||||||
<div
|
|
||||||
class="box content"
|
|
||||||
style="max-height: 300px; overflow-y: auto"
|
|
||||||
v-html="selectedEmail.content || 'No content'"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<label class="label">Status</label>
|
|
||||||
<div>
|
|
||||||
<span v-if="selectedEmail.status === 1" class="tag is-warning">Pending</span>
|
|
||||||
<span v-else-if="selectedEmail.status === 2" class="tag is-success">Sent</span>
|
|
||||||
<span v-else-if="selectedEmail.status === 3" class="tag is-danger">Failed</span>
|
|
||||||
<span v-else-if="selectedEmail.status === 4" class="tag is-info">Scheduled</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column">
|
|
||||||
<label class="label">Sent At</label>
|
|
||||||
<p>{{ new Date(selectedEmail.create_time).toLocaleString("vi-VN") }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column">
|
|
||||||
<label class="label">Updated At</label>
|
|
||||||
<p>
|
|
||||||
{{ selectedEmail.update_time ? new Date(selectedEmail.update_time).toLocaleString("vi-VN") : "---" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import { formatDistanceToNow } from "date-fns";
|
|
||||||
import axios from "axios";
|
|
||||||
import { useNuxtApp } from "nuxt/app";
|
|
||||||
import { apiUrl } from '@/components/marketing/email/Email.utils';
|
|
||||||
|
|
||||||
interface EmailSent {
|
|
||||||
id: number;
|
|
||||||
receiver: string;
|
|
||||||
subject: string;
|
|
||||||
content: string;
|
|
||||||
status: number;
|
|
||||||
create_time: string;
|
|
||||||
update_time?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nuxtApp = useNuxtApp();
|
|
||||||
const $snackbar = nuxtApp.$snackbar as (message?: string) => void;
|
|
||||||
|
|
||||||
const emailsSent = ref<EmailSent[]>([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
const selectedEmail = ref<EmailSent | null>(null);
|
|
||||||
const showDetailModal = ref(false);
|
|
||||||
|
|
||||||
const extractMessageContent = (html: string | null | undefined): string => {
|
|
||||||
if (!html || html === "") return "No content";
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(html, "text/html");
|
|
||||||
const messageContentDiv = doc.querySelector("div.message-content");
|
|
||||||
|
|
||||||
if (messageContentDiv) {
|
|
||||||
const content = messageContentDiv.innerHTML.trim();
|
|
||||||
if (content) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
return messageContentDiv.innerHTML;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const regex = /<div[^>]*class=["'][^"']*message-content[^"']*["'][^>]*>([\s\S]*?)<\/div>/i;
|
|
||||||
const match = html.match(regex);
|
|
||||||
|
|
||||||
if (match && match[1]) {
|
|
||||||
return match[1].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error extracting message content:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stripHtml = (html: string, maxLength: number = 1000): string => {
|
|
||||||
if (!html) return "";
|
|
||||||
const text = html.replace(/<[^>]*>/g, "");
|
|
||||||
return text.length > maxLength ? text.substring(0, maxLength) + "..." : text;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDataEmailsSent = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${apiUrl}/Email_Sent/?sort=-id`);
|
|
||||||
if (response.status === 200) {
|
|
||||||
emailsSent.value = response.data.rows || response.data || [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching sent emails:", error);
|
|
||||||
$snackbar("Failed to load sent emails");
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
try {
|
|
||||||
if (dateString === "") return "";
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleString("vi-VN", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return "Invalid date";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRelativeTime = (dateString: string) => {
|
|
||||||
try {
|
|
||||||
if (dateString === "") return "";
|
|
||||||
return formatDistanceToNow(new Date(dateString), { addSuffix: true });
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDetail = (email: EmailSent) => {
|
|
||||||
selectedEmail.value = email;
|
|
||||||
showDetailModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const truncateText = (text: string | undefined | null, maxLength: number = 50): string => {
|
|
||||||
if (!text) return "";
|
|
||||||
const textStr = String(text);
|
|
||||||
if (textStr.length <= maxLength) return textStr;
|
|
||||||
return textStr.substring(0, maxLength) + "...";
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getDataEmailsSent();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.is-clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-clickable:hover {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-line {
|
|
||||||
height: 20px;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: loading 1.5s infinite;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loading {
|
|
||||||
0% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rotating {
|
|
||||||
animation: rotate 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -2,14 +2,28 @@
|
|||||||
<div>
|
<div>
|
||||||
<article class="message is-dark">
|
<article class="message is-dark">
|
||||||
<div class="message-body py-2 mt-5 has-text-dark fs-16">
|
<div class="message-body py-2 mt-5 has-text-dark fs-16">
|
||||||
{{$store.lang==='en'? 'Click the button below to log out of the system.' : 'Nhấn vào nút bên dưới để thoát khỏi hệ thống.'}}
|
{{
|
||||||
|
$store.lang === "en"
|
||||||
|
? "Click the button below to log out of the system."
|
||||||
|
: "Nhấn vào nút bên dưới để thoát khỏi hệ thống."
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<div class="mt-5 pt-3">
|
<div class="mt-5 pt-3">
|
||||||
<button class="button is-primary has-text-white" @click="$requestLogin()">{{$store.lang==='en'? 'Sign out' : 'Đăng xuất'}}</button>
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="logout"
|
||||||
|
>
|
||||||
|
{{ $store.lang === "en" ? "Sign out" : "Đăng xuất" }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
const { $store, $requestLogin } = useNuxtApp()
|
const { $store, $requestLogin } = useNuxtApp();
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
$store.login = undefined;
|
||||||
|
window.location.assign("/");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
65
app/components/viewer/Rights.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup>
|
||||||
|
import { isEqual } from "es-toolkit";
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { $copy, $snackbar } = useNuxtApp();
|
||||||
|
|
||||||
|
const localMenu = ref($copy(store.menu));
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
store.menu = $copy(localMenu.value);
|
||||||
|
localMenu.value = $copy(store.menu);
|
||||||
|
$snackbar("Đã cập nhật phân quyền");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="max-width: 800px; margin-inline: auto">
|
||||||
|
<table class="table is-fullwidth is-hoverable is-bordered fs-14 mb-4">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #e4fff0">
|
||||||
|
<th>
|
||||||
|
<SvgIcon v-bind="{ name: 'visibility.svg', size: 20 }" />
|
||||||
|
</th>
|
||||||
|
<th>Menu</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="m in localMenu"
|
||||||
|
:key="m.id"
|
||||||
|
@click="m.view = !m.view"
|
||||||
|
class="is-clickable"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="localMenu.filter((menu) => menu.view).length === 1 && m.view"
|
||||||
|
v-model="m.view"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
||||||
|
<p>{{ m.vi }}</p>
|
||||||
|
<p class="fs-13 has-text-grey">{{ m.code }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="block">
|
||||||
|
<button
|
||||||
|
:disabled="isEqual(store.menu, localMenu)"
|
||||||
|
@click="save"
|
||||||
|
class="button is-primary"
|
||||||
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<SvgIcon v-bind="{ name: 'save.svg', type: 'white', size: 19 }" />
|
||||||
|
</span>
|
||||||
|
<span>Lưu thay đổi</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -13,15 +13,19 @@
|
|||||||
@close="$store.removeSnackbar()"
|
@close="$store.removeSnackbar()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal v-if="showmodal" v-bind="showmodal" @close="showmodal = undefined" />
|
<Modal
|
||||||
|
v-if="showmodal"
|
||||||
|
v-bind="showmodal"
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from "vue";
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from "vue-router";
|
||||||
import SnackBar from '@/components/snackbar/SnackBar.vue';
|
import SnackBar from "@/components/snackbar/SnackBar.vue";
|
||||||
import Modal from '@/components/Modal.vue';
|
import Modal from "@/components/Modal.vue";
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { $getdata, $store } = useNuxtApp();
|
const { $getdata, $store } = useNuxtApp();
|
||||||
var authorized = ref(false);
|
var authorized = ref(false);
|
||||||
@@ -29,10 +33,7 @@ const snackbar = ref(undefined);
|
|||||||
const showmodal = ref(undefined);
|
const showmodal = ref(undefined);
|
||||||
function getViewport() {
|
function getViewport() {
|
||||||
let viewport;
|
let viewport;
|
||||||
var width =
|
var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||||
window.innerWidth ||
|
|
||||||
document.documentElement.clientWidth ||
|
|
||||||
document.body.clientWidth;
|
|
||||||
if (width <= 768)
|
if (width <= 768)
|
||||||
viewport = 1; //'mobile'
|
viewport = 1; //'mobile'
|
||||||
else if (width >= 769 && width <= 1023)
|
else if (width >= 769 && width <= 1023)
|
||||||
@@ -42,25 +43,24 @@ function getViewport() {
|
|||||||
else if (width >= 1216 && width <= 1407)
|
else if (width >= 1216 && width <= 1407)
|
||||||
viewport = 4; //'widescreen'
|
viewport = 4; //'widescreen'
|
||||||
else if (width >= 1408) viewport = 5; //'fullhd'
|
else if (width >= 1408) viewport = 5; //'fullhd'
|
||||||
$store.commit('viewport', viewport);
|
$store.commit("viewport", viewport);
|
||||||
}
|
}
|
||||||
async function checkRedirect() {
|
async function checkRedirect() {
|
||||||
console.log('checkRedirect');
|
console.info("checkRedirect");
|
||||||
if (route.query.username && route.query.token) {
|
if (route.query.username && route.query.token) {
|
||||||
let row = await $getdata(
|
let row = await $getdata(
|
||||||
'user',
|
"user",
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
filter: { username: route.query.username },
|
filter: { username: route.query.username },
|
||||||
values:
|
values: "id,avatar,username,fullname,type,type__code,type__name,is_admin",
|
||||||
'id,avatar,username,fullname,type,type__code,type__name,is_admin',
|
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if (row === 'error' || row === undefined) return;
|
if (row === "error" || row === undefined) return;
|
||||||
else {
|
else {
|
||||||
row.token = route.query.token;
|
row.token = route.query.token;
|
||||||
$store.commit('login', row);
|
$store.commit("login", row);
|
||||||
}
|
}
|
||||||
} else if (!$store.login) {
|
} else if (!$store.login) {
|
||||||
return;
|
return;
|
||||||
@@ -69,34 +69,14 @@ async function checkRedirect() {
|
|||||||
}
|
}
|
||||||
async function checkLogin() {
|
async function checkLogin() {
|
||||||
if ($store.login ? $store.login.token : false) {
|
if ($store.login ? $store.login.token : false) {
|
||||||
$store.commit(
|
$store.commit("rights", await $getdata("grouprights", { group: $store.login.type }));
|
||||||
'rights',
|
let authtoken = await $getdata("token", { token: $store.login.token }, undefined, true);
|
||||||
await $getdata('grouprights', { group: $store.login.type }),
|
|
||||||
);
|
|
||||||
$store.commit(
|
|
||||||
'dealer',
|
|
||||||
await $getdata(
|
|
||||||
'dealer',
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
filter: { user: $store.login.id },
|
|
||||||
values: 'id,code,name,phone,email,create_time',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let authtoken = await $getdata(
|
|
||||||
'token',
|
|
||||||
{ token: $store.login.token },
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
if (authtoken ? authtoken.expiry : true) return;
|
if (authtoken ? authtoken.expiry : true) return;
|
||||||
authorized.value = true;
|
authorized.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkRedirect();
|
// checkRedirect();
|
||||||
getViewport();
|
getViewport();
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ export default defineNuxtPlugin(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "dealersetting",
|
name: "dealersetting",
|
||||||
url: "data/Biz_Setting/",
|
url: "data/Common/",
|
||||||
url_detail: "data-detail/Biz_Setting/",
|
url_detail: "data-detail/Common/",
|
||||||
params: { sort: "index" },
|
params: { sort: "index" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -250,8 +250,8 @@ export default defineNuxtPlugin(() => {
|
|||||||
{
|
{
|
||||||
name: "common",
|
name: "common",
|
||||||
commit: "common",
|
commit: "common",
|
||||||
url: "data/Biz_Setting/",
|
url: "data/Common/",
|
||||||
url_detail: "data-detail/Biz_Setting/",
|
url_detail: "data-detail/Common/",
|
||||||
params: { sort: "index" },
|
params: { sort: "index" },
|
||||||
},
|
},
|
||||||
{ name: "upload", url: "upload/", params: {} },
|
{ name: "upload", url: "upload/", params: {} },
|
||||||
@@ -302,8 +302,8 @@ export default defineNuxtPlugin(() => {
|
|||||||
{
|
{
|
||||||
name: "bizsetting",
|
name: "bizsetting",
|
||||||
commit: "bizsetting",
|
commit: "bizsetting",
|
||||||
url: "data/Biz_Setting/",
|
url: "data/Common/",
|
||||||
url_detail: "data-detail/Biz_Setting/",
|
url_detail: "data-detail/Common/",
|
||||||
params: {},
|
params: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1285,7 +1285,7 @@ export default defineNuxtPlugin(() => {
|
|||||||
/**
|
/**
|
||||||
* @param {'edit' | 'view'} right
|
* @param {'edit' | 'view'} right
|
||||||
* @param {{ code?: string, category?: string }} options
|
* @param {{ code?: string, category?: string }} options
|
||||||
* `code` & `category` from `Biz_Setting`
|
* `code` & `category` from `Common`
|
||||||
* - If passed options, returns edit rights for `code` (useful for components used in modals in many different tabs)
|
* - If passed options, returns edit rights for `code` (useful for components used in modals in many different tabs)
|
||||||
* - If not passed, returns edit rights for current tab & subtab
|
* - If not passed, returns edit rights for current tab & subtab
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
export default defineNuxtPlugin(async () => {
|
||||||
const { $getapi, $readyapi } = useNuxtApp()
|
const { $getapi, $readyapi } = useNuxtApp();
|
||||||
let connlist = $readyapi(['moneyunit', 'datatype', 'filterchoice', 'colorchoice', 'textalign', 'placement', 'colorscheme',
|
let connlist = $readyapi(["common", "tablesetting"]);
|
||||||
'filtertype', 'sorttype', 'tablesetting', 'settingchoice', 'sharechoice', 'menuchoice', 'settingtype', 'settingclass',
|
let filter = connlist.filter((v) => !v.ready);
|
||||||
'sex', 'legaltype', 'cart'])
|
if (filter.length > 0) await $getapi(filter);
|
||||||
let filter = connlist.filter(v=>!v.ready)
|
});
|
||||||
if(filter.length>0) await $getapi(filter)
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { defineNuxtPlugin } from "#app";
|
import { defineNuxtPlugin } from "#app";
|
||||||
import Employee from '@/components/viewer/Employee.vue';
|
import Employee from "@/components/viewer/Employee.vue";
|
||||||
import Kpi from '@/components/viewer/Kpi.vue';
|
import Kpi from "@/components/viewer/Kpi.vue";
|
||||||
import Salary from '@/components/viewer/Salary.vue';
|
import Salary from "@/components/viewer/Salary.vue";
|
||||||
import Dashboard from '@/components/viewer/Dashboard.vue';
|
import Dashboard from "@/components/viewer/Dashboard.vue";
|
||||||
import HRM from '@/components/viewer/HRM.vue';
|
import HRM from "@/components/viewer/HRM.vue";
|
||||||
import Rollcall from '@/components/viewer/Rollcall.vue';
|
import Rollcall from "@/components/viewer/Rollcall.vue";
|
||||||
import Notebox from "~/components/common/Notebox.vue";
|
import Notebox from "~/components/common/Notebox.vue";
|
||||||
import ProductCountbox from "~/components/common/ProductCountbox.vue";
|
import ProductCountbox from "~/components/common/ProductCountbox.vue";
|
||||||
import SvgIcon from "~/components/SvgIcon.vue";
|
import SvgIcon from "~/components/SvgIcon.vue";
|
||||||
@@ -20,13 +20,9 @@ import ChipImage from "~/components/media/ChipImage.vue";
|
|||||||
import Avatarbox from "~/components/common/Avatarbox.vue";
|
import Avatarbox from "~/components/common/Avatarbox.vue";
|
||||||
import Email from "~/components/marketing/email/Email.vue";
|
import Email from "~/components/marketing/email/Email.vue";
|
||||||
import ViewList from "~/components/common/ViewList.vue";
|
import ViewList from "~/components/common/ViewList.vue";
|
||||||
import InternalEntry from "~/components/modal/InternalEntry.vue"
|
|
||||||
|
|
||||||
import Configuration from "~/components/maintab/Configuration.vue";
|
import Configuration from "~/components/maintab/Configuration.vue";
|
||||||
import DebtView from "~/components/accounting/DebtView.vue";
|
import DebtView from "~/components/accounting/DebtView.vue";
|
||||||
import ReportDaily from "~/components/report/Daily.vue";
|
|
||||||
import ReportFormTo from "~/components/report/FromTo.vue";
|
|
||||||
import ReportMonthly from "~/components/report/Monthly.vue";
|
|
||||||
|
|
||||||
//format
|
//format
|
||||||
import FormatNumber from "~/components/datatable/format/FormatNumber.vue";
|
import FormatNumber from "~/components/datatable/format/FormatNumber.vue";
|
||||||
@@ -50,36 +46,18 @@ import ScrollBox from "~/components/datatable/ScrollBox.vue";
|
|||||||
import Product from "~/components/product/Product.vue";
|
import Product from "~/components/product/Product.vue";
|
||||||
import Reservation from "~/components/modal/Reservation.vue";
|
import Reservation from "~/components/modal/Reservation.vue";
|
||||||
import UserMainTab from "~/components/modal/UserMainTab.vue";
|
import UserMainTab from "~/components/modal/UserMainTab.vue";
|
||||||
import TransactionFiles from "~/components/transaction/TransactionFiles.vue";
|
|
||||||
import PaymentSchedule from "~/components/application/PaymentSchedule.vue";
|
import PaymentSchedule from "~/components/application/PaymentSchedule.vue";
|
||||||
import TransactionView from "~/components/transaction/TransactionView.vue";
|
|
||||||
import ContractPaymentUpload from "~/components/application/ContractPaymentUpload.vue";
|
|
||||||
import CountWithAdd from "~/components/common/CountWithAdd.vue";
|
import CountWithAdd from "~/components/common/CountWithAdd.vue";
|
||||||
import CalculationView from "~/components/application/CalculationView.vue";
|
import CalculationView from "~/components/application/CalculationView.vue";
|
||||||
import InternalAccount from "~/components/accounting/InternalAccount.vue";
|
import InternalAccount from "~/components/accounting/InternalAccount.vue";
|
||||||
import MenuAccount from "~/components/menu/MenuAccount.vue";
|
import MenuAccount from "~/components/menu/MenuAccount.vue";
|
||||||
import PhaseAdvance from "~/components/application/PhaseAdvance.vue";
|
import PhaseAdvance from "~/components/application/PhaseAdvance.vue";
|
||||||
import ImageLayout from '@/components/media/ImageLayout.vue';
|
import ImageLayout from "@/components/media/ImageLayout.vue";
|
||||||
import ProjectDocuments from '~/components/product/ProjectDocuments.vue';
|
import Cart from "~/components/product/Cart.vue";
|
||||||
import ProductEdit from '~/components/product/ProductEdit.vue';
|
import CountdownTimer from "~/components/common/CountdownTimer.vue";
|
||||||
|
import CustomerInfo2 from "~/components/customer/CustomerInfo2.vue";
|
||||||
import ProductHandover from '~/components/product/ProductHandover.vue'
|
import MenuFile from "~/components/menu/MenuFile.vue";
|
||||||
import Cart from '~/components/product/Cart.vue'
|
import Rights from "@/components/viewer/Rights.vue";
|
||||||
import CountdownTimer from '~/components/common/CountdownTimer.vue'
|
|
||||||
import CustomerInfo2 from '~/components/customer/CustomerInfo2.vue'
|
|
||||||
import MenuFile from '~/components/menu/MenuFile.vue'
|
|
||||||
import DebtProduct from '~/components/accounting/DebtProduct.vue'
|
|
||||||
import DebtCustomer from '~/components/accounting/DebtCustomer.vue'
|
|
||||||
import Due from '~/components/debt/Due.vue';
|
|
||||||
import Overdue from '@/components/debt/Overdue.vue';
|
|
||||||
import DebtSendEmail from '@/components/debt/DebtSendEmail.vue';
|
|
||||||
import DebtCheckbox from '@/components/debt/DebtCheckbox.vue';
|
|
||||||
import Allocate from '@/components/transaction/Allocate.vue';
|
|
||||||
import Handover from '~/components/handover/Handover.vue'
|
|
||||||
import HandoverSendEmail from '@/components/handover/HandoverSendEmail.vue';
|
|
||||||
import HandoverCheckbox from '@/components/handover/HandoverCheckbox.vue';
|
|
||||||
import HandoverStatus from '@/components/handover/HandoverStatus.vue';
|
|
||||||
import CountDown from '@/components/dialog/CountDown.vue';
|
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
DebtView,
|
DebtView,
|
||||||
@@ -88,14 +66,10 @@ const components = {
|
|||||||
CustomerInfo2,
|
CustomerInfo2,
|
||||||
CountdownTimer,
|
CountdownTimer,
|
||||||
PhaseAdvance,
|
PhaseAdvance,
|
||||||
InternalEntry,
|
|
||||||
ViewList,
|
ViewList,
|
||||||
ColorText,
|
ColorText,
|
||||||
CalculationView,
|
CalculationView,
|
||||||
CountWithAdd,
|
CountWithAdd,
|
||||||
ContractPaymentUpload,
|
|
||||||
TransactionView,
|
|
||||||
TransactionFiles,
|
|
||||||
Reservation,
|
Reservation,
|
||||||
Notebox,
|
Notebox,
|
||||||
ProductCountbox,
|
ProductCountbox,
|
||||||
@@ -118,9 +92,6 @@ const components = {
|
|||||||
MenuParam,
|
MenuParam,
|
||||||
ScrollBox,
|
ScrollBox,
|
||||||
MenuPayment,
|
MenuPayment,
|
||||||
ReportDaily,
|
|
||||||
ReportFormTo,
|
|
||||||
ReportMonthly,
|
|
||||||
DataModel,
|
DataModel,
|
||||||
FormatNumber,
|
FormatNumber,
|
||||||
FormatDate,
|
FormatDate,
|
||||||
@@ -134,27 +105,15 @@ const components = {
|
|||||||
InternalAccount,
|
InternalAccount,
|
||||||
MenuAccount,
|
MenuAccount,
|
||||||
ImageLayout,
|
ImageLayout,
|
||||||
ProjectDocuments,
|
|
||||||
Cart,
|
Cart,
|
||||||
MenuFile,
|
MenuFile,
|
||||||
DebtProduct,
|
|
||||||
DebtCustomer,
|
|
||||||
Due,
|
|
||||||
Overdue,
|
|
||||||
DebtSendEmail,
|
|
||||||
DebtCheckbox,
|
|
||||||
ProductHandover,
|
|
||||||
Allocate,
|
|
||||||
Handover,
|
|
||||||
HandoverSendEmail,
|
|
||||||
HandoverCheckbox,
|
|
||||||
HandoverStatus,
|
|
||||||
Dashboard,
|
Dashboard,
|
||||||
HRM,
|
HRM,
|
||||||
Rollcall,
|
Rollcall,
|
||||||
Salary,
|
Salary,
|
||||||
Kpi,
|
Kpi,
|
||||||
Employee
|
Employee,
|
||||||
|
Rights,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { defineNuxtPlugin } from '#app';
|
|
||||||
import { CkeditorPlugin } from '@ckeditor/ckeditor5-vue';
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.use(CkeditorPlugin);
|
|
||||||
});
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
/**
|
|
||||||
* plugins/excelExporter.js
|
|
||||||
*
|
|
||||||
* Xuất Excel từ DataTable với đầy đủ:
|
|
||||||
* - Group header (2 dòng, colspan/rowspan merge)
|
|
||||||
* - Stack split (array → nhiều sub-row, mỗi item 1 dòng)
|
|
||||||
* - splitTemplate, splitExpression, splitField, splitType currency
|
|
||||||
* - Template: FormatNumber, dayjs date, điều kiện row.type===x, Math.abs diff
|
|
||||||
* - Lọc cột action (DebtCheckbox, SvgIcon, modal...)
|
|
||||||
* - Màu header từ headerStyle / tablesetting
|
|
||||||
*
|
|
||||||
* Không cần cài thêm package — SheetJS load từ CDN lần đầu dùng (lazy).
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Import SheetJS từ package đã cài (npm install xlsx)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
import * as _XLSX from 'xlsx'
|
|
||||||
|
|
||||||
async function getXLSX() {
|
|
||||||
return _XLSX
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Helpers
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function stripHtml(str) {
|
|
||||||
if (!str) return ''
|
|
||||||
return String(str).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtCurrency(v) {
|
|
||||||
if (v === null || v === undefined || v === '') return ''
|
|
||||||
const n = Number(v)
|
|
||||||
if (isNaN(n)) return String(v)
|
|
||||||
return n.toLocaleString('vi-VN')
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtDate(raw, fmt = 'DD/MM/YYYY') {
|
|
||||||
if (!raw) return ''
|
|
||||||
try {
|
|
||||||
const d = new Date(raw)
|
|
||||||
const dd = String(d.getDate()).padStart(2, '0')
|
|
||||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
||||||
return fmt.replace('DD', dd).replace('MM', mm).replace('YYYY', String(d.getFullYear()))
|
|
||||||
} catch { return String(raw) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSplitTemplate(template, item) {
|
|
||||||
return template.replace(/\{(\w+)\}/g, (_, key) => item[key] ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function evalSplitExpression(expression, item) {
|
|
||||||
try {
|
|
||||||
const expr = expression.replace(/\{(\w+)\}/g, (_, key) => {
|
|
||||||
const v = item[key]
|
|
||||||
return (v === null || v === undefined) ? 'null' : Number(v)
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line no-new-func
|
|
||||||
return Function('"use strict"; return (' + expr + ')')()
|
|
||||||
} catch { return null }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Resolve text của 1 item trong stack array
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function getStackItemText(field, item) {
|
|
||||||
if (field.splitExpression) {
|
|
||||||
const val = evalSplitExpression(field.splitExpression, item)
|
|
||||||
if (val === null || val === undefined) return '—'
|
|
||||||
return field.splitType === 'currency' ? fmtCurrency(val) : String(val)
|
|
||||||
}
|
|
||||||
if (field.splitTemplate) return resolveSplitTemplate(field.splitTemplate, item)
|
|
||||||
const val = item[field.splitField]
|
|
||||||
if (val === null || val === undefined) return '—'
|
|
||||||
return field.splitType === 'currency' ? fmtCurrency(val) : String(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Resolve text của 1 cell bình thường
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function getCellText(field, row) {
|
|
||||||
if (field.name === '__stt') return row.__stt ?? ''
|
|
||||||
|
|
||||||
if (field.split === 'stack') {
|
|
||||||
const arr = row[field.name]
|
|
||||||
if (!Array.isArray(arr) || arr.length === 0) return '—'
|
|
||||||
// Gom tất cả item thành 1 chuỗi ngăn cách bởi newline
|
|
||||||
return arr.map(item => getStackItemText(field, item)).join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.template) return evalTemplate(field.template, row)
|
|
||||||
|
|
||||||
if (field.format === 'date' && row[field.name]) return fmtDate(row[field.name])
|
|
||||||
|
|
||||||
if (field.format === 'number' || field.type === 'currency') {
|
|
||||||
return row[field.name] !== null && row[field.name] !== undefined
|
|
||||||
? fmtCurrency(row[field.name]) : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return row[field.name] ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function evalTemplate(template, row) {
|
|
||||||
// <FormatNumber v-bind="{value: row.xxx}" />
|
|
||||||
const fnMatch = template.match(/FormatNumber[^}]*value:\s*row\.(\w+)/)
|
|
||||||
if (fnMatch) {
|
|
||||||
const v = row[fnMatch[1]]
|
|
||||||
return v === null || v === undefined ? '' : fmtCurrency(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// row.type === N ? row.xxx : null
|
|
||||||
const condMatch = template.match(/row\.(\w+)\s*===\s*(\d+)\s*\?\s*row\.(\w+)\s*:\s*null/)
|
|
||||||
if (condMatch) {
|
|
||||||
const v = row[condMatch[1]] === Number(condMatch[2]) ? row[condMatch[3]] : null
|
|
||||||
return v === null || v === undefined ? '' : fmtCurrency(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// $dayjs(row.xxx).format('...')
|
|
||||||
const djMatch = template.match(/\$dayjs\(row\.(\w+)\)\.format\(['"]([^'"]+)['"]\)/)
|
|
||||||
if (djMatch) return fmtDate(row[djMatch[1]], djMatch[2])
|
|
||||||
|
|
||||||
// Math.abs($dayjs().startOf('day').diff(row.xxx, 'day'))
|
|
||||||
const diffMatch = template.match(/Math\.abs\(.*\.diff\(row\.(\w+),\s*'day'\)\)/)
|
|
||||||
if (diffMatch) {
|
|
||||||
try {
|
|
||||||
const d = new Date(row[diffMatch[1]]); d.setHours(0, 0, 0, 0)
|
|
||||||
const today = new Date(); today.setHours(0, 0, 0, 0)
|
|
||||||
return String(Math.abs(Math.round((today - d) / 86400000)))
|
|
||||||
} catch { return '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// {{row.xxx}} nội suy đơn giản
|
|
||||||
const interpolated = template.replace(/\{\{row\.(\w+)\}\}/g, (_, k) => row[k] ?? '')
|
|
||||||
return stripHtml(interpolated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Xây dựng group header row (mirror DataTable.vue groupHeaderRow computed)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function buildGroupHeaderRow(fields) {
|
|
||||||
const result = []
|
|
||||||
let i = 0
|
|
||||||
while (i < fields.length) {
|
|
||||||
const f = fields[i]
|
|
||||||
if (!f.groupHeader) {
|
|
||||||
result.push({ label: stripHtml(f.label), colspan: 1, rowspan: 2, isGroup: false })
|
|
||||||
i++
|
|
||||||
} else {
|
|
||||||
let count = 1
|
|
||||||
while (i + count < fields.length && fields[i + count].groupHeader === f.groupHeader) count++
|
|
||||||
result.push({ label: f.groupHeader, colspan: count, rowspan: 1, isGroup: true })
|
|
||||||
i += count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Style helpers
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function hexFromStyle(headerStyle) {
|
|
||||||
const m = (headerStyle || '').match(/background-color:\s*#([0-9a-fA-F]{6})/i)
|
|
||||||
return m ? m[1].toUpperCase() : '204853'
|
|
||||||
}
|
|
||||||
|
|
||||||
function thinBorder() {
|
|
||||||
const s = { style: 'thin', color: { rgb: 'FFD0D0D0' } }
|
|
||||||
return { top: s, bottom: s, left: s, right: s }
|
|
||||||
}
|
|
||||||
|
|
||||||
function headerCellStyle(bgHex, fgHex) {
|
|
||||||
return {
|
|
||||||
fill: { patternType: 'solid', fgColor: { rgb: 'FF' + bgHex } },
|
|
||||||
font: { bold: true, color: { rgb: 'FF' + fgHex }, sz: 10, name: 'Arial' },
|
|
||||||
alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
|
|
||||||
border: thinBorder(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dataCellStyle(alignRight, wrapText) {
|
|
||||||
return {
|
|
||||||
font: { sz: 10, name: 'Arial' },
|
|
||||||
alignment: { horizontal: alignRight ? 'right' : 'left', vertical: 'top', wrapText },
|
|
||||||
border: thinBorder(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Hàm xuất Excel chính
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function exportToExcel({ fields, data, tablesetting = [], filename = 'export' }) {
|
|
||||||
const XLSX = await getXLSX()
|
|
||||||
|
|
||||||
const hasGroup = fields.some(f => f.groupHeader)
|
|
||||||
const HEADER_ROWS = hasGroup ? 2 : 1
|
|
||||||
|
|
||||||
const fgHex = (tablesetting.find(s => s.code === 'header-font-color')?.detail || '#FFFFFF')
|
|
||||||
.replace('#', '').toUpperCase()
|
|
||||||
|
|
||||||
// ── Xây dựng AOA ──────────────────────────────────────────────────────────
|
|
||||||
const aoa = []
|
|
||||||
|
|
||||||
if (hasGroup) {
|
|
||||||
const groupRow = buildGroupHeaderRow(fields)
|
|
||||||
const row1 = []
|
|
||||||
const row2 = []
|
|
||||||
let colCursor = 0
|
|
||||||
|
|
||||||
groupRow.forEach(col => {
|
|
||||||
if (!col.isGroup) {
|
|
||||||
row1.push(col.label)
|
|
||||||
row2.push('') // merge về row1 sau
|
|
||||||
colCursor++
|
|
||||||
} else {
|
|
||||||
row1.push(col.label)
|
|
||||||
for (let x = 1; x < col.colspan; x++) row1.push('')
|
|
||||||
const groupFields = fields.slice(colCursor, colCursor + col.colspan)
|
|
||||||
groupFields.forEach(f => row2.push(stripHtml(f.label)))
|
|
||||||
colCursor += col.colspan
|
|
||||||
}
|
|
||||||
})
|
|
||||||
aoa.push(row1)
|
|
||||||
aoa.push(row2)
|
|
||||||
} else {
|
|
||||||
aoa.push(fields.map(f => stripHtml(f.label)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Dữ liệu ────────────────────────────────────────────────────────────────
|
|
||||||
data.forEach((row, idx) => {
|
|
||||||
if (row.__stt === undefined) row.__stt = idx + 1
|
|
||||||
|
|
||||||
// Đếm số sub-row cần (bằng max length của các stack field)
|
|
||||||
const maxStack = Math.max(1, ...fields.map(f =>
|
|
||||||
f.split === 'stack' && Array.isArray(row[f.name]) ? (row[f.name].length || 1) : 1
|
|
||||||
))
|
|
||||||
|
|
||||||
for (let si = 0; si < maxStack; si++) {
|
|
||||||
aoa.push(fields.map(f => {
|
|
||||||
// Cột stack: lấy item[si]
|
|
||||||
if (f.split === 'stack') {
|
|
||||||
const arr = Array.isArray(row[f.name]) ? row[f.name] : []
|
|
||||||
const item = arr[si]
|
|
||||||
return item ? getStackItemText(f, item) : ''
|
|
||||||
}
|
|
||||||
// Cột thường: chỉ ghi ở sub-row đầu
|
|
||||||
return si === 0 ? String(getCellText(f, row) ?? '') : ''
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Worksheet ──────────────────────────────────────────────────────────────
|
|
||||||
const ws = XLSX.utils.aoa_to_sheet(aoa)
|
|
||||||
|
|
||||||
// ── Merge cells ────────────────────────────────────────────────────────────
|
|
||||||
if (hasGroup) {
|
|
||||||
const merges = []
|
|
||||||
const groupRow = buildGroupHeaderRow(fields)
|
|
||||||
let c = 0
|
|
||||||
groupRow.forEach(col => {
|
|
||||||
if (!col.isGroup) {
|
|
||||||
merges.push({ s: { r: 0, c }, e: { r: 1, c } }) // rowspan=2
|
|
||||||
c++
|
|
||||||
} else {
|
|
||||||
if (col.colspan > 1)
|
|
||||||
merges.push({ s: { r: 0, c }, e: { r: 0, c: c + col.colspan - 1 } }) // colspan
|
|
||||||
c += col.colspan
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ws['!merges'] = merges
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Độ rộng cột ────────────────────────────────────────────────────────────
|
|
||||||
ws['!cols'] = fields.map(f => {
|
|
||||||
const labelLen = stripHtml(f.label).length
|
|
||||||
const minWch = Math.max(f.minwidth ? f.minwidth / 7 : 10, labelLen + 2)
|
|
||||||
const maxWch = f.maxwidth ? f.maxwidth / 7 : 40
|
|
||||||
return { wch: Math.min(minWch, maxWch) }
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Chiều cao dòng header ──────────────────────────────────────────────────
|
|
||||||
ws['!rows'] = Array.from({ length: HEADER_ROWS }, () => ({ hpt: 36 }))
|
|
||||||
|
|
||||||
// ── Style từng cell ────────────────────────────────────────────────────────
|
|
||||||
const wsRange = XLSX.utils.decode_range(ws['!ref'])
|
|
||||||
for (let R = wsRange.s.r; R <= wsRange.e.r; R++) {
|
|
||||||
for (let C = wsRange.s.c; C <= wsRange.e.c; C++) {
|
|
||||||
const addr = XLSX.utils.encode_cell({ r: R, c: C })
|
|
||||||
if (!ws[addr]) ws[addr] = { t: 's', v: '' }
|
|
||||||
const f = fields[C]
|
|
||||||
if (R < HEADER_ROWS) {
|
|
||||||
ws[addr].s = headerCellStyle(hexFromStyle(f?.headerStyle), fgHex)
|
|
||||||
} else {
|
|
||||||
const isRight = f?.textalign === 'right' || f?.splitType === 'currency'
|
|
||||||
const hasStack = f?.split === 'stack'
|
|
||||||
ws[addr].s = dataCellStyle(isRight, hasStack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Ghi file ───────────────────────────────────────────────────────────────
|
|
||||||
const wb = XLSX.utils.book_new()
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, 'Data')
|
|
||||||
XLSX.writeFile(wb, `${filename}.xlsx`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Nuxt Plugin
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
exportTableExcel: exportToExcel,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useStore = defineStore("maindev", {
|
export const useStore = defineStore("hrm-dev", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
viewport: undefined,
|
viewport: undefined,
|
||||||
login: undefined,
|
login: undefined,
|
||||||
@@ -10,20 +10,103 @@ export const useStore = defineStore("maindev", {
|
|||||||
settings: [],
|
settings: [],
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
snackbar: undefined,
|
snackbar: undefined,
|
||||||
productdocument: undefined,
|
|
||||||
applicationstatus: undefined,
|
|
||||||
applicationproduct: undefined,
|
|
||||||
lastlegendfiltertab: 'Giỏ hàng',
|
|
||||||
layersetting: undefined,
|
|
||||||
country: undefined,
|
country: undefined,
|
||||||
lang: "vi",
|
lang: "vi",
|
||||||
branch: {},
|
branch: {},
|
||||||
rights: [],
|
rights: [],
|
||||||
product: [],
|
menu: [
|
||||||
cart: [],
|
{
|
||||||
selectedPaymentSchedulesForEmailInDue: [],
|
id: 1,
|
||||||
selectedPaymentSchedulesForEmailInOverdue: [],
|
category: "topmenu",
|
||||||
selectedProductsForHandoverEmail: [],
|
classify: "left",
|
||||||
|
code: "dashboard",
|
||||||
|
vi: "Dashboard",
|
||||||
|
link: null,
|
||||||
|
view: true,
|
||||||
|
detail: {
|
||||||
|
base: "Dashboard",
|
||||||
|
component: "DashboardMaster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
category: "topmenu",
|
||||||
|
classify: "left",
|
||||||
|
code: "hrm",
|
||||||
|
vi: "Quản lý nhân sự",
|
||||||
|
link: null,
|
||||||
|
view: true,
|
||||||
|
detail: {
|
||||||
|
base: "HRM",
|
||||||
|
component: "HRMMaster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
category: "topmenu",
|
||||||
|
classify: "left",
|
||||||
|
code: "rollcall",
|
||||||
|
vi: "Chấm công",
|
||||||
|
link: null,
|
||||||
|
view: true,
|
||||||
|
detail: {
|
||||||
|
base: "Rollcall",
|
||||||
|
component: "RollcallMaster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
category: "topmenu",
|
||||||
|
classify: "left",
|
||||||
|
code: "salary",
|
||||||
|
vi: "Tính lương",
|
||||||
|
link: null,
|
||||||
|
view: false,
|
||||||
|
detail: {
|
||||||
|
base: "Salary",
|
||||||
|
component: "SalaryMaster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
category: "topmenu",
|
||||||
|
classify: "left",
|
||||||
|
code: "kpi_efficiency",
|
||||||
|
vi: "KPI & Hiệu suất",
|
||||||
|
link: null,
|
||||||
|
view: false,
|
||||||
|
detail: {
|
||||||
|
base: "Kpi",
|
||||||
|
component: "KpiMaster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
category: "topmenu",
|
||||||
|
classify: "left",
|
||||||
|
code: "employee",
|
||||||
|
vi: "Cổng nhân viên",
|
||||||
|
link: null,
|
||||||
|
view: false,
|
||||||
|
detail: {
|
||||||
|
base: "Employee",
|
||||||
|
component: "EmployeeMaster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
category: "topmenu",
|
||||||
|
classify: "left",
|
||||||
|
code: "rights",
|
||||||
|
vi: "Phân quyền",
|
||||||
|
link: null,
|
||||||
|
view: false,
|
||||||
|
detail: {
|
||||||
|
base: "Rights",
|
||||||
|
component: "RightsMaster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -34,38 +117,10 @@ export const useStore = defineStore("maindev", {
|
|||||||
removeSnackbar() {
|
removeSnackbar() {
|
||||||
this.snackbar = undefined;
|
this.snackbar = undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProduct(products) {
|
|
||||||
this.product = products;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCart(carts) {
|
|
||||||
this.cart = carts;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSingleProduct(updatedProduct) {
|
|
||||||
const index = this.product.findIndex(p => p.id === updatedProduct.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.product[index] = updatedProduct;
|
|
||||||
} else {
|
|
||||||
this.product.push(updatedProduct);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeProduct(productId) {
|
|
||||||
this.product = this.product.filter(p => p.id !== productId);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
persist: {
|
persist: {
|
||||||
pick: [
|
pick: ["token", "login", "lang", "menu"],
|
||||||
"token",
|
|
||||||
"login",
|
|
||||||
"lang",
|
|
||||||
"dealer",
|
|
||||||
"lastlegendfiltertab",
|
|
||||||
"layersetting"
|
|
||||||
],
|
|
||||||
storage: piniaPluginPersistedstate.localStorage(),
|
storage: piniaPluginPersistedstate.localStorage(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
17322
package-lock.json
generated
28
package.json
@@ -5,57 +5,41 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
|
"format": "prettier . --write",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"build-bulma": "sass --load-path=node_modules my-bulma-project.scss my-bulma-project.css"
|
"build-bulma": "sass --load-path=node_modules my-bulma-project.scss my-bulma-project.css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aps_sdk/authentication": "^1.0.0",
|
|
||||||
"@aps_sdk/model-derivative": "^1.2.0",
|
|
||||||
"@aps_sdk/oss": "^1.2.2",
|
|
||||||
"@ckeditor/ckeditor5-vue": "^7.3.1",
|
|
||||||
"@nuxt/image": "^1.11.0",
|
"@nuxt/image": "^1.11.0",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tato30/vue-pdf": "^1.11.4",
|
|
||||||
"@vue-email/components": "^0.0.21",
|
"@vue-email/components": "^0.0.21",
|
||||||
"@vue-email/render": "^0.0.9",
|
"@vue-email/render": "^0.0.9",
|
||||||
"@vueup/vue-quill": "^1.2.0",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"bowser": "^2.11.0",
|
"bulma": "^1.0.4",
|
||||||
"bulma": "^1.0.2",
|
|
||||||
"ckeditor5": "^43.3.1",
|
|
||||||
"ckeditor5-premium-features": "^43.3.1",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"docx-preview": "^0.3.6",
|
"docx-preview": "^0.3.6",
|
||||||
"es-toolkit": "^1.42.0",
|
"es-toolkit": "^1.42.0",
|
||||||
"highcharts": "^12.2.0",
|
"highcharts": "^12.2.0",
|
||||||
"highcharts-vue": "^2.0.1",
|
"highcharts-vue": "^2.0.1",
|
||||||
"ipx": "^3.0.1",
|
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"nuxt-qrcode": "^0.4.8",
|
"nuxt-qrcode": "^0.4.8",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.5.0",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"qs": "^6.14.0",
|
|
||||||
"quill": "^2.0.3",
|
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.81.0",
|
||||||
"sharp": "^0.33.5",
|
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"vue": "latest",
|
"vue": "^3.5.34",
|
||||||
"vue-advanced-cropper": "^2.8.9",
|
"vue-advanced-cropper": "^2.8.9",
|
||||||
"vue-pdf": "^4.3.0",
|
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
"vue3-datepicker": "^0.4.0",
|
|
||||||
"vue3-quill": "^0.3.1",
|
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"overrides": {
|
|
||||||
"vue": "latest"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.4.7",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3",
|
||||||
|
"prettier": "3.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
public/icon/cart.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 22q-.825 0-1.412-.587T5 20t.588-1.412T7 18t1.413.588T9 20t-.587 1.413T7 22m10 0q-.825 0-1.412-.587T15 20t.588-1.412T17 18t1.413.588T19 20t-.587 1.413T17 22M6.15 6l2.4 5h7l2.75-5zM5.2 4h14.75q.575 0 .875.513t.025 1.037l-3.55 6.4q-.275.5-.737.775T15.55 13H8.1L7 15h11q.425 0 .713.288T19 16t-.288.713T18 17H7q-1.125 0-1.7-.987t-.05-1.963L6.6 11.6L3 4H2q-.425 0-.712-.288T1 3t.288-.712T2 2h1.625q.275 0 .525.15t.375.425zm3.35 7h7z"/></svg>
|
||||||
|
After Width: | Height: | Size: 665 B |
@@ -1 +1 @@
|
|||||||
<?xml version="1.0" ?><svg data-name="Livello 1" id="Livello_1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><title/><path d="M116.73,31.83a3,3,0,0,0-4.2-.61L64.14,67.34a1,1,0,0,1-1.2,0L15.5,31.06a3,3,0,1,0-3.64,4.77L49.16,64.36,12.27,92.16A3,3,0,1,0,15.88,97L54.11,68.14l5.18,4a7,7,0,0,0,8.43.06l5.44-4.06L111.84,97a3,3,0,1,0,3.59-4.81L78.17,64.35,116.12,36A3,3,0,0,0,116.73,31.83Z"/><path d="M113,19H15A15,15,0,0,0,0,34V94a15,15,0,0,0,15,15h98a15,15,0,0,0,15-15V34A15,15,0,0,0,113,19Zm9,75a9,9,0,0,1-9,9H15a9,9,0,0,1-9-9V34a9,9,0,0,1,9-9h98a9,9,0,0,1,9,9Z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zM20 8l-7.475 4.675q-.125.075-.262.113t-.263.037t-.262-.037t-.263-.113L4 8v10h16zm-8 3l8-5H4zM4 8v.25v-1.475v.025V6v.8v-.012V8.25zv10z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 474 B |
1
public/icon/money.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12.025 21q-.425 0-.712-.288T11.025 20v-1.15Q9.9 18.6 9.05 17.975t-1.375-1.75q-.175-.35-.012-.737t.587-.563q.35-.15.725.013t.575.537q.425.75 1.075 1.138t1.6.387q1.025 0 1.737-.462t.713-1.438q0-.875-.55-1.387t-2.55-1.163q-2.15-.675-2.95-1.612t-.8-2.288q0-1.625 1.05-2.525t2.15-1.025V4q0-.425.288-.713T12.025 3t.713.288t.287.712v1.1q.95.15 1.65.613t1.15 1.137q.225.325.088.725t-.563.575q-.35.15-.725.013t-.7-.488t-.763-.537t-1.087-.188q-1.1 0-1.675.488T9.825 8.65q0 .825.75 1.3t2.6 1q1.725.5 2.613 1.588t.887 2.512q0 1.775-1.05 2.7t-2.6 1.15V20q0 .425-.288.713t-.712.287"/></svg>
|
||||||
|
After Width: | Height: | Size: 804 B |
@@ -1,12 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M3 17h18q.425 0 .713.288T22 18t-.288.713T21 19H3q-.425 0-.712-.288T2 18t.288-.712T3 17m1-5.55l-.475.85q-.15.275-.45.35t-.575-.075t-.35-.45t.075-.575l.475-.85h-.95q-.325 0-.537-.212T1 9.95t.213-.537t.537-.213h.95l-.475-.8q-.15-.275-.075-.575t.35-.45t.575-.075t.45.35l.475.8l.475-.8q.15-.275.45-.35t.575.075t.35.45t-.075.575l-.475.8h.95q.325 0 .538.213T7 9.95t-.213.538t-.537.212H5.3l.475.85q.15.275.075.575t-.35.45t-.575.075t-.45-.35zm8 0l-.475.85q-.15.275-.45.35t-.575-.075t-.35-.45t.075-.575l.475-.85h-.95q-.325 0-.537-.212T9 9.95t.213-.537t.537-.213h.95l-.475-.8q-.15-.275-.075-.575t.35-.45t.575-.075t.45.35l.475.8l.475-.8q.15-.275.45-.35t.575.075t.35.45t-.075.575l-.475.8h.95q.325 0 .537.213T15 9.95t-.213.538t-.537.212h-.95l.475.85q.15.275.075.575t-.35.45t-.575.075t-.45-.35zm8 0l-.475.85q-.15.275-.45.35t-.575-.075t-.35-.45t.075-.575l.475-.85h-.95q-.325 0-.537-.212T17 9.95t.213-.537t.537-.213h.95l-.475-.8q-.15-.275-.075-.575t.35-.45t.575-.075t.45.35l.475.8l.475-.8q.15-.275.45-.35t.575.075t.35.45t-.075.575l-.475.8h.95q.325 0 .538.213T23 9.95t-.213.538t-.537.212h-.95l.475.85q.15.275.075.575t-.35.45t-.575.075t-.45-.35z"/></svg>
|
||||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>Iconly/Curved/Password</title>
|
|
||||||
<g id="Iconly/Curved/Password" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<g id="Password" transform="translate(2.749900, 2.750100)" stroke="#000000" stroke-width="1.5">
|
|
||||||
<path d="M7.9389,9.25 C7.9389,10.273 7.1099,11.102 6.0869,11.102 C5.0639,11.102 4.2349,10.273 4.2349,9.25 C4.2349,8.227 5.0639,7.398 6.0869,7.398 L6.0899,7.398 C7.1119,7.399 7.9389,8.228 7.9389,9.25 Z" id="Stroke-1"></path>
|
|
||||||
<polyline id="Stroke-3" points="7.9419 9.25 14.2599 9.25 14.2599 11.102"></polyline>
|
|
||||||
<line x1="11.4321" y1="11.102" x2="11.4321" y2="9.25" id="Stroke-5"></line>
|
|
||||||
<path d="M0,9.25 C0,2.313 2.313,0 9.25,0 C16.187,0 18.5,2.313 18.5,9.25 C18.5,16.187 16.187,18.5 9.25,18.5 C2.313,18.5 0,16.187 0,9.25 Z" id="Stroke-7"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
|||||||
<?xml version="1.0" ?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g id="_133_save-02" data-name="133 save-02"><path d="M26,29H6a3,3,0,0,1-3-3V6A3,3,0,0,1,6,3H20.26a3,3,0,0,1,1.95.72L28,8.64A3,3,0,0,1,29,10.92V26A3,3,0,0,1,26,29ZM6,5A1,1,0,0,0,5,6V26a1,1,0,0,0,1,1H26a1,1,0,0,0,1-1V10.92a1,1,0,0,0-.35-.76L20.91,5.24A1,1,0,0,0,20.26,5Z"/><path d="M18,10H10A2,2,0,0,1,8,8V4A1,1,0,0,1,9,3H19a1,1,0,0,1,1,1V8A2,2,0,0,1,18,10ZM10,5V8h8V5Z"/><path d="M23,29H9a1,1,0,0,1-1-1V17a2,2,0,0,1,2-2H22a2,2,0,0,1,2,2V28A1,1,0,0,1,23,29ZM10,27H22V17H10Z"/></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h11.175q.4 0 .763.15t.637.425l2.85 2.85q.275.275.425.638t.15.762V19q0 .825-.587 1.413T19 21zM19 7.85L16.15 5H5v14h14zM12 18q1.25 0 2.125-.875T15 15t-.875-2.125T12 12t-2.125.875T9 15t.875 2.125T12 18m-5-8h7q.425 0 .713-.288T15 9V7q0-.425-.288-.712T14 6H7q-.425 0-.712.288T6 7v2q0 .425.288.713T7 10M5 7.85V19V5z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 567 B After Width: | Height: | Size: 597 B |
1
public/icon/visibility.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 16q1.875 0 3.188-1.312T16.5 11.5t-1.312-3.187T12 7T8.813 8.313T7.5 11.5t1.313 3.188T12 16m0-1.8q-1.125 0-1.912-.788T9.3 11.5t.788-1.912T12 8.8t1.913.788t.787 1.912t-.787 1.913T12 14.2m0 4.8q-3.35 0-6.113-1.8t-4.362-4.75q-.125-.225-.187-.462t-.063-.488t.063-.488t.187-.462q1.6-2.95 4.363-4.75T12 4t6.113 1.8t4.362 4.75q.125.225.188.463t.062.487t-.062.488t-.188.462q-1.6 2.95-4.362 4.75T12 19"/></svg>
|
||||||
|
After Width: | Height: | Size: 630 B |
@@ -1,3 +0,0 @@
|
|||||||
import { getViewerToken } from '../utils/aps.js';
|
|
||||||
|
|
||||||
export default defineEventHandler(getViewerToken);
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const urn = getRouterParam(event, 'urn');
|
|
||||||
const manifest = await getManifest(urn);
|
|
||||||
|
|
||||||
if (!manifest)
|
|
||||||
return { status: 'n/a' };
|
|
||||||
|
|
||||||
let messages = [];
|
|
||||||
if (manifest.derivatives) {
|
|
||||||
for (const derivative of manifest.derivatives) {
|
|
||||||
messages = messages.concat(derivative.messages || []);
|
|
||||||
if (derivative.children) {
|
|
||||||
for (const child of derivative.children) {
|
|
||||||
messages.concat(child.messages || []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: manifest.status,
|
|
||||||
progress: manifest.progress,
|
|
||||||
messages
|
|
||||||
};
|
|
||||||
})
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
|
||||||
if (event.method === 'GET') {
|
|
||||||
const objects = await listObjects();
|
|
||||||
return objects.map((o) => ({
|
|
||||||
name: o.objectKey,
|
|
||||||
urn: urnify(o.objectId),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.method === 'POST') {
|
|
||||||
const formData = await readMultipartFormData(event);
|
|
||||||
if (!formData) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
statusMessage: `formData should not be ${formData}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = formData[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const obj = await uploadObject(file.filename, file.data);
|
|
||||||
await translateObject(urnify(obj.objectId), req.fields['model-zip-entrypoint']);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: obj.objectKey,
|
|
||||||
urn: urnify(obj.objectId),
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
statusMessage: 'Upload file failed',
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export default defineEventHandler((event) => {
|
|
||||||
const originalUrl = event.node.req.url || "";
|
|
||||||
if (originalUrl.startsWith("/product:")) {
|
|
||||||
const rewritten = originalUrl.replace("/product:", "/product/");
|
|
||||||
event.node.req.url = rewritten;
|
|
||||||
event._path = rewritten;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../.nuxt/tsconfig.server.json"
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { AuthenticationClient, Scopes } from '@aps_sdk/authentication';
|
|
||||||
import { ModelDerivativeClient, View, OutputType } from '@aps_sdk/model-derivative';
|
|
||||||
import { OssClient, Region, PolicyKey } from '@aps_sdk/oss';
|
|
||||||
|
|
||||||
const apsClientId = 'PkqB8KlPGekLGKElPSjPhC9C2NGyXMN64QVhPSaqUhV46zEn';
|
|
||||||
const apsClientSecret = 'Jr70CpXFaV2PRd9lux3ZXBKpSMwFsyO6xq1Y7AJy1Q4xcK6nZMLq20KB8SlipC4Z';
|
|
||||||
const apsBucket = `${apsClientId.toLowerCase()}-basic-app`;
|
|
||||||
const authenticationClient = new AuthenticationClient();
|
|
||||||
const ossClient = new OssClient();
|
|
||||||
const modelDerivativeClient = new ModelDerivativeClient();
|
|
||||||
|
|
||||||
async function getInternalToken() {
|
|
||||||
const credentials = await authenticationClient.getTwoLeggedToken(
|
|
||||||
apsClientId,
|
|
||||||
apsClientSecret,
|
|
||||||
[Scopes.DataRead, Scopes.DataCreate, Scopes.DataWrite, Scopes.BucketCreate, Scopes.BucketRead],
|
|
||||||
);
|
|
||||||
return credentials.access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getViewerToken = async () => {
|
|
||||||
return await authenticationClient.getTwoLeggedToken(apsClientId, apsClientSecret, [
|
|
||||||
Scopes.ViewablesRead,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ensureBucketExists = async (bucketKey) => {
|
|
||||||
const accessToken = await getInternalToken();
|
|
||||||
try {
|
|
||||||
await ossClient.getBucketDetails(bucketKey, { accessToken });
|
|
||||||
} catch (err) {
|
|
||||||
if (err.axiosError.response.status === 404) {
|
|
||||||
await ossClient.createBucket(
|
|
||||||
Region.Us,
|
|
||||||
{ bucketKey: bucketKey, policyKey: PolicyKey.Persistent },
|
|
||||||
{ accessToken },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listObjects = async () => {
|
|
||||||
await ensureBucketExists(apsBucket);
|
|
||||||
const accessToken = await getInternalToken();
|
|
||||||
let resp = await ossClient.getObjects(apsBucket, { limit: 64, accessToken });
|
|
||||||
let objects = resp.items;
|
|
||||||
while (resp.next) {
|
|
||||||
const startAt = new URL(resp.next).searchParams.get('startAt');
|
|
||||||
resp = await ossClient.getObjects(apsBucket, { limit: 64, startAt, accessToken });
|
|
||||||
objects = objects.concat(resp.items);
|
|
||||||
}
|
|
||||||
return objects;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const uploadObject = async (objectName, buffer) => {
|
|
||||||
await ensureBucketExists(apsBucket);
|
|
||||||
const accessToken = await getInternalToken();
|
|
||||||
const obj = await ossClient.uploadObject(apsBucket, objectName, buffer, { accessToken });
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const translateObject = async (urn, rootFilename) => {
|
|
||||||
const accessToken = await getInternalToken();
|
|
||||||
const job = await modelDerivativeClient.startJob(
|
|
||||||
{
|
|
||||||
input: {
|
|
||||||
urn,
|
|
||||||
compressedUrn: !!rootFilename,
|
|
||||||
rootFilename,
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
formats: [
|
|
||||||
{
|
|
||||||
views: [View._2d, View._3d],
|
|
||||||
type: OutputType.Svf2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ accessToken },
|
|
||||||
);
|
|
||||||
return job.result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getManifest = async (urn) => {
|
|
||||||
const accessToken = await getInternalToken();
|
|
||||||
try {
|
|
||||||
const manifest = await modelDerivativeClient.getManifest(urn, { accessToken });
|
|
||||||
return manifest;
|
|
||||||
} catch (err) {
|
|
||||||
if (err.axiosError.response.status === 404) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const urnify = (id) => Buffer.from(id).toString('base64').replace(/=/g, '');
|
|
||||||