chore: install prettier

This commit is contained in:
Viet An
2026-05-04 15:22:27 +07:00
parent 93d29ca7d8
commit bd58e2b847
267 changed files with 22950 additions and 13581 deletions

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
**/*.min.js
my-bulma-project.css
my-bulma-project.css.map

24
.prettierrc Normal file
View 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
View File

@@ -0,0 +1,7 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true
}

View File

@@ -14,9 +14,14 @@
.blockdiv { .blockdiv {
max-width: 1900px !important; max-width: 1900px !important;
padding: 1rem 2rem 2rem; padding: 1rem 2rem 2rem;
@include mobile { padding: 1rem; } @include mobile {
padding: 1rem;
}
.columns .column { .columns .column {
@include mobile { padding-left: 0; padding-right: 0; } @include mobile {
padding-left: 0;
padding-right: 0;
}
} }
} }

View File

@@ -13,4 +13,4 @@
// might break lots of stuff // might break lots of stuff
// .skeleton-block:not(:last-child), .media:not(:last-child), .level:not(:last-child), .fixed-grid:not(:last-child), .grid:not(:last-child), .tabs:not(:last-child), .pagination:not(:last-child), .message:not(:last-child), .card:not(:last-child), .breadcrumb:not(:last-child), .field:not(:last-child), .file:not(:last-child), .title:not(:last-child), .subtitle:not(:last-child), .tags:not(:last-child), .table:not(:last-child), .table-container:not(:last-child), .progress:not(:last-child), .notification:not(:last-child), .content:not(:last-child), .buttons:not(:last-child), .box:not(:last-child), .block:not(:last-child) { // .skeleton-block:not(:last-child), .media:not(:last-child), .level:not(:last-child), .fixed-grid:not(:last-child), .grid:not(:last-child), .tabs:not(:last-child), .pagination:not(:last-child), .message:not(:last-child), .card:not(:last-child), .breadcrumb:not(:last-child), .field:not(:last-child), .file:not(:last-child), .title:not(:last-child), .subtitle:not(:last-child), .tags:not(:last-child), .table:not(:last-child), .table-container:not(:last-child), .progress:not(:last-child), .notification:not(:last-child), .content:not(:last-child), .buttons:not(:last-child), .box:not(:last-child), .block:not(:last-child) {
// margin-bottom: inherit; // margin-bottom: inherit;
// } // }

View File

@@ -2,61 +2,66 @@
// Font size loops // Font size loops
@for $i from 10 through 50 { @for $i from 10 through 50 {
.fs-#{$i} { font-size: $i + px; } .fs-#{$i} {
.fsb-#{$i} { font-size: $i + px; font-weight: bold; } font-size: $i + px;
}
.fsb-#{$i} {
font-size: $i + px;
font-weight: bold;
}
} }
.font-thin { .font-thin {
font-weight: 100; font-weight: 100;
} }
.font-extralight { .font-extralight {
font-weight: 200; font-weight: 200;
} }
.font-light { .font-light {
font-weight: 300; font-weight: 300;
} }
.font-normal { .font-normal {
font-weight: 400; font-weight: 400;
} }
.font-medium { .font-medium {
font-weight: 500; font-weight: 500;
} }
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
.font-extrabold { .font-extrabold {
font-weight: 800; font-weight: 800;
} }
.font-black { .font-black {
font-weight: 900; font-weight: 900;
} }
.rounded-xs { .rounded-xs {
border-radius: 0.125rem; border-radius: 0.125rem;
} }
.rounded-sm { .rounded-sm {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.rounded-md { .rounded-md {
border-radius: 0.375rem; border-radius: 0.375rem;
} }
.rounded-lg { .rounded-lg {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.rounded-xl { .rounded-xl {
border-radius: 0.75rem; border-radius: 0.75rem;
} }
.rounded-2xl { .rounded-2xl {
border-radius: 1rem; border-radius: 1rem;
} }
.rounded-3xl { .rounded-3xl {
border-radius: 1.5rem; border-radius: 1.5rem;
} }
.rounded-4xl { .rounded-4xl {
border-radius: 2rem; border-radius: 2rem;
} }
.rounded-none { .rounded-none {
border-radius: 0; border-radius: 0;
@@ -83,20 +88,20 @@
// ─── CSS custom properties ───────────────────────────────────────────────── // ─── CSS custom properties ─────────────────────────────────────────────────
:root { :root {
--spacing: 0.25rem; --spacing: 0.25rem;
--container-3xs: 16rem; --container-3xs: 16rem;
--container-2xs: 18rem; --container-2xs: 18rem;
--container-xs: 20rem; --container-xs: 20rem;
--container-sm: 24rem; --container-sm: 24rem;
--container-md: 28rem; --container-md: 28rem;
--container-lg: 32rem; --container-lg: 32rem;
--container-xl: 36rem; --container-xl: 36rem;
--container-2xl: 42rem; --container-2xl: 42rem;
--container-3xl: 48rem; --container-3xl: 48rem;
--container-4xl: 56rem; --container-4xl: 56rem;
--container-5xl: 64rem; --container-5xl: 64rem;
--container-6xl: 72rem; --container-6xl: 72rem;
--container-7xl: 80rem; --container-7xl: 80rem;
} }
// ─── Shared mixin ────────────────────────────────────────────────────────── // ─── Shared mixin ──────────────────────────────────────────────────────────
@@ -108,9 +113,16 @@
// ─── Class types ─────────────────────────────────────────────────────────── // ─── Class types ───────────────────────────────────────────────────────────
$class-types: ( $class-types: (
"w": (width), "w": (
"h": (height), width,
"size": (width, height), ),
"h": (
height,
),
"size": (
width,
height,
),
); );
// ─── Numeric: w-0 → w-48, h-0 → h-48, size-0 → size-48 ─────────────────── // ─── Numeric: w-0 → w-48, h-0 → h-48, size-0 → size-48 ───────────────────
@@ -124,32 +136,110 @@ $class-types: (
// ─── Fractions ───────────────────────────────────────────────────────────── // ─── Fractions ─────────────────────────────────────────────────────────────
$fractions: ( $fractions: (
"1\\/2": (1, 2), "1\\/2": (
"1\\/3": (1, 3), 1,
"2\\/3": (2, 3), 2,
"1\\/4": (1, 4), ),
"2\\/4": (2, 4), "1\\/3": (
"3\\/4": (3, 4), 1,
"1\\/5": (1, 5), 3,
"2\\/5": (2, 5), ),
"3\\/5": (3, 5), "2\\/3": (
"4\\/5": (4, 5), 2,
"1\\/6": (1, 6), 3,
"2\\/6": (2, 6), ),
"3\\/6": (3, 6), "1\\/4": (
"4\\/6": (4, 6), 1,
"5\\/6": (5, 6), 4,
"1\\/12": (1, 12), ),
"2\\/12": (2, 12), "2\\/4": (
"3\\/12": (3, 12), 2,
"4\\/12": (4, 12), 4,
"5\\/12": (5, 12), ),
"6\\/12": (6, 12), "3\\/4": (
"7\\/12": (7, 12), 3,
"8\\/12": (8, 12), 4,
"9\\/12": (9, 12), ),
"10\\/12": (10, 12), "1\\/5": (
"11\\/12": (11, 12), 1,
5,
),
"2\\/5": (
2,
5,
),
"3\\/5": (
3,
5,
),
"4\\/5": (
4,
5,
),
"1\\/6": (
1,
6,
),
"2\\/6": (
2,
6,
),
"3\\/6": (
3,
6,
),
"4\\/6": (
4,
6,
),
"5\\/6": (
5,
6,
),
"1\\/12": (
1,
12,
),
"2\\/12": (
2,
12,
),
"3\\/12": (
3,
12,
),
"4\\/12": (
4,
12,
),
"5\\/12": (
5,
12,
),
"6\\/12": (
6,
12,
),
"7\\/12": (
7,
12,
),
"8\\/12": (
8,
12,
),
"9\\/12": (
9,
12,
),
"10\\/12": (
10,
12,
),
"11\\/12": (
11,
12,
),
); );
@each $prefix, $props in $class-types { @each $prefix, $props in $class-types {
@@ -163,13 +253,12 @@ $fractions: (
} }
// ─── Container sizes (w- only) ───────────────────────────────────────────── // ─── Container sizes (w- only) ─────────────────────────────────────────────
$containers: ( $containers: ("3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl");
"3xs", "2xs", "xs", "sm", "md", "lg", "xl",
"2xl", "3xl", "4xl", "5xl", "6xl", "7xl"
);
@each $name in $containers { @each $name in $containers {
.w-#{$name} { width: var(--container-#{$name}); } .w-#{$name} {
width: var(--container-#{$name});
}
} }
// ─── Shared keywords (auto, px, full, min, max, fit) ─────────────────────── // ─── Shared keywords (auto, px, full, min, max, fit) ───────────────────────
@@ -178,9 +267,9 @@ $shared-keywords: (
"auto": auto, "auto": auto,
"px": 1px, "px": 1px,
"full": 100%, "full": 100%,
"min": min-content, "min": min-content,
"max": max-content, "max": max-content,
"fit": fit-content, "fit": fit-content,
); );
@each $prefix, $props in $class-types { @each $prefix, $props in $class-types {
@@ -210,5 +299,9 @@ $viewport-keywords: (
} }
} }
.w-screen { width: 100vw; } .w-screen {
.h-screen { height: 100vh; } width: 100vw;
}
.h-screen {
height: 100vh;
}

View File

@@ -1,14 +1,21 @@
<template> <template>
<span :class="`icon-text fsb-${props.size||17} ${props.type || 'has-text-primary'}`"> <span :class="`icon-text fsb-${props.size || 17} ${props.type || 'has-text-primary'}`">
<span>{{ title }}</span> <span>{{ title }}</span>
<SvgIcon id="ignore" v-bind="{name: 'right.svg', type: props.type? props.type.replace('has-text-', '') : null, <SvgIcon
size: (props.size>=30? props.size*0.7 : props.size) || 20, alt: 'Mũi tên chỉ hướng'}"></SvgIcon> id="ignore"
v-bind="{
name: 'right.svg',
type: props.type ? props.type.replace('has-text-', '') : null,
size: (props.size >= 30 ? props.size * 0.7 : props.size) || 20,
alt: 'Mũi tên chỉ hướng',
}"
></SvgIcon>
</span> </span>
</template> </template>
<script setup> <script setup>
var props = defineProps({ var props = defineProps({
type: String, type: String,
size: Number, size: Number,
title: String title: String,
}) });
</script> </script>

View File

@@ -1,6 +1,9 @@
<template> <template>
<Teleport to="#__nuxt > div"> <Teleport to="#__nuxt > div">
<div class="modal is-active" @click="doClick"> <div
class="modal is-active"
@click="doClick"
>
<div <div
class="modal-background" class="modal-background"
:style="`opacity:${count === 0 ? 0.7 : 0.3} !important;`" :style="`opacity:${count === 0 ? 0.7 : 0.3} !important;`"
@@ -10,14 +13,23 @@
:id="docid" :id="docid"
:style="`width:${vWidth}; border-radius:16px;`" :style="`width:${vWidth}; border-radius:16px;`"
> >
<header class="modal-card-head my-0 py-2" v-if="title"> <header
class="modal-card-head my-0 py-2"
v-if="title"
>
<div style="width: 100%"> <div style="width: 100%">
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control is-expanded has-text-left"> <div class="control is-expanded has-text-left">
<p class="fsb-18 has-text-primary" v-html="title"></p> <p
class="fsb-18 has-text-primary"
v-html="title"
></p>
</div> </div>
<div class="control has-text-right"> <div class="control has-text-right">
<button class="delete is-medium" @click="closeModal()"></button> <button
class="delete is-medium"
@click="closeModal()"
></button>
</div> </div>
</div> </div>
</div> </div>
@@ -49,14 +61,13 @@ const store = useStore();
const { $id } = useNuxtApp(); const { $id } = useNuxtApp();
const props = defineProps({ const props = defineProps({
component: String, component: String,
width: String, width: String,
height: String, height: String,
vbind: Object, vbind: Object,
title: String, title: String,
}); });
const componentFiles = import.meta.glob("@/components/**/*.vue"); const componentFiles = import.meta.glob("@/components/**/*.vue");
const resolvedComponent = shallowRef(null); const resolvedComponent = shallowRef(null);
@@ -68,10 +79,8 @@ function loadDynamicComponent() {
} }
const fullPath = `/components/${props.component}.vue`; const fullPath = `/components/${props.component}.vue`;
const componentPath = Object.keys(componentFiles).find((path) => const componentPath = Object.keys(componentFiles).find((path) => path.endsWith(fullPath));
path.endsWith(fullPath)
);
if (componentPath) { if (componentPath) {
resolvedComponent.value = defineAsyncComponent(componentFiles[componentPath]); resolvedComponent.value = defineAsyncComponent(componentFiles[componentPath]);
@@ -118,7 +127,7 @@ const doClick = function (e) {
}; };
onMounted(() => { onMounted(() => {
document.documentElement.classList.add('is-clipped'); document.documentElement.classList.add("is-clipped");
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeModal(); if (e.key === "Escape") closeModal();
}); });
@@ -128,6 +137,6 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
count--; count--;
if (count === 0) document.documentElement.classList.remove('is-clipped'); if (count === 0) document.documentElement.classList.remove("is-clipped");
}) });
</script> </script>

View File

@@ -1,49 +1,110 @@
<template> <template>
<div> <div>
<div class="field has-addons" :id="docid"> <div
class="field has-addons"
:id="docid"
>
<div class="control has-icons-left is-expanded"> <div class="control has-icons-left is-expanded">
<div :class="`dropdown ${ pos || ''} ${focused? 'is-active' : ''}`" style="width: 100%;"> <div
<div class="dropdown-trigger" style="width: 100%;"> :class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`"
<input style="width: 100%"
:disabled="disabled" >
:class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-dark' : ''}`" <div
type="text" class="dropdown-trigger"
@focus="setFocus" style="width: 100%"
@blur="lostFocus" >
@keyup.enter="pressEnter" <input
@keyup="beginSearch" :disabled="disabled"
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-dark' : ''}`"
type="text"
@focus="setFocus"
@blur="lostFocus"
@keyup.enter="pressEnter"
@keyup="beginSearch"
v-model="value" v-model="value"
:placeholder="placeholder" :placeholder="placeholder"
/> />
</div> </div>
<div class="dropdown-menu" style="min-width: 100%" role="menu" @click="doClick()"> <div
<div class="dropdown-content px-3" style="min-width: 100%;"> class="dropdown-menu"
<p class="has-text-warning" v-if="data.length===0">{{ isVietnamese ? 'Không giá trị thỏa mãn' : 'No matching values' }}</p> style="min-width: 100%"
<ScrollBox v-bind="{data: data, name: field, fontsize: 14, maxheight: '200px', notick: true}" @selected="choose" v-else></ScrollBox> role="menu"
@click="doClick()"
>
<div
class="dropdown-content px-3"
style="min-width: 100%"
>
<p
class="has-text-warning"
v-if="data.length === 0"
>
{{ isVietnamese ? "Không có giá trị thỏa mãn" : "No matching values" }}
</p>
<ScrollBox
v-bind="{
data: data,
name: field,
fontsize: 14,
maxheight: '200px',
notick: true,
}"
@selected="choose"
v-else
></ScrollBox>
</div> </div>
</div> </div>
</div> </div>
<span class="icon is-left"> <span class="icon is-left">
<SvgIcon v-bind="{name: 'magnify.svg', type: 'gray', size: 22}"></SvgIcon> <SvgIcon v-bind="{ name: 'magnify.svg', type: 'gray', size: 22 }"></SvgIcon>
</span> </span>
</div> </div>
<div class="control" v-if="clearable && value"> <div
<button class="button is-primary px-2" @click="clearValue" style="height: 100%" type="button"> class="control"
<SvgIcon v-bind="{name: 'close.svg', type: 'white', size: 24}"></SvgIcon> v-if="clearable && value"
>
<button
class="button is-primary px-2"
@click="clearValue"
style="height: 100%"
type="button"
>
<SvgIcon v-bind="{ name: 'close.svg', type: 'white', size: 24 }"></SvgIcon>
</button> </button>
</div> </div>
<div class="control" v-if="viewaddon"> <div
<button class="button is-dark px-2" @click="viewInfo()" style="height: 100%" type="button"> class="control"
<SvgIcon v-bind="{name: 'view.svg', type: 'white', size: 24}"></SvgIcon> v-if="viewaddon"
>
<button
class="button is-dark px-2"
@click="viewInfo()"
style="height: 100%"
type="button"
>
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 24 }"></SvgIcon>
</button> </button>
</div> </div>
<div class="control" v-if="addon"> <div
<button class="button is-primary px-2" @click="addNew()" style="height: 100%" type="button"> class="control"
<SvgIcon v-bind="{name: 'add1.png', type: 'white', size: 24}"></SvgIcon> v-if="addon"
>
<button
class="button is-primary px-2"
@click="addNew()"
style="height: 100%"
type="button"
>
<SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 24 }"></SvgIcon>
</button> </button>
</div> </div>
</div> </div>
<Modal @dataevent="dataevent" @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@dataevent="dataevent"
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div> </div>
</template> </template>
@@ -51,7 +112,22 @@
import { useStore } from "@/stores/index"; import { useStore } from "@/stores/index";
export default { export default {
props: ['api', 'field', 'column', 'first', 'optionid', 'filter', 'addon', 'viewaddon', 'position', 'disabled', 'vdata', 'clearable', 'placeholder', 'searchfield'], props: [
"api",
"field",
"column",
"first",
"optionid",
"filter",
"addon",
"viewaddon",
"position",
"disabled",
"vdata",
"clearable",
"placeholder",
"searchfield",
],
setup() { setup() {
const store = useStore(); const store = useStore();
return { store }; return { store };
@@ -64,7 +140,7 @@ export default {
value: undefined, value: undefined,
selected: undefined, selected: undefined,
showmodal: undefined, showmodal: undefined,
params: this.api? this.$findapi(this.api)['params'] : undefined, params: this.api ? this.$findapi(this.api)["params"] : undefined,
orgdata: undefined, orgdata: undefined,
error: false, error: false,
focused: false, focused: false,
@@ -72,142 +148,142 @@ export default {
count2: 0, count2: 0,
docid: this.$id(), docid: this.$id(),
pos: undefined, pos: undefined,
} };
}, },
computed: { computed: {
isVietnamese() { isVietnamese() {
return this.store.lang === "vi"; return this.store.lang === "vi";
} },
}, },
async created() { async created() {
this.getPos() this.getPos();
if(this.vdata) { if (this.vdata) {
this.orgdata = this.$copy(this.vdata) this.orgdata = this.$copy(this.vdata);
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field])) this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
} }
if(this.first) { if (this.first) {
this.data = this.orgdata? this.$copy(this.orgdata) : await this.getData() this.data = this.orgdata ? this.$copy(this.orgdata) : await this.getData();
if(this.optionid) { if (this.optionid) {
let f = {} let f = {};
f[this.field] = this.optionid f[this.field] = this.optionid;
if(this.optionid>0) f = {id: this.optionid} if (this.optionid > 0) f = { id: this.optionid };
this.selected = this.$find(this.data, f) this.selected = this.$find(this.data, f);
if(this.selected && this.vdata) { if (this.selected && this.vdata) {
return this.value = this.selected[this.field] return (this.value = this.selected[this.field]);
} }
} }
} else if(this.optionid) { } else if (this.optionid) {
this.selected = await this.$getdata(this.api, {id: this.optionid}, undefined, true) this.selected = await this.$getdata(this.api, { id: this.optionid }, undefined, true);
} }
if(this.selected) this.doSelect(this.selected) if (this.selected) this.doSelect(this.selected);
}, },
watch: { watch: {
optionid: function(newVal) { optionid: function (newVal) {
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid}) if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
if(this.selected) this.doSelect(this.selected) if (this.selected) this.doSelect(this.selected);
else this.value = undefined else this.value = undefined;
}, },
filter: async function(newVal) { filter: async function (newVal) {
this.data = await this.getData() this.data = await this.getData();
}, },
vdata: function(newval) { vdata: function (newval) {
if(newval) { if (newval) {
this.orgdata = this.$copy(this.vdata) this.orgdata = this.$copy(this.vdata);
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field])) this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
this.data = this.$copy(this.orgdata) this.data = this.$copy(this.orgdata);
this.selected = undefined this.selected = undefined;
this.value = undefined this.value = undefined;
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid}) if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
if(this.selected) this.doSelect(this.selected) if (this.selected) this.doSelect(this.selected);
} }
} },
}, },
methods: { methods: {
choose(v) { choose(v) {
this.focused = false this.focused = false;
this.count1 = 0 this.count1 = 0;
this.count2 = 0 this.count2 = 0;
this.doSelect(v) this.doSelect(v);
}, },
setFocus() { setFocus() {
this.focused = true this.focused = true;
this.count1 = 0 this.count1 = 0;
this.count2 = 0 this.count2 = 0;
}, },
lostFocus() { lostFocus() {
let self = this let self = this;
setTimeout(()=>{ setTimeout(() => {
if(self.focused && self.count1===0) self.focused = false if (self.focused && self.count1 === 0) self.focused = false;
}, 200) }, 200);
}, },
pressEnter() { pressEnter() {
if(this.data.length===0) return if (this.data.length === 0) return;
this.choose(this.data[0]) this.choose(this.data[0]);
}, },
doClick() { doClick() {
this.count1 += 1 this.count1 += 1;
}, },
doSelect(option) { doSelect(option) {
if(this.$empty(option)) return if (this.$empty(option)) return;
this.$emit('option', option) this.$emit("option", option);
this.$emit('modalevent', {name: 'option', data: option}) this.$emit("modalevent", { name: "option", data: option });
this.selected = option this.selected = option;
this.value = this.selected[this.field] this.value = this.selected[this.field];
}, },
clearValue() { clearValue() {
this.value = undefined this.value = undefined;
this.selected = undefined this.selected = undefined;
this.$emit('option', null) this.$emit("option", null);
this.$emit('modalevent', {name: 'option', data: null}) this.$emit("modalevent", { name: "option", data: null });
}, },
findObject(val) { findObject(val) {
let rows = this.$copy(this.orgdata) let rows = this.$copy(this.orgdata);
if(this.$empty(val)) this.data = rows if (this.$empty(val)) this.data = rows;
else { else {
let text = this.$nonAccent(val) let text = this.$nonAccent(val);
this.data = rows.filter(v=>v.search.toLowerCase().indexOf(text.toLowerCase())>=0) this.data = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
} }
}, },
async getData() { async getData() {
this.params.filter = this.filter this.params.filter = this.filter;
let data = await this.$getdata(this.api, undefined, this.params) let data = await this.$getdata(this.api, undefined, this.params);
return data return data;
}, },
async getApi(val) { async getApi(val) {
if(this.vdata) return this.findObject(val) if (this.vdata) return this.findObject(val);
let text = val? val.toLowerCase() : '' let text = val ? val.toLowerCase() : "";
let f = {} let f = {};
// Sử dụng searchfield nếu có, nếu không thì dùng column // Sử dụng searchfield nếu có, nếu không thì dùng column
const fieldsToSearch = this.searchfield || this.column; const fieldsToSearch = this.searchfield || this.column;
fieldsToSearch.map(v=>{ fieldsToSearch.map((v) => {
f[`${v}__icontains`] = text f[`${v}__icontains`] = text;
}) });
this.params.filter_or = f this.params.filter_or = f;
if(this.filter) this.params.filter = this.$copy(this.filter) if (this.filter) this.params.filter = this.$copy(this.filter);
let arr = await this.$getdata(this.api, undefined, this.params) let arr = await this.$getdata(this.api, undefined, this.params);
this.data = this.$copy(arr) this.data = this.$copy(arr);
}, },
beginSearch(e) { beginSearch(e) {
let val = e.target.value let val = e.target.value;
this.search = val this.search = val;
if (this.timer) clearTimeout(this.timer) if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.getApi(val), 150) this.timer = setTimeout(() => this.getApi(val), 150);
}, },
addNew() { addNew() {
this.showmodal = this.$copy(this.addon) this.showmodal = this.$copy(this.addon);
}, },
dataevent(v) { dataevent(v) {
console.log("SearchBox received dataevent:", v); // Debug log console.log("SearchBox received dataevent:", v); // Debug log
if (!v || !v.id) { if (!v || !v.id) {
console.error("Invalid data received in SearchBox:", v); console.error("Invalid data received in SearchBox:", v);
return; return;
} }
// Tìm và cập nhật trong danh sách // Tìm và cập nhật trong danh sách
let idx = this.$findIndex(this.data, {id: v.id}) let idx = this.$findIndex(this.data, { id: v.id });
if (idx < 0) { if (idx < 0) {
// Nếu chưa có trong danh sách, thêm vào đầu // Nếu chưa có trong danh sách, thêm vào đầu
this.data.unshift(v); this.data.unshift(v);
@@ -217,10 +293,10 @@ export default {
this.data[idx] = v; this.data[idx] = v;
console.log("Updated existing item in data:", v); console.log("Updated existing item in data:", v);
} }
// Cập nhật orgdata nếu có // Cập nhật orgdata nếu có
if (this.orgdata) { if (this.orgdata) {
let orgIdx = this.$findIndex(this.orgdata, {id: v.id}); let orgIdx = this.$findIndex(this.orgdata, { id: v.id });
if (orgIdx < 0) { if (orgIdx < 0) {
this.orgdata.unshift(v); this.orgdata.unshift(v);
// Thêm search field cho orgdata // Thêm search field cho orgdata
@@ -234,42 +310,46 @@ export default {
} }
} }
} }
// **Tự động select item vừa tạo/cập nhật** // **Tự động select item vừa tạo/cập nhật**
this.doSelect(v); this.doSelect(v);
// Đóng modal // Đóng modal
this.showmodal = undefined; this.showmodal = undefined;
console.log("SearchBox data after update:", this.data); console.log("SearchBox data after update:", this.data);
}, },
viewInfo() { viewInfo() {
if(!this.selected) return this.$dialog(this.isVietnamese ? 'Vui lòng lựa chọn trước khi xem thông tin.' : 'Please select before viewing', this.isVietnamese ? 'Thông báo' : 'Notice') if (!this.selected)
let copy = this.$copy(this.viewaddon) return this.$dialog(
copy.vbind = {row: this.selected} this.isVietnamese ? "Vui lòng lựa chọn trước khi xem thông tin." : "Please select before viewing",
this.showmodal = copy this.isVietnamese ? "Thông báo" : "Notice",
);
let copy = this.$copy(this.viewaddon);
copy.vbind = { row: this.selected };
this.showmodal = copy;
}, },
getPos() { getPos() {
switch(this.position) { switch (this.position) {
case 'is-top-left': case "is-top-left":
this.pos = 'is-up is-left' this.pos = "is-up is-left";
break; break;
case 'is-top-right': case "is-top-right":
this.pos = 'is-up is-right' this.pos = "is-up is-right";
break; break;
case 'is-bottom-left': case "is-bottom-left":
this.pos = 'is-right' this.pos = "is-right";
break; break;
case 'is-bottom-right': case "is-bottom-right":
this.pos = 'is-right' this.pos = "is-right";
break; break;
} }
} },
} },
} };
</script> </script>
<style scoped> <style scoped>
.field:not(:last-child) { .field:not(:last-child) {
margin-bottom: 0; margin-bottom: 0;
} }
</style> </style>

View File

@@ -13,7 +13,6 @@ export default {
}; };
</script> </script>
<style> <style>
/* primary: $blue-dianne (#204853) */ /* primary: $blue-dianne (#204853) */
.svg-primary { .svg-primary {
filter: invert(19%) sepia(18%) saturate(1514%) hue-rotate(151deg) brightness(97%) contrast(85%); filter: invert(19%) sepia(18%) saturate(1514%) hue-rotate(151deg) brightness(97%) contrast(85%);
@@ -31,7 +30,7 @@ export default {
/* findata/info/warning: $sirocco (#758385) */ /* findata/info/warning: $sirocco (#758385) */
/* Cả ba đều dùng chung bộ lọc này */ /* Cả ba đều dùng chung bộ lọc này */
.svg-findata, .svg-findata,
.svg-info, .svg-info,
.svg-warning { .svg-warning {
filter: invert(56%) sepia(10%) saturate(301%) hue-rotate(167deg) brightness(92%) contrast(82%); filter: invert(56%) sepia(10%) saturate(301%) hue-rotate(167deg) brightness(92%) contrast(82%);

View File

@@ -1,14 +1,17 @@
<template> <template>
<span class="tooltip"> <span class="tooltip">
<span v-html="props.html"></span> <span v-html="props.html"></span>
<span class="tooltiptext" v-html="props.tooltip"></span> <span
class="tooltiptext"
v-html="props.tooltip"
></span>
</span> </span>
</template> </template>
<script setup> <script setup>
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from "vue";
var props = defineProps({ var props = defineProps({
html: String, html: String,
tooltip: String, tooltip: String,
width: Number width: Number,
}) });
</script> </script>

View File

@@ -1,17 +1,31 @@
<template> <template>
<nav class="navbar has-shadow sticky px-3" style="top: 0" role="navigation"> <nav
class="navbar has-shadow sticky px-3"
style="top: 0"
role="navigation"
>
<div class="navbar-brand mr-5"> <div class="navbar-brand mr-5">
<span class="navbar-item is-gap-1"> <span class="navbar-item is-gap-1">
<div style="width: 16px; height: 16px" class="has-background-primary rounded-full"></div> <div class="size-4 has-background-primary rounded-full"></div>
<span class="fs-17 font-semibold has-text-primary">{{ $dayjs().format('DD/MM') }}</span> <span class="fs-17 font-semibold has-text-primary">{{ $dayjs().format("DD/MM") }}</span>
</span> </span>
<a <a
class="navbar-item p-0 has-text-primary" class="navbar-item p-0 has-text-primary"
@click="changeTab(leftmenu[0])" @click="changeTab(leftmenu[0])"
> >
<svg style="max-height: none; width: 44px" width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
<path d="M40.5 6C59.0015 6 74 20.9985 74 39.5C74 58.0015 59.0015 73 40.5 73C21.9985 73 7 58.0015 7 39.5C7 20.9985 21.9985 6 40.5 6ZM17.9834 29.0654V48.2373H30.9395V44.8955H22.0371V40.3174H30.2373V36.9756H22.0371V32.4072H30.9014V29.0654H17.9834ZM33.8604 48.2373H37.9141V41.4404H40.873L44.5039 48.2373H48.9785L44.9092 40.7852C44.9677 40.7598 45.0277 40.7378 45.085 40.7109C46.1269 40.2242 46.9225 39.5252 47.4717 38.6143C48.0209 37.6969 48.2959 36.6012 48.2959 35.3281C48.2959 34.0613 48.0244 32.9595 47.4814 32.0234C46.9448 31.0812 46.1613 30.3545 45.1318 29.8428C44.1085 29.3248 42.8725 29.0655 41.4248 29.0654H33.8604V48.2373ZM50.8965 48.2373H54.9492V42.0215H58.3574C59.83 42.0214 61.0843 41.7499 62.1201 41.207C63.1623 40.6641 63.9577 39.9052 64.5068 38.9316C65.0559 37.9582 65.331 36.8354 65.3311 35.5625C65.3311 34.2895 65.0595 33.1659 64.5166 32.1924C63.9799 31.2127 63.2001 30.4476 62.1768 29.8984C61.1533 29.343 59.914 29.0654 58.46 29.0654H50.8965V48.2373ZM57.6826 32.3789C58.4689 32.3789 59.1182 32.5139 59.6299 32.7822C60.1416 33.0443 60.5228 33.4151 60.7725 33.8955C61.0283 34.3698 61.1562 34.9259 61.1562 35.5625C61.1562 36.1925 61.0281 36.7507 60.7725 37.2373C60.5228 37.7178 60.1416 38.0955 59.6299 38.3701C59.1245 38.6384 58.482 38.7734 57.7021 38.7734H54.9492V32.3789H57.6826ZM40.6475 32.3789C41.4274 32.3789 42.0733 32.4948 42.585 32.7256C43.1028 32.9502 43.4867 33.2811 43.7363 33.7178C43.9922 34.1546 44.1201 34.6916 44.1201 35.3281C44.1201 35.9584 43.9922 36.4858 43.7363 36.9102C43.4867 37.3344 43.1064 37.6531 42.5947 37.8652C42.083 38.0774 41.4399 38.1836 40.666 38.1836H37.9141V32.3789H40.6475Z" fill="currentColor"/> style="max-height: none; width: 44px"
</svg> width="80"
height="80"
viewBox="0 0 80 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M40.5 6C59.0015 6 74 20.9985 74 39.5C74 58.0015 59.0015 73 40.5 73C21.9985 73 7 58.0015 7 39.5C7 20.9985 21.9985 6 40.5 6ZM17.9834 29.0654V48.2373H30.9395V44.8955H22.0371V40.3174H30.2373V36.9756H22.0371V32.4072H30.9014V29.0654H17.9834ZM33.8604 48.2373H37.9141V41.4404H40.873L44.5039 48.2373H48.9785L44.9092 40.7852C44.9677 40.7598 45.0277 40.7378 45.085 40.7109C46.1269 40.2242 46.9225 39.5252 47.4717 38.6143C48.0209 37.6969 48.2959 36.6012 48.2959 35.3281C48.2959 34.0613 48.0244 32.9595 47.4814 32.0234C46.9448 31.0812 46.1613 30.3545 45.1318 29.8428C44.1085 29.3248 42.8725 29.0655 41.4248 29.0654H33.8604V48.2373ZM50.8965 48.2373H54.9492V42.0215H58.3574C59.83 42.0214 61.0843 41.7499 62.1201 41.207C63.1623 40.6641 63.9577 39.9052 64.5068 38.9316C65.0559 37.9582 65.331 36.8354 65.3311 35.5625C65.3311 34.2895 65.0595 33.1659 64.5166 32.1924C63.9799 31.2127 63.2001 30.4476 62.1768 29.8984C61.1533 29.343 59.914 29.0654 58.46 29.0654H50.8965V48.2373ZM57.6826 32.3789C58.4689 32.3789 59.1182 32.5139 59.6299 32.7822C60.1416 33.0443 60.5228 33.4151 60.7725 33.8955C61.0283 34.3698 61.1562 34.9259 61.1562 35.5625C61.1562 36.1925 61.0281 36.7507 60.7725 37.2373C60.5228 37.7178 60.1416 38.0955 59.6299 38.3701C59.1245 38.6384 58.482 38.7734 57.7021 38.7734H54.9492V32.3789H57.6826ZM40.6475 32.3789C41.4274 32.3789 42.0733 32.4948 42.585 32.7256C43.1028 32.9502 43.4867 33.2811 43.7363 33.7178C43.9922 34.1546 44.1201 34.6916 44.1201 35.3281C44.1201 35.9584 43.9922 36.4858 43.7363 36.9102C43.4867 37.3344 43.1064 37.6531 42.5947 37.8652C42.083 38.0774 41.4399 38.1836 40.666 38.1836H37.9141V32.3789H40.6475Z"
fill="currentColor"
/>
</svg>
</a> </a>
<a <a
role="button" role="button"
@@ -28,30 +42,45 @@
<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 is-gap-1 is-align-items-center" > class="navbar-menu"
<template v-for="(v, i) in leftmenu" :key="i" :id="v.code"> id="navMenu"
<a class="navbar-item rounded-lg is-clipped p-0" v-if="!v.submenu" @click="changeTab(v)"> >
<span :class="[ <div class="navbar-start is-gap-1 is-align-items-center">
'px-2 py-2 font-medium', <template
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30' v-for="(v, i) in leftmenu"
:key="i"
:id="v.code"
>
<a
v-if="!v.submenu"
:class="[
'navbar-item rounded-lg is-clipped font-medium',
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30',
]" ]"
style="font-size: 13.5px"
@click="changeTab(v)"
>
{{ v[lang] }}
</a>
<div
v-else
class="navbar-item rounded-lg has-dropdown is-hoverable"
>
<a
:class="[
'navbar-link rounded-lg is-arrowless font-medium',
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30',
]"
@click="changeTab(v)"
style="font-size: 13.5px" style="font-size: 13.5px"
> >
{{ v[lang] }} <p class="is-flex is-align-items-center">
</span>
</a>
<div class="navbar-item rounded-lg has-dropdown is-hoverable" v-else>
<a class="navbar-link rounded-lg p-0" @click="changeTab(v)">
<p
:class="[
'px-2 py-2 rounded-lg font-medium is-flex is-align-items-center',
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
]"
style="font-size: 13.5px"
>
<span>{{ v[lang] }}</span> <span>{{ v[lang] }}</span>
<Icon name="material-symbols:keyboard-arrow-down-rounded" :size="20" /> <Icon
name="material-symbols:keyboard-arrow-down-rounded"
:size="20"
/>
</p> </p>
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
@@ -76,19 +105,23 @@
</a> </a>
</div> --> </div> -->
<div class="navbar-end"> <div class="navbar-end">
<a class="navbar-item"> <a class="navbar-item is-flex is-gap-2 is-justify-content-space-between is-align-items-center">
<div> <div>
<p class="fs-13">Xin chào,</p> <p class="fs-13">Xin chào,</p>
<p class="fs-14 font-bold">Quản </p> <p class="fs-14 font-bold">Quản </p>
</div> </div>
<Avatarbox text="Q" type="findata" size="two" /> <Avatarbox
text="Q"
type="findata"
size="two"
/>
</a> </a>
</div> </div>
</div> </div>
</nav> </nav>
</template> </template>
<script setup> <script setup>
import Avatarbox from '@/components/common/Avatarbox.vue'; import Avatarbox from "@/components/common/Avatarbox.vue";
import { watch } from "vue"; import { watch } from "vue";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@@ -99,229 +132,229 @@ const lang = ref($store.lang);
const menu = [ const menu = [
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'dashboard', code: "dashboard",
vi: 'Dashboard', vi: "Dashboard",
link: null, link: null,
detail: { detail: {
base: 'Dashboard', base: "Dashboard",
component: 'DashboardMaster', component: "DashboardMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 2, id: 2,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'orders', code: "orders",
vi: 'Đơn hàng', vi: "Đơn hàng",
link: null, link: null,
detail: { detail: {
base: 'Orders', base: "Orders",
component: 'OrdersMaster', component: "OrdersMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'inventory', code: "inventory",
vi: 'Tồn kho', vi: "Tồn kho",
link: null, link: null,
detail: { detail: {
base: 'Inventory', base: "Inventory",
component: 'InventoryMaster', component: "InventoryMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'rights', code: "rights",
vi: 'Phân quyền', vi: "Phân quyền",
link: null, link: null,
detail: { detail: {
base: 'Rights', base: "Rights",
component: 'RightsMaster', component: "RightsMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'POS', code: "POS",
vi: 'POS', vi: "POS",
link: null, link: null,
detail: { detail: {
base: 'POS', base: "POS",
component: 'POSMaster', component: "POSMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'receipts', code: "receipts",
vi: 'Hoá đơn', vi: "Hoá đơn",
link: null, link: null,
detail: { detail: {
base: 'Receipts', base: "Receipts",
component: 'ReceiptsMaster', component: "ReceiptsMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'imports', code: "imports",
vi: 'Nhập hàng', vi: "Nhập hàng",
link: null, link: null,
detail: { detail: {
base: 'Imports', base: "Imports",
component: 'ImportsMaster', component: "ImportsMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'exports', code: "exports",
vi: 'Xuất hàng', vi: "Xuất hàng",
link: null, link: null,
detail: { detail: {
base: 'Exports', base: "Exports",
component: 'ExportsMaster', component: "ExportsMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'inventory-transfer', code: "inventory-transfer",
vi: 'Chuyển kho', vi: "Chuyển kho",
link: null, link: null,
detail: { detail: {
base: 'InventoryTransfer', base: "InventoryTransfer",
component: 'InventoryTransferMaster', component: "InventoryTransferMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'inventory-count', code: "inventory-count",
vi: 'Kiểm kho', vi: "Kiểm kho",
link: null, link: null,
detail: { detail: {
base: 'InventoryCount', base: "InventoryCount",
component: 'InventoryCountMaster', component: "InventoryCountMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'cash-book', code: "cash-book",
vi: 'Sổ quỹ', vi: "Sổ quỹ",
link: null, link: null,
detail: { detail: {
base: 'CashBook', base: "CashBook",
component: 'CashBookMaster', component: "CashBookMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'topmenu', category: "topmenu",
classify: 'left', classify: "left",
code: 'report', code: "report",
vi: 'Báo cáo', vi: "Báo cáo",
link: null, link: null,
submenu: [ submenu: [
{ {
id: 1, id: 1,
category: 'submenu', category: "submenu",
classify: 'report', classify: "report",
code: 'ncc', code: "ncc",
vi: 'NCC', vi: "NCC",
link: null, link: null,
detail: { detail: {
base: 'NCC', base: "NCC",
component: 'NCCMaster', component: "NCCMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'submenu', category: "submenu",
classify: 'report', classify: "report",
code: 'customers', code: "customers",
vi: 'Khách hàng', vi: "Khách hàng",
link: null, link: null,
detail: { detail: {
base: 'Customers', base: "Customers",
component: 'CustomersMaster', component: "CustomersMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'submenu', category: "submenu",
classify: 'report', classify: "report",
code: 'goods', code: "goods",
vi: 'Hàng hoá', vi: "Hàng hoá",
link: null, link: null,
detail: { detail: {
base: 'Goods', base: "Goods",
component: 'GoodsMaster', component: "GoodsMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'submenu', category: "submenu",
classify: 'report', classify: "report",
code: 'report-cash-book', code: "report-cash-book",
vi: 'Sổ quỹ', vi: "Sổ quỹ",
link: null, link: null,
detail: { detail: {
base: 'ReportCashBook', base: "ReportCashBook",
component: 'ReportCashBookMaster', component: "ReportCashBookMaster",
}, },
index: 0, index: 0,
}, },
{ {
id: 1, id: 1,
category: 'submenu', category: "submenu",
classify: 'report', classify: "report",
code: 'finance', code: "finance",
vi: 'Tài chính', vi: "Tài chính",
link: null, link: null,
detail: { detail: {
base: 'Finance', base: "Finance",
component: 'FinanceMaster', component: "FinanceMaster",
}, },
index: 0, index: 0,
}, },
], ],
index: 0, index: 0,
}, },
] ];
// if($store.rights.length>0) { // if($store.rights.length>0) {
// menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0) // menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
// } // }
if(menu.length===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.') $snackbar($store.lang === "vi" ? "Bạn không có quyền truy cập" : "You do not have permission to access.");
} }
// menu.map(v=>{ // menu.map(v=>{
// let arr = $filter($store.common, {category: 'submenu', classify: v.code}) // let arr = $filter($store.common, {category: 'submenu', classify: v.code})
@@ -330,8 +363,8 @@ if(menu.length===0) {
// } // }
// v.submenu = arr.length>0? arr : null // v.submenu = arr.length>0? arr : null
// }) // })
var leftmenu = $filter(menu, {category: 'topmenu', classify: 'left'}) var leftmenu = $filter(menu, { category: "topmenu", classify: "left" });
var currentTab = ref(leftmenu.length>0? leftmenu[0] : undefined) var currentTab = ref(leftmenu.length > 0 ? leftmenu[0] : undefined);
var subTab = ref(); var subTab = ref();
var tabConfig = $find(menu, { code: "configuration" }); var tabConfig = $find(menu, { code: "configuration" });
var avatar = ref(); var avatar = ref();
@@ -364,11 +397,16 @@ function changeTab(tab, subtab) {
router.push({ query: query }); router.push({ query: query });
} }
function openProfile() { function openProfile() {
let modal = { component: "user/Profile", width: "1100px", height: "360px", title: $store.lang==='vi'? 'Thông tin cá nhân' : '"User profile"' }; let modal = {
component: "user/Profile",
width: "1100px",
height: "360px",
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? $find(menu, {code: route.query.tab}) : undefined let found = route.query.tab ? $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 = {
@@ -391,7 +429,7 @@ watch(
}; };
isAdmin.value = $store.login.type__code === "admin"; isAdmin.value = $store.login.type__code === "admin";
lang.value = $store.lang; lang.value = $store.lang;
} },
); );
</script> </script>
<style scoped> <style scoped>
@@ -413,20 +451,30 @@ watch(
} }
} }
@media screen and (max-width: 1023px) {
.navbar-brand {
margin-right: 0 !important;
}
.navbar-item,
a.navbar-link {
border-radius: 0;
}
.navbar-dropdown {
box-shadow: 0 0.2em 0 hsla(var(--bulma-scheme-h), var(--bulma-scheme-s), var(--bulma-scheme-invert-l), 0.1);
}
}
.navbar-item:hover { .navbar-item:hover {
.navbar-link { .navbar-link {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
> p { > p {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
} }
} }
</style>
.navbar-item > .navbar-link:after {
display: none;
}
</style>

View File

@@ -1,48 +1,51 @@
<template> <template>
<div v-if="record"> <div v-if="record">
<div class="columns is-multiline mx-0 mt-1" id="printable"> <div
<div class="column is-5"> class="columns is-multiline mx-0 mt-1"
<div class="field"> id="printable"
<label class="label">{{ $lang('code') }}:</label> >
<div class="control"> <div class="column is-5">
{{ `${record.code}` }} <div class="field">
<label class="label">{{ $lang("code") }}:</label>
<div class="control">
{{ `${record.code}` }}
</div>
</div> </div>
</div> </div>
</div> <div class="column is-7">
<div class="column is-7"> <div class="field">
<div class="field"> <label class="label">{{ $lang("account-type") }}:</label>
<label class="label">{{ $lang('account-type') }}:</label> <div class="control">
<div class="control"> {{ `${record.type__code} / ${record.type__name}` }}
{{ `${record.type__code} / ${record.type__name}` }} </div>
</div> </div>
</div> </div>
</div> <div class="column is-5">
<div class="column is-5"> <div class="field">
<div class="field"> <label class="label">{{ $lang("currency") }}:</label>
<label class="label">{{ $lang('currency') }}:</label> <div class="control">
<div class="control"> {{ `${record.currency__code} / ${record.currency__name}` }}
{{ `${record.currency__code} / ${record.currency__name}` }} </div>
</div> </div>
</div> </div>
</div> <div class="column is-7">
<div class="column is-7"> <div class="field">
<div class="field"> <label class="label">{{ $lang("balance") }}:</label>
<label class="label">{{ $lang('balance') }}:</label> <div class="control">
<div class="control"> {{ $numtoString(record.balance) }}
{{ $numtoString(record.balance) }} </div>
</div> <!--<p class="help is-findata">{{$vnmoney($formatNumber(record.balance))}}</p>-->
<!--<p class="help is-findata">{{$vnmoney($formatNumber(record.balance))}}</p>-->
</div>
</div>
<div class="column is-5">
<div class="field">
<label class="label">{{ $lang('open-date') }}:</label>
<div class="control">
{{ `${$dayjs(record.create_time).format('DD/MM/YYYY')}` }}
</div> </div>
</div> </div>
</div> <div class="column is-5">
<!--<div class="column is-7"> <div class="field">
<label class="label">{{ $lang("open-date") }}:</label>
<div class="control">
{{ `${$dayjs(record.create_time).format("DD/MM/YYYY")}` }}
</div>
</div>
</div>
<!--<div class="column is-7">
<div class="field"> <div class="field">
<label class="label">Chi nhánh:</label> <label class="label">Chi nhánh:</label>
<div class="control"> <div class="control">
@@ -50,30 +53,38 @@
</div> </div>
</div> </div>
</div> --> </div> -->
</div>
<div class="border-bottom"></div>
<div
class="mt-5"
id="ignore"
>
<button
class="button is-primary has-text-white"
@click="$exportpdf('printable', record.code)"
>
{{ $lang("print") }}
</button>
</div>
</div> </div>
<div class="border-bottom"></div>
<div class="mt-5" id="ignore">
<button class="button is-primary has-text-white" @click="$exportpdf('printable', record.code)">{{$lang('print')}}</button>
</div>
</div>
</template> </template>
<script> <script>
export default { export default {
props: ['row'], props: ["row"],
data() { data() {
return { return {
errors: {}, errors: {},
record: undefined record: undefined,
} };
},
async created() {
this.record = await this.$getdata("internalaccount", { id: this.row.account || this.row.id }, undefined, true);
},
methods: {
selected(attr, obj) {
this.record[attr] = obj;
if (attr === "_type") this.category = obj.category__code;
}, },
async created() { },
this.record = await this.$getdata('internalaccount', {id: this.row.account || this.row.id}, undefined, true) };
}, </script>
methods: {
selected(attr, obj) {
this.record[attr] = obj
if(attr==='_type') this.category = obj.category__code
}
}
}
</script>

View File

@@ -1,50 +1,60 @@
<!-- components/dialog/ConfirmDeleteEntry.vue -->
<template> <template>
<div class="has-text-centered"> <div class="has-text-centered">
<div class="mb-3 p-3">
<div class=" mb-3 p-3"> <p class="is-size-5 has-text-weight-semibold mb-4">Bạn chắc chắn muốn xóa bút toán này?</p>
<p class="is-size-5 has-text-weight-semibold mb-4"> <p class="mt-3 has-text-danger has-text-weight-semibold">
Bạn chắc chắn muốn xóa bút toán này? Hành động này <strong>không thể hoàn tác</strong>.<br />
</p>
<p class="mt-3 has-text-danger has-text-weight-semibold">
Hành động này <strong>không thể hoàn tác</strong>.<br>
Dữ liệu liên quan (nếu ) sẽ bị xóa vĩnh viễn. Dữ liệu liên quan (nếu ) sẽ bị xóa vĩnh viễn.
</p> </p>
</div> </div>
<div class="field is-grouped is-justify-content-center"> <div class="field is-grouped is-justify-content-center">
<!-- Captcha addon group - shown only when captcha is not confirmed --> <!-- Captcha addon group - shown only when captcha is not confirmed -->
<p class="control" v-if="!isConfirmed"> <div
class="control"
v-if="!isConfirmed"
>
<div class="field has-addons"> <div class="field has-addons">
<p class="control"> <p class="control">
<input <input
class="input" class="input"
type="text" type="text"
placeholder="Nhập mã xác nhận" placeholder="Nhập mã xác nhận"
v-model="userInputCaptcha" v-model="userInputCaptcha"
@keyup.enter="isConfirmed && confirmDelete()" @keyup.enter="isConfirmed && confirmDelete()"
> />
</p> </p>
<p class="control"> <p class="control">
<a class="button is-static has-text-weight-bold has-background-grey-lighter" <a
style="font-family: 'Courier New', monospace; letter-spacing: 2px;"> class="button is-static has-text-weight-bold has-background-grey-lighter"
style="font-family: &quot;Courier New&quot;, monospace; letter-spacing: 2px"
>
{{ captchaCode }} {{ captchaCode }}
</a> </a>
</p> </p>
<p class="control"> <p class="control">
<button class="button" @click="generateCaptcha" title="Tạo mã mới"> <button
class="button"
@click="generateCaptcha"
title="Tạo mã mới"
>
<span class="icon"> <span class="icon">
<SvgIcon name="refresh.svg" type="primary" :size="23" /> <SvgIcon
name="refresh.svg"
type="primary"
:size="23"
></SvgIcon>
</span> </span>
</button> </button>
</p> </p>
</div> </div>
</p> </div>
<!-- Action buttons --> <!-- Action buttons -->
<!-- Confirm button - shown only when captcha IS confirmed --> <!-- Confirm button - shown only when captcha IS confirmed -->
<p class="control" v-if="isConfirmed"> <p
class="control"
v-if="isConfirmed"
>
<button <button
class="button is-danger" class="button is-danger"
:class="{ 'is-loading': isDeleting }" :class="{ 'is-loading': isDeleting }"
@@ -69,75 +79,70 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from "vue";
import { useNuxtApp } from '#app' import { useNuxtApp } from "#app";
const props = defineProps({ const props = defineProps({
entryId: { entryId: {
type: [String, Number], type: [String, Number],
required: true required: true,
} },
}) });
const emit = defineEmits(['close', 'deleted']) const emit = defineEmits(["close", "deleted"]);
const { $snackbar ,$insertapi} = useNuxtApp() const { $snackbar, $insertapi } = useNuxtApp();
const isDeleting = ref(false) const isDeleting = ref(false);
const captchaCode = ref('') const captchaCode = ref("");
const userInputCaptcha = ref('') const userInputCaptcha = ref("");
const isConfirmed = computed(() => { const isConfirmed = computed(() => {
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== '' return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== "";
}) });
const generateCaptcha = () => { const generateCaptcha = () => {
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase() captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase();
userInputCaptcha.value = '' userInputCaptcha.value = "";
} };
// Initial generation // Initial generation
generateCaptcha() generateCaptcha();
const confirmDelete = async () => { const confirmDelete = async () => {
if (isDeleting.value || !isConfirmed.value) return if (isDeleting.value || !isConfirmed.value) return;
isDeleting.value = true isDeleting.value = true;
try { try {
// Gọi API xóa theo đúng endpoint delete-entry/{id} // Gọi API xóa theo đúng endpoint delete-entry/{id}
const result = await $insertapi('deleteentry', {id: props.entryId}) const result = await $insertapi("deleteentry", { id: props.entryId });
if (result === 'error' || !result) { if (result === "error" || !result) {
throw new Error('API xóa trả về lỗi') throw new Error("API xóa trả về lỗi");
} }
$snackbar( $snackbar(`Đã xóa bút toán ID ${props.entryId} thành công`, "Thành công", "Success");
`Đã xóa bút toán ID ${props.entryId} thành công`,
'Thành công',
'Success'
)
emit('deleted', props.entryId)
emit('close')
emit("deleted", props.entryId);
emit("close");
} catch (err) { } catch (err) {
console.error('Xóa bút toán thất bại:', err) console.error("Xóa bút toán thất bại:", err);
let errorMsg = "Không thể xóa bút toán. Vui lòng thử lại.";
let errorMsg = 'Không thể xóa bút toán. Vui lòng thử lại.'
// Nếu backend trả về thông báo cụ thể // Nếu backend trả về thông báo cụ thể
if (err?.response?.data?.detail) { if (err?.response?.data?.detail) {
errorMsg = err.response.data.detail errorMsg = err.response.data.detail;
} else if (err?.response?.data?.non_field_errors) { } else if (err?.response?.data?.non_field_errors) {
errorMsg = err.response.data.non_field_errors.join(' ') errorMsg = err.response.data.non_field_errors.join(" ");
} }
$snackbar(errorMsg, 'Lỗi', 'Danger') $snackbar(errorMsg, "Lỗi", "Danger");
} finally { } finally {
isDeleting.value = false isDeleting.value = false;
} }
} };
const cancel = () => { const cancel = () => {
emit('close') emit("close");
} };
</script> </script>

View File

@@ -9,7 +9,12 @@
@date="selected('fdate', $event)" @date="selected('fdate', $event)"
></Datepicker> ></Datepicker>
</div> </div>
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p> <p
class="help is-danger"
v-if="errors.issued_date"
>
{{ errors.issued_date }}
</p>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="column is-3">
@@ -21,11 +26,19 @@
@date="selected('tdate', $event)" @date="selected('tdate', $event)"
></Datepicker> ></Datepicker>
</div> </div>
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p> <p
class="help is-danger"
v-if="errors.issued_date"
>
{{ errors.issued_date }}
</p>
</div> </div>
</div> </div>
</div> </div>
<DataView v-bind="vbind" v-if="vbind" /> <DataView
v-bind="vbind"
v-if="vbind"
/>
</template> </template>
<script setup> <script setup>
const { $dayjs, $id } = useNuxtApp(); const { $dayjs, $id } = useNuxtApp();
@@ -37,9 +50,9 @@ const vbind = ref(null);
onMounted(() => { onMounted(() => {
loadData(); loadData();
}) });
function selected(attr, value) { function selected(attr, value) {
if (attr === "fdate") fdate.value = value; if (attr === "fdate") fdate.value = value;
else tdate.value = value; else tdate.value = value;
loadData(); loadData();
@@ -56,18 +69,27 @@ function loadData() {
values: values:
"customer__code,customer__fullname,customer__type__name,customer__legal_type__name,customer__legal_code", "customer__code,customer__fullname,customer__type__name,customer__legal_type__name,customer__legal_code",
distinct_values: { distinct_values: {
sum_sale_price: { type: "Sum", field: "product__prdbk__transaction__sale_price" }, sum_sale_price: {
sum_received: { type: "Sum", field: "product__prdbk__transaction__amount_received" }, type: "Sum",
sum_remain: { type: "Sum", field: "product__prdbk__transaction__amount_remain" }, field: "product__prdbk__transaction__sale_price",
},
sum_received: {
type: "Sum",
field: "product__prdbk__transaction__amount_received",
},
sum_remain: {
type: "Sum",
field: "product__prdbk__transaction__amount_remain",
},
}, },
summary: "annotate", summary: "annotate",
filter: { filter: {
date__gte: fdate.value, date__gte: fdate.value,
date__lte: tdate.value date__lte: tdate.value,
}, },
sort: "-sum_remain", sort: "-sum_remain",
}, },
} };
}); });
} }
</script> </script>

View File

@@ -9,7 +9,12 @@
@date="selected('fdate', $event)" @date="selected('fdate', $event)"
></Datepicker> ></Datepicker>
</div> </div>
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p> <p
class="help is-danger"
v-if="errors.issued_date"
>
{{ errors.issued_date }}
</p>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="column is-3">
@@ -21,11 +26,19 @@
@date="selected('tdate', $event)" @date="selected('tdate', $event)"
></Datepicker> ></Datepicker>
</div> </div>
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p> <p
class="help is-danger"
v-if="errors.issued_date"
>
{{ errors.issued_date }}
</p>
</div> </div>
</div> </div>
</div> </div>
<DataView v-bind="vbind" v-if="vbind" /> <DataView
v-bind="vbind"
v-if="vbind"
/>
</template> </template>
<script> <script>
export default { export default {
@@ -60,13 +73,21 @@ export default {
values: values:
"product,product__prdbk__transaction__amount_received,product__trade_code,product__prdbk__transaction__sale_price,product__zone_type__name,customer,customer__code,customer__fullname", "product,product__prdbk__transaction__amount_received,product__trade_code,product__prdbk__transaction__sale_price,product__zone_type__name,customer,customer__code,customer__fullname",
distinct_values: { distinct_values: {
sumCR: { type: "Sum", filter: { type__code: "CR" }, field: "amount" }, sumCR: {
sumDR: { type: "Sum", filter: { type__code: "DR" }, field: "amount" }, type: "Sum",
filter: { type__code: "CR" },
field: "amount",
},
sumDR: {
type: "Sum",
filter: { type__code: "DR" },
field: "amount",
},
}, },
summary: "annotate", summary: "annotate",
filter: { date__gte: this.fdate, date__lte: this.tdate }, filter: { date__gte: this.fdate, date__lte: this.tdate },
}, },
}) }),
); );
}, },
}, },

View File

@@ -6,7 +6,7 @@
pagename: 'debt_report', pagename: 'debt_report',
api: 'transaction', api: 'transaction',
timeopt: { time: 36000, disable: ['add'] }, timeopt: { time: 36000, disable: ['add'] },
filter: { phase: 3 } filter: { phase: 3 },
}" }"
@option="handleTimeOption" @option="handleTimeOption"
@excel="exportExcel" @excel="exportExcel"
@@ -15,17 +15,26 @@
/> />
<!-- Loading --> <!-- Loading -->
<div v-if="loading" class="has-text-centered py-6"> <div
v-if="loading"
class="has-text-centered py-6"
>
<p class="has-text-grey mb-3">Đang tải dữ liệu...</p> <p class="has-text-grey mb-3">Đang tải dữ liệu...</p>
<progress class="progress is-small is-primary" max="100"></progress> <progress
class="progress is-small is-primary"
max="100"
></progress>
</div> </div>
<div class="" v-else> <div
class=""
v-else
>
<!-- Table --> <!-- Table -->
<div <div
v-if="filteredRows.length > 0" v-if="filteredRows.length > 0"
class="table-container" class="table-container"
style="overflow-x: auto; max-width: 100%;" style="overflow-x: auto; max-width: 100%"
> >
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth debt-table"> <table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth debt-table">
<thead> <thead>
@@ -37,22 +46,40 @@
> >
STT STT
</th> </th>
<th rowspan="2" class="fixed-col has-background-primary has-text-white"> <th
rowspan="2"
class="fixed-col has-background-primary has-text-white"
>
Mã KH Mã KH
</th> </th>
<th rowspan="2" class="fixed-col has-background-primary has-text-white"> <th
rowspan="2"
class="fixed-col has-background-primary has-text-white"
>
Mã Căn Mã Căn
</th> </th>
<th rowspan="2" class="fixed-col has-background-primary has-text-white"> <th
rowspan="2"
class="fixed-col has-background-primary has-text-white"
>
Ngày ký HĐ Ngày ký HĐ
</th> </th>
<th rowspan="2" class="fixed-col has-background-primary has-text-white"> <th
rowspan="2"
class="fixed-col has-background-primary has-text-white"
>
Giá trị HĐMB Giá trị HĐMB
</th> </th>
<th rowspan="2" class="fixed-col has-background-primary has-text-white"> <th
rowspan="2"
class="fixed-col has-background-primary has-text-white"
>
Tiền nộp theo HĐV/TTTHNV Tiền nộp theo HĐV/TTTHNV
</th> </th>
<th rowspan="2" class="fixed-col has-background-primary has-text-white"> <th
rowspan="2"
class="fixed-col has-background-primary has-text-white"
>
Tỷ lệ Tỷ lệ
</th> </th>
@@ -66,13 +93,19 @@
{{ sch.label }} {{ sch.label }}
</th> </th>
<th rowspan="2" class="has-background-primary has-text-white"> <th
rowspan="2"
class="has-background-primary has-text-white"
>
Số tiền quá hạn Số tiền quá hạn
</th> </th>
</tr> </tr>
<tr> <tr>
<template v-for="(sch, si) in scheduleHeaders" :key="si"> <template
v-for="(sch, si) in scheduleHeaders"
:key="si"
>
<th class="has-background-primary has-text-white sub-header">Ngày</th> <th class="has-background-primary has-text-white sub-header">Ngày</th>
<th class="has-background-primary has-text-white sub-header">Số tiền</th> <th class="has-background-primary has-text-white sub-header">Số tiền</th>
<th class="has-background-primary has-text-white sub-header">Lũy kế sang đợt</th> <th class="has-background-primary has-text-white sub-header">Lũy kế sang đợt</th>
@@ -84,13 +117,20 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="(row, ri) in filteredRows" :key="ri"> <tr
v-for="(row, ri) in filteredRows"
:key="ri"
>
<!-- Fixed columns --> <!-- Fixed columns -->
<td class="fixed-col has-text-centered">{{ ri + 1 }}</td> <td class="fixed-col has-text-centered">{{ ri + 1 }}</td>
<td class="fixed-col">{{ row.customer_code }}</td> <td class="fixed-col">{{ row.customer_code }}</td>
<td class="fixed-col has-text-weight-semibold has-text-primary">{{ row.trade_code }}</td> <td class="fixed-col has-text-weight-semibold has-text-primary">
{{ row.trade_code }}
</td>
<td class="fixed-col">{{ row.contract_date }}</td> <td class="fixed-col">{{ row.contract_date }}</td>
<td class="fixed-col has-text-right">{{ fmt(row.sale_price) }}</td> <td class="fixed-col has-text-right">
{{ fmt(row.sale_price) }}
</td>
<td class="fixed-col has-text-right has-text-weight-semibold has-background-warning-light"> <td class="fixed-col has-text-right has-text-weight-semibold has-background-warning-light">
{{ fmt(row.ttthnv_paid) }} {{ fmt(row.ttthnv_paid) }}
</td> </td>
@@ -99,11 +139,18 @@
</td> </td>
<!-- Scrollable columns --> <!-- Scrollable columns -->
<template v-for="(sch, si) in scheduleHeaders" :key="si"> <template
v-for="(sch, si) in scheduleHeaders"
:key="si"
>
<template v-if="row.schedules[si]"> <template v-if="row.schedules[si]">
<td>{{ row.schedules[si].to_date }}</td> <td>{{ row.schedules[si].to_date }}</td>
<td class="has-text-right">{{ fmt(row.schedules[si].amount) }}</td> <td class="has-text-right">
<td class="has-text-right has-text-info">{{ fmt(row.schedules[si].luy_ke_sang_dot) }}</td> {{ fmt(row.schedules[si].amount) }}
</td>
<td class="has-text-right has-text-info">
{{ fmt(row.schedules[si].luy_ke_sang_dot) }}
</td>
<td class="has-text-right has-text-weight-semibold has-text-success"> <td class="has-text-right has-text-weight-semibold has-text-success">
{{ fmt(row.schedules[si].thuc_thanh_toan) }} {{ fmt(row.schedules[si].thuc_thanh_toan) }}
</td> </td>
@@ -121,7 +168,13 @@
</td> </td>
</template> </template>
<template v-else> <template v-else>
<td colspan="6" class="has-text-centered has-text-grey-light" style="font-style: italic">—</td> <td
colspan="6"
class="has-text-centered has-text-grey-light"
style="font-style: italic"
>
</td>
</template> </template>
</template> </template>
@@ -129,25 +182,35 @@
class="has-text-right has-text-weight-bold" class="has-text-right has-text-weight-bold"
:class="Number(row.overdue) > 0 ? 'has-background-danger-light' : ''" :class="Number(row.overdue) > 0 ? 'has-background-danger-light' : ''"
> >
{{ Number(row.overdue) > 0 ? fmt(row.overdue) : '—' }} {{ Number(row.overdue) > 0 ? fmt(row.overdue) : "—" }}
</td> </td>
</tr> </tr>
</tbody> </tbody>
<tfoot> <tfoot>
<tr class="has-background-light"> <tr class="has-background-light">
<td :colspan="7" class="fixed-col has-text-right has-text-weight-bold">TỔNG CỘNG:</td> <td
:colspan="7"
class="fixed-col has-text-right has-text-weight-bold"
>
TỔNG CỘNG:
</td>
<template v-for="(sch, si) in scheduleHeaders" :key="si"> <template
v-for="(sch, si) in scheduleHeaders"
:key="si"
>
<td></td> <td></td>
<td class="has-text-right has-text-weight-semibold">{{ fmt(colSum(si, 'amount')) }}</td> <td class="has-text-right has-text-weight-semibold">
{{ fmt(colSum(si, "amount")) }}
</td>
<td></td> <td></td>
<td class="has-text-right has-text-weight-semibold has-text-success"> <td class="has-text-right has-text-weight-semibold has-text-success">
{{ fmt(colSum(si, 'thuc_thanh_toan')) }} {{ fmt(colSum(si, "thuc_thanh_toan")) }}
</td> </td>
<td></td> <td></td>
<td class="has-text-right has-text-weight-semibold has-text-danger"> <td class="has-text-right has-text-weight-semibold has-text-danger">
{{ fmt(colSum(si, 'amount_remain')) }} {{ fmt(colSum(si, "amount_remain")) }}
</td> </td>
</template> </template>
@@ -160,7 +223,10 @@
</div> </div>
<!-- Empty --> <!-- Empty -->
<div v-else class="has-text-centered py-6"> <div
v-else
class="has-text-centered py-6"
>
<p class="has-text-grey">Không có dữ liệu</p> <p class="has-text-grey">Không có dữ liệu</p>
</div> </div>
</div> </div>
@@ -168,283 +234,279 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from "vue";
import TimeOption from '~/components/datatable/TimeOption' import TimeOption from "~/components/datatable/TimeOption";
const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp() const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp();
const loading = ref(false) const loading = ref(false);
const rows = ref([]) const rows = ref([]);
const filteredRows = ref([]) const filteredRows = ref([]);
const scheduleHeaders = ref([]) const scheduleHeaders = ref([]);
const currentFilter = ref(null) const currentFilter = ref(null);
const currentSearch = ref(null) const currentSearch = ref(null);
function handleTimeOption(option) { function handleTimeOption(option) {
if (!option) { if (!option) {
currentFilter.value = null currentFilter.value = null;
currentSearch.value = null currentSearch.value = null;
applyFilters() applyFilters();
return return;
} }
if (option.filter) { if (option.filter) {
currentFilter.value = option.filter currentFilter.value = option.filter;
currentSearch.value = null currentSearch.value = null;
applyFilters() applyFilters();
} else if (option.filter_or) { } else if (option.filter_or) {
currentFilter.value = null currentFilter.value = null;
currentSearch.value = option.filter_or currentSearch.value = option.filter_or;
applyFilters() applyFilters();
} }
} }
function applyFilters() { function applyFilters() {
let filtered = [...rows.value] let filtered = [...rows.value];
if (currentFilter.value && currentFilter.value.create_time__date__gte) { if (currentFilter.value && currentFilter.value.create_time__date__gte) {
const filterDate = new Date(currentFilter.value.create_time__date__gte) const filterDate = new Date(currentFilter.value.create_time__date__gte);
filtered = filtered.filter(row => { filtered = filtered.filter((row) => {
const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null;
return contractDate && contractDate >= filterDate return contractDate && contractDate >= filterDate;
}) });
} }
if (currentSearch.value) { if (currentSearch.value) {
const searchTerms = Object.values(currentSearch.value).map(v => const searchTerms = Object.values(currentSearch.value).map((v) =>
String(v).toLowerCase().replace('__icontains', '') String(v).toLowerCase().replace("__icontains", ""),
) );
filtered = filtered.filter(row => { filtered = filtered.filter((row) => {
const searchableText = [ const searchableText = [
row.customer_code, row.customer_code,
row.customer_name, row.customer_name,
row.trade_code, row.trade_code,
row.contract_date, row.contract_date,
String(row.sale_price) String(row.sale_price),
].join(' ').toLowerCase() ]
return searchTerms.some(term => searchableText.includes(term)) .join(" ")
}) .toLowerCase();
return searchTerms.some((term) => searchableText.includes(term));
});
} }
filteredRows.value = filtered filteredRows.value = filtered;
} }
async function loadData() { async function loadData() {
loading.value = true loading.value = true;
rows.value = [] rows.value = [];
filteredRows.value = [] filteredRows.value = [];
scheduleHeaders.value = [] scheduleHeaders.value = [];
try { try {
const txnConn = $copy($findapi('transaction')) const txnConn = $copy($findapi("transaction"));
txnConn.params = { txnConn.params = {
filter: { phase: 3 }, filter: { phase: 3 },
values: 'id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase', values:
sort: 'id' "id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase",
} sort: "id",
};
const detailConn = $copy($findapi('reservation')) const detailConn = $copy($findapi("reservation"));
detailConn.params = { detailConn.params = {
filter: { transaction__phase: 3, phase: 3 }, filter: { transaction__phase: 3, phase: 3 },
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status', values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
sort: 'transaction' sort: "transaction",
} };
const schConn = $copy($findapi('payment_schedule')) const schConn = $copy($findapi("payment_schedule"));
schConn.params = { schConn.params = {
filter: { txn_detail__phase: 3 }, filter: { txn_detail__phase: 3 },
values: values:
'id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received', "id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received",
sort: 'txn_detail__transaction,cycle' sort: "txn_detail__transaction,cycle",
} };
const ttthnvConn = $copy($findapi('reservation')) const ttthnvConn = $copy($findapi("reservation"));
ttthnvConn.params = { ttthnvConn.params = {
filter: { transaction__phase: 3, phase: 4 }, filter: { transaction__phase: 3, phase: 4 },
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status', values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
sort: 'transaction' sort: "transaction",
} };
const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([ const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([txnConn, detailConn, schConn, ttthnvConn]);
txnConn,
detailConn,
schConn,
ttthnvConn
])
const transactions = txnRs?.data?.rows || [] const transactions = txnRs?.data?.rows || [];
const details = detailRs?.data?.rows || [] const details = detailRs?.data?.rows || [];
const schedules = schRs?.data?.rows || [] const schedules = schRs?.data?.rows || [];
const ttthnvList = ttthnvRs?.data?.rows || [] const ttthnvList = ttthnvRs?.data?.rows || [];
if (!transactions.length) { if (!transactions.length) {
loading.value = false loading.value = false;
return return;
} }
// TTTHNV map // TTTHNV map
const ttthnvMap = {} const ttthnvMap = {};
ttthnvList.forEach(t => { ttthnvList.forEach((t) => {
const tid = t.transaction const tid = t.transaction;
ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0) ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0);
}) });
// Group schedules by transaction // Group schedules by transaction
const schByTxn = {} const schByTxn = {};
schedules.forEach(s => { schedules.forEach((s) => {
const tid = s.txn_detail__transaction const tid = s.txn_detail__transaction;
if (!schByTxn[tid]) schByTxn[tid] = [] if (!schByTxn[tid]) schByTxn[tid] = [];
schByTxn[tid].push(s) schByTxn[tid].push(s);
}) });
// Tìm số đợt tối đa // Tìm số đợt tối đa
let maxCycles = 0 let maxCycles = 0;
Object.values(schByTxn).forEach(list => { Object.values(schByTxn).forEach((list) => {
const paymentList = list.filter(s => Number(s.cycle) > 0) const paymentList = list.filter((s) => Number(s.cycle) > 0);
if (paymentList.length > maxCycles) maxCycles = paymentList.length if (paymentList.length > maxCycles) maxCycles = paymentList.length;
}) });
scheduleHeaders.value = Array.from({ length: maxCycles }, (_, i) => ({ scheduleHeaders.value = Array.from({ length: maxCycles }, (_, i) => ({
label: `L0${i + 1}`, label: `L0${i + 1}`,
index: i index: i,
})) }));
rows.value = transactions.map(txn => { rows.value = transactions.map((txn) => {
const txnSchedules = (schByTxn[txn.id] || []) const txnSchedules = (schByTxn[txn.id] || [])
.filter(s => Number(s.cycle) > 0) .filter((s) => Number(s.cycle) > 0)
.sort((a, b) => Number(a.cycle) - Number(b.cycle)) .sort((a, b) => Number(a.cycle) - Number(b.cycle));
const ttthnvPaid = ttthnvMap[txn.id] || 0 const ttthnvPaid = ttthnvMap[txn.id] || 0;
const salePriceNum = Number(txn.sale_price || 0) const salePriceNum = Number(txn.sale_price || 0);
// ─────────────────────────────────────────────── // ───────────────────────────────────────────────
// QUAN TRỌNG: Theo yêu cầu mới nhất của bạn // QUAN TRỌNG: Theo yêu cầu mới nhất của bạn
// Lũy kế HĐCN = TTTHNV // Lũy kế HĐCN = TTTHNV
const luyKe = ttthnvPaid const luyKe = ttthnvPaid;
// ─────────────────────────────────────────────── // ───────────────────────────────────────────────
// Phân bổ TTTHNV dần vào từng đợt → tính lũy kế sang đợt // Phân bổ TTTHNV dần vào từng đợt → tính lũy kế sang đợt
let remainingTTTHNV = ttthnvPaid let remainingTTTHNV = ttthnvPaid;
const schedulesWithCalc = txnSchedules.map(sch => { const schedulesWithCalc = txnSchedules.map((sch) => {
const scheduleAmount = Number(sch.amount || 0) const scheduleAmount = Number(sch.amount || 0);
// Lũy kế sang đợt = min(remaining TTTHNV, số tiền đợt) // Lũy kế sang đợt = min(remaining TTTHNV, số tiền đợt)
const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount) const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount);
// Số tiền đã thực thanh toán = paid_amount - lũy kế sang đợt // Số tiền đã thực thanh toán = paid_amount - lũy kế sang đợt
const paidAmountFromSchedule = Number(sch.paid_amount || 0) const paidAmountFromSchedule = Number(sch.paid_amount || 0);
const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot) const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot);
// Dư nợ = số tiền đợt - lũy kế sang đợt - thực thanh toán // Dư nợ = số tiền đợt - lũy kế sang đợt - thực thanh toán
const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan) const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan);
remainingTTTHNV -= luyKeSangDot remainingTTTHNV -= luyKeSangDot;
remainingTTTHNV = Math.max(0, remainingTTTHNV) remainingTTTHNV = Math.max(0, remainingTTTHNV);
return { return {
to_date: sch.to_date ? $dayjs(sch.to_date).format('DD/MM/YYYY') : '—', to_date: sch.to_date ? $dayjs(sch.to_date).format("DD/MM/YYYY") : "—",
amount: scheduleAmount, amount: scheduleAmount,
luy_ke_sang_dot: luyKeSangDot, luy_ke_sang_dot: luyKeSangDot,
thuc_thanh_toan: thucThanhToan, thuc_thanh_toan: thucThanhToan,
amount_remain: amountRemain, amount_remain: amountRemain,
status: sch.status status: sch.status,
} };
}) });
// Tính quá hạn // Tính quá hạn
const todayDate = new Date() const todayDate = new Date();
const overdue = txnSchedules.reduce((sum, sch, idx) => { const overdue = txnSchedules.reduce((sum, sch, idx) => {
const toDate = sch.to_date ? new Date(sch.to_date) : null const toDate = sch.to_date ? new Date(sch.to_date) : null;
const remain = schedulesWithCalc[idx]?.amount_remain || 0 const remain = schedulesWithCalc[idx]?.amount_remain || 0;
if (toDate && toDate < todayDate && remain > 0) { if (toDate && toDate < todayDate && remain > 0) {
return sum + remain return sum + remain;
} }
return sum return sum;
}, 0) }, 0);
const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null) const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null);
return { return {
customer_code: txn.customer__code || '', customer_code: txn.customer__code || "",
customer_name: txn.customer__fullname || '', customer_name: txn.customer__fullname || "",
trade_code: txn.product__trade_code || txn.code || '', trade_code: txn.product__trade_code || txn.code || "",
contract_date: txn.date ? $dayjs(txn.date).format('DD/MM/YYYY') : '—', contract_date: txn.date ? $dayjs(txn.date).format("DD/MM/YYYY") : "—",
contract_date_raw: txn.date, contract_date_raw: txn.date,
sale_price: salePriceNum, sale_price: salePriceNum,
ttthnv_paid: ttthnvPaid, ttthnv_paid: ttthnvPaid,
luy_ke: luyKe, // ← chính là TTTHNV luy_ke: luyKe, // ← chính là TTTHNV
schedules: paddedSchedules, schedules: paddedSchedules,
overdue: overdue overdue: overdue,
} };
}) });
filteredRows.value = rows.value filteredRows.value = rows.value;
} catch (e) { } catch (e) {
console.error('BaoCaoCongNo error:', e) console.error("BaoCaoCongNo error:", e);
} finally { } finally {
loading.value = false loading.value = false;
} }
} }
function fmt(val) { function fmt(val) {
const n = Number(val) const n = Number(val);
if (isNaN(n) || (!n && n !== 0)) return '—' if (isNaN(n) || (!n && n !== 0)) return "—";
return n.toLocaleString('vi-VN') return n.toLocaleString("vi-VN");
} }
function pct(num, denom) { function pct(num, denom) {
const n = Number(num) const n = Number(num);
const d = Number(denom) const d = Number(denom);
if (!d || isNaN(n)) return '—' if (!d || isNaN(n)) return "—";
return (n / d * 100).toFixed(1) + '%' return ((n / d) * 100).toFixed(1) + "%";
} }
function pctClass(paid, amount) { function pctClass(paid, amount) {
const p = Number(paid) const p = Number(paid);
const a = Number(amount) const a = Number(amount);
if (isNaN(p) || isNaN(a) || !a) return '' if (isNaN(p) || isNaN(a) || !a) return "";
const ratio = p / a const ratio = p / a;
if (ratio >= 1) return 'has-text-success' if (ratio >= 1) return "has-text-success";
if (ratio >= 0.5) return 'has-text-info' if (ratio >= 0.5) return "has-text-info";
return 'has-text-danger' return "has-text-danger";
} }
function colSum(scheduleIndex, field) { function colSum(scheduleIndex, field) {
return filteredRows.value.reduce((sum, row) => { return filteredRows.value.reduce((sum, row) => {
const sch = row.schedules[scheduleIndex] const sch = row.schedules[scheduleIndex];
return sum + (sch ? Number(sch[field] || 0) : 0) return sum + (sch ? Number(sch[field] || 0) : 0);
}, 0) }, 0);
} }
const totalOverdue = computed(() => const totalOverdue = computed(() => filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0));
filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0)
)
function exportExcel() { function exportExcel() {
const headers = [ const headers = [
'STT', "STT",
'Mã KH', "Mã KH",
'Mã Căn', "Mã Căn",
'Ngày ký HĐ', "Ngày ký HĐ",
'Giá trị HĐMB', "Giá trị HĐMB",
'Tiền nộp TTTHNV', "Tiền nộp TTTHNV",
'Lũy kế tiền về HĐCN', "Lũy kế tiền về HĐCN",
'Tỷ lệ HĐCN' "Tỷ lệ HĐCN",
] ];
scheduleHeaders.value.forEach(h => { scheduleHeaders.value.forEach((h) => {
headers.push( headers.push(
`${h.label} - Ngày`, `${h.label} - Ngày`,
`${h.label} - Số tiền`, `${h.label} - Số tiền`,
`${h.label} - Lũy kế sang`, `${h.label} - Lũy kế sang`,
`${h.label} - Số tiền đã thực thanh toán`, `${h.label} - Số tiền đã thực thanh toán`,
`${h.label} - Tỷ lệ`, `${h.label} - Tỷ lệ`,
`${h.label} - Dư nợ` `${h.label} - Dư nợ`,
) );
}) });
headers.push('Số tiền quá hạn') headers.push("Số tiền quá hạn");
const data = filteredRows.value.map((row, i) => { const data = filteredRows.value.map((row, i) => {
const base = [ const base = [
@@ -455,11 +517,11 @@ function exportExcel() {
fmt(row.sale_price), fmt(row.sale_price),
fmt(row.ttthnv_paid), fmt(row.ttthnv_paid),
fmt(row.luy_ke), fmt(row.luy_ke),
pct(row.luy_ke, row.sale_price) pct(row.luy_ke, row.sale_price),
] ];
scheduleHeaders.value.forEach((_, si) => { scheduleHeaders.value.forEach((_, si) => {
const sch = row.schedules[si] const sch = row.schedules[si];
if (sch) { if (sch) {
base.push( base.push(
sch.to_date, sch.to_date,
@@ -467,31 +529,31 @@ function exportExcel() {
fmt(sch.luy_ke_sang_dot), fmt(sch.luy_ke_sang_dot),
fmt(sch.thuc_thanh_toan), fmt(sch.thuc_thanh_toan),
pct(sch.thuc_thanh_toan, sch.amount), pct(sch.thuc_thanh_toan, sch.amount),
fmt(sch.amount_remain) fmt(sch.amount_remain),
) );
} else { } else {
base.push('', '', '', '', '', '') base.push("", "", "", "", "", "");
} }
}) });
base.push(fmt(row.overdue)) base.push(fmt(row.overdue));
return base return base;
}) });
const csvRows = [headers, ...data] const csvRows = [headers, ...data];
const csv = csvRows.map(r => r.map(c => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',')).join('\n') const csv = csvRows.map((r) => r.map((c) => `"${String(c ?? "").replace(/"/g, '""')}"`).join(",")).join("\n");
const BOM = '\uFEFF' const BOM = "\uFEFF";
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' }) const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob);
const a = document.createElement('a') const a = document.createElement("a");
a.href = url a.href = url;
a.download = `bao-cao-cong-no-${$dayjs().format('YYYYMMDD')}.csv` a.download = `bao-cao-cong-no-${$dayjs().format("YYYYMMDD")}.csv`;
a.click() a.click();
URL.revokeObjectURL(url) URL.revokeObjectURL(url);
} }
onMounted(() => loadData()) onMounted(() => loadData());
</script> </script>
<style scoped> <style scoped>
@@ -578,4 +640,4 @@ onMounted(() => loadData())
.fixed-col { .fixed-col {
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.08); box-shadow: 2px 0 5px rgba(0, 0, 0, 0.08);
} }
</style> </style>

View File

@@ -1,31 +1,51 @@
<template> <template>
<div> <div>
<DataView v-bind="{api: 'internalaccount', setting: store.lang==='en'? 'internal-account-en' : 'internal-account', pagename: pagename, <DataView
modal: {title: 'Tài khoản', component: 'accounting/AccountView', width: '50%', 'height': '300px'}}" /> v-bind="{
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" /> api: 'internalaccount',
</div> setting: store.lang === 'en' ? 'internal-account-en' : 'internal-account',
pagename: pagename,
modal: {
title: 'Tài khoản',
component: 'accounting/AccountView',
width: '50%',
height: '300px',
},
}"
/>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
/>
</div>
</template> </template>
<script> <script>
import { useStore } from '~/stores/index' import { useStore } from "~/stores/index";
export default { export default {
setup() { setup() {
const store = useStore() const store = useStore();
return {store} return { store };
}, },
data() { data() {
return { return {
showmodal: undefined, showmodal: undefined,
pagename: 'pagedata32' pagename: "pagedata32",
} };
}, },
methods: { methods: {
deposit() { deposit() {
this.showmodal = {component: 'accounting/InternalDeposit', title: 'Nộp tiền tài khoản nội bộ', width: '40%', height: '300px', this.showmodal = {
vbind: {pagename: this.pagename}} component: "accounting/InternalDeposit",
title: "Nộp tiền tài khoản nội bộ",
width: "40%",
height: "300px",
vbind: { pagename: this.pagename },
};
}, },
doClick() { doClick() {
this.$approvalcode() this.$approvalcode();
} },
} },
} };
</script> </script>

View File

@@ -1,176 +1,288 @@
<template> <template>
<div> <div>
<div class="columns is-multiline mx-0"> <div class="columns is-multiline mx-0">
<div class="column is-8"> <div class="column is-8">
<div class="field">
<label class="label">{{ $lang('account') }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox v-bind="{api:'internalaccount', field:'label', column:['label'], first: true, optionid: record.account}"
:disabled="record.account" @option="selected('_account', $event)" v-if="!record.id"></SearchBox>
<span v-else>{{record.account__code}}</span>
</div>
<p class="help is-danger" v-if="errors._account">{{ errors._account }}</p>
</div>
</div>
<div class="column is-4">
<div class="field"> <div class="field">
<label class="label" <label class="label">{{ $lang("account") }}<b class="ml-1 has-text-danger">*</b></label>
>Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label <div class="control">
<SearchBox
v-bind="{
api: 'internalaccount',
field: 'label',
column: ['label'],
first: true,
optionid: record.account,
}"
:disabled="record.account"
@option="selected('_account', $event)"
v-if="!record.id"
></SearchBox>
<span v-else>{{ record.account__code }}</span>
</div>
<p
class="help is-danger"
v-if="errors._account"
> >
{{ errors._account }}
</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label>
<div class="control"> <div class="control">
<Datepicker <Datepicker
v-bind="{ record: record, attr: 'date', maxdate: new Date()}" v-bind="{ record: record, attr: 'date', maxdate: new Date() }"
@date="selected('date', $event)" @date="selected('date', $event)"
></Datepicker> ></Datepicker>
</div> </div>
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p> <p
class="help is-danger"
v-if="errors.issued_date"
>
{{ errors.issued_date }}
</p>
</div> </div>
</div> </div>
<div class="column is-4"> <div class="column is-4">
<div class="field">
<label class="label">Thu / chi<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox v-bind="{api:'entrytype', field:'name', column:['name'], first: true, optionid: record.type}"
:disabled="record.type" @option="selected('_type', $event)" v-if="!record.id"></SearchBox>
<span v-else>{{record.type__name}}</span>
</div>
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">{{$lang('amount-only')}}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<InputNumber v-bind="{record: record, attr: 'amount', placeholder: ''}" @number="selected('amount', $event)"></InputNumber>
</div>
<p class="help is-danger" v-if="errors.amount">{{errors.amount}}</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">Phương thức thanh toán<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox v-bind="{api:'entrycategory', field:'name', column:['name'], first: true, optionid: record.type}"
:disabled="record.type" @option="selected('_category', $event)" v-if="!record.id"></SearchBox>
<span v-else>{{record.type__name}}</span>
</div>
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Sản phẩm<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox v-bind="{
api: 'product',
field: 'label',
searchfield: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
first: true
}" @option="selected('product', $event)" />
</div>
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Khách hàng<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox v-bind="{
api: 'customer',
field: 'label',
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
column: ['code', 'fullname', 'phone', 'legal_code'],
first: true
}" @option="selected('customer', $event)" />
</div>
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
</div>
</div>
<div class="column is-12">
<div class="field">
<label class="label">{{ $lang('content') }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<textarea class="textarea" rows="2" v-model="record.content"></textarea>
</div>
<p class="help is-danger" v-if="errors.content">{{errors.content}}</p>
</div>
</div>
<div class="column is-4">
<div class="field"> <div class="field">
<label class="label">Mã tham chiếu</label> <label class="label">Thu / chi<b class="ml-1 has-text-danger">*</b></label>
<div class="control"> <div class="control">
<input class="input has-text-black" type="text" placeholder="Tối đa 30 ký tự" v-model="record.ref"> <SearchBox
v-bind="{
api: 'entrytype',
field: 'name',
column: ['name'],
first: true,
optionid: record.type,
}"
:disabled="record.type"
@option="selected('_type', $event)"
v-if="!record.id"
></SearchBox>
<span v-else>{{ record.type__name }}</span>
</div>
<p
class="help is-danger"
v-if="errors._type"
>
{{ errors._type }}
</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">{{ $lang("amount-only") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<InputNumber
v-bind="{ record: record, attr: 'amount', placeholder: '' }"
@number="selected('amount', $event)"
></InputNumber>
</div>
<p
class="help is-danger"
v-if="errors.amount"
>
{{ errors.amount }}
</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">Phương thức thanh toán<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox
v-bind="{
api: 'entrycategory',
field: 'name',
column: ['name'],
first: true,
optionid: record.type,
}"
:disabled="record.type"
@option="selected('_category', $event)"
v-if="!record.id"
></SearchBox>
<span v-else>{{ record.type__name }}</span>
</div>
<p
class="help is-danger"
v-if="errors._type"
>
{{ errors._type }}
</p>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Sản phẩm<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox
v-bind="{
api: 'product',
field: 'label',
searchfield: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
first: true,
}"
@option="selected('product', $event)"
/>
</div>
<p
class="help is-danger"
v-if="errors._type"
>
{{ errors._type }}
</p>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Khách hàng<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox
v-bind="{
api: 'customer',
field: 'label',
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
column: ['code', 'fullname', 'phone', 'legal_code'],
first: true,
}"
@option="selected('customer', $event)"
/>
</div>
<p
class="help is-danger"
v-if="errors._type"
>
{{ errors._type }}
</p>
</div>
</div>
<div class="column is-12">
<div class="field">
<label class="label">{{ $lang("content") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<textarea
class="textarea"
rows="2"
v-model="record.content"
></textarea>
</div>
<p
class="help is-danger"
v-if="errors.content"
>
{{ errors.content }}
</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">Mã tham chiếu</label>
<div class="control">
<input
class="input has-text-black"
type="text"
placeholder="Tối đa 30 ký tự"
v-model="record.ref"
/>
</div>
</div>
</div>
<div
class="column is-12"
v-if="entry"
>
<div class="field">
<label class="label">Chứng từ đi kèm (nếu có)</label>
<div class="control">
<FileGallery
v-bind="{
row: entry,
pagename: pagename,
api: 'entryfile',
info: false,
}"
></FileGallery>
</div>
</div>
</div>
</div>
<div
class="mt-5 ml-3"
v-if="!entry"
>
<button
:class="['button is-primary has-text-white mr-2', isUpdating && 'is-loading']"
@click="confirm()"
>
{{ $lang("confirm") }}
</button>
</div>
<Modal
@close="showContractModal = undefined"
v-bind="showContractModal"
v-if="showContractModal"
></Modal>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
@confirm="update()"
v-if="showmodal"
></Modal>
</div> </div>
</div>
</div>
<div class="column is-12" v-if="entry">
<div class="field">
<label class="label">Chứng từ đi kèm (nếu có)</label>
<div class="control">
<FileGallery v-bind="{row: entry, pagename: pagename, api: 'entryfile', info: false}"></FileGallery>
</div>
</div>
</div>
</div>
<div class="mt-5 ml-3" v-if="!entry">
<button
:class="[
'button is-primary has-text-white mr-2',
isUpdating && 'is-loading'
]"
@click="confirm()">{{$lang('confirm')}}</button>
</div>
<Modal @close="showContractModal=undefined" v-bind="showContractModal" v-if="showContractModal"></Modal>
<Modal @close="showmodal=undefined" v-bind="showmodal" @confirm="update()" v-if="showmodal"></Modal>
</div>
</template> </template>
<script> <script>
import { useStore } from '~/stores/index' import { useStore } from "~/stores/index";
export default { export default {
setup() { setup() {
const store = useStore() const store = useStore();
return {store} return { store };
}, },
props: ['pagename', 'row', 'option'], props: ["pagename", "row", "option"],
data() { data() {
return { return {
record: {date: this.$dayjs().format('YYYY-MM-DD')}, record: { date: this.$dayjs().format("YYYY-MM-DD") },
errors: {}, errors: {},
isUpdating: false, isUpdating: false,
showmodal: undefined, showmodal: undefined,
showContractModal: undefined, showContractModal: undefined,
entry: undefined entry: undefined,
} };
}, },
created() { created() {
if(!this.option) return if (!this.option) return;
this.record.account = this.option.account this.record.account = this.option.account;
this.record.type = this.option.type this.record.type = this.option.type;
}, },
methods: { methods: {
selected(attr, obj) { selected(attr, obj) {
this.record[attr] = obj this.record[attr] = obj;
this.record = this.$copy(this.record) this.record = this.$copy(this.record);
}, },
checkError() { checkError() {
this.errors = {} this.errors = {};
if(this.$empty(this.record._account)) this.errors._account = 'Chưa chọn tài khoản' if (this.$empty(this.record._account)) this.errors._account = "Chưa chọn tài khoản";
if(this.$empty(this.record._type)) this.errors._type = 'Chưa chọn loại hạch toán' if (this.$empty(this.record._type)) this.errors._type = "Chưa chọn loại hạch toán";
if(this.$empty(this.record.amount)) this.errors.amount = 'Chưa nhập số tiền' if (this.$empty(this.record.amount)) this.errors.amount = "Chưa nhập số tiền";
else if(this.$formatNumber(this.record.amount)<=0) this.errors.amount = 'Số tiền phải > 0' else if (this.$formatNumber(this.record.amount) <= 0) this.errors.amount = "Số tiền phải > 0";
if(this.$empty(this.record.content)) this.errors.content = 'Chưa nhập nội dung' if (this.$empty(this.record.content)) this.errors.content = "Chưa nhập nội dung";
if(Object.keys(this.errors).length>0) return true if (Object.keys(this.errors).length > 0) return true;
if(this.record._type.code==='DR' && (this.record._account.balance<this.$formatNumber(this.record.amount))) { if (this.record._type.code === "DR" && this.record._account.balance < this.$formatNumber(this.record.amount)) {
this.errors._account = 'Số tài khoản không đủ để trích nợ' this.errors._account = "Số tài khoản không đủ để trích nợ";
} }
return Object.keys(this.errors).length>0 return Object.keys(this.errors).length > 0;
}, },
confirm() { confirm() {
if(this.checkError()) return if (this.checkError()) return;
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10}, this.showmodal = {
title: this.$lang('confirm'), width: '500px', height: '100px'} component: `dialog/Confirm`,
vbind: { content: this.$lang("confirm-action"), duration: 10 },
title: this.$lang("confirm"),
width: "500px",
height: "100px",
};
}, },
async update() { async update() {
this.isUpdating = true; this.isUpdating = true;
@@ -179,28 +291,29 @@ export default {
amount: this.record.amount, amount: this.record.amount,
content: this.record.content, content: this.record.content,
type: this.record._type.code, type: this.record._type.code,
category: this.record._category ? this.record._category.id : 1, user: this.store.login.id, category: this.record._category ? this.record._category.id : 1,
ref: this.row ? this.row.code : (!this.$empty(this.record.ref) ? this.record.ref.trim() : null), user: this.store.login.id,
ref: this.row ? this.row.code : !this.$empty(this.record.ref) ? this.record.ref.trim() : null,
customer: this.record.customer ? this.record.customer.id : null, customer: this.record.customer ? this.record.customer.id : null,
product: this.record.product ? this.record.product.id : null, product: this.record.product ? this.record.product.id : null,
date: this.$empty(this.record.date) ? null : this.record.date date: this.$empty(this.record.date) ? null : this.record.date,
} };
let rs1 = await this.$insertapi('accountentry', obj1, undefined, false) let rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
if(rs1==='error') return if (rs1 === "error") return;
if (this.record._category.id === 2) { if (this.record._category.id === 2) {
const genDoc = await this.$generateDocument({ const genDoc = await this.$generateDocument({
doc_code: 'PHIEU_THU_TIEN_MAT', doc_code: "PHIEU_THU_TIEN_MAT",
entry_id: rs1.id, entry_id: rs1.id,
output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}` output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}`,
}); });
await this.$insertapi('file', { await this.$insertapi("file", {
name: genDoc.data.pdf, name: genDoc.data.pdf,
user: this.store.login.id, user: this.store.login.id,
type: 4, type: 4,
size: 1000, size: 1000,
file: genDoc.data.pdf // or genDoc.data.pdf file: genDoc.data.pdf, // or genDoc.data.pdf
}); });
this.showContractModal = { this.showContractModal = {
@@ -209,20 +322,27 @@ export default {
width: "95%", width: "95%",
height: "95vh", height: "95vh",
vbind: { vbind: {
directDocument: genDoc.data directDocument: genDoc.data,
}, },
} };
} }
this.entry = rs1 this.entry = rs1;
if(this.pagename) { if (this.pagename) {
let data = await this.$getdata('internalaccount', {code__in: [this.record._account.code]}) let data = await this.$getdata("internalaccount", {
this.$updatepage(this.pagename, data) code__in: [this.record._account.code],
});
this.$updatepage(this.pagename, data);
} }
this.isUpdating = false; this.isUpdating = false;
this.$emit('modalevent', {name: 'entry', data: rs1}) this.$emit("modalevent", { name: "entry", data: rs1 });
this.$dialog(`Hạch toán <b>${this.record._type.name}</b> số tiền <b>${this.$numtoString(this.record.amount)}</b> vào tài khoản <b>${this.record._account.code}</b> thành công.`, 'Thành công', 'Success', 10) this.$dialog(
} `Hạch toán <b>${this.record._type.name}</b> số tiền <b>${this.$numtoString(this.record.amount)}</b> vào tài khoản <b>${this.record._account.code}</b> thành công.`,
} "Thành công",
} "Success",
</script> 10,
);
},
},
};
</script>

View File

@@ -1,247 +1,326 @@
<template> <template>
<div v-if="record" id="printable"> <div
<Caption v-bind="{ title: $lang('info') }" /> v-if="record"
<div class="columns is-multiline is-2 m-0"> id="printable"
<div class="column is-3"> >
<div class="field"> <Caption v-bind="{ title: $lang('info') }" />
<label class="label">{{ $lang('code') }}:</label> <div class="columns is-multiline is-2 m-0">
<div class="control"> <div class="column is-3">
<span>{{ record.code }}</span> <div class="field">
</div> <label class="label">{{ $lang("code") }}:</label>
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p> <div class="control">
</div> <span>{{ record.code }}</span>
</div> </div>
<div class="column is-3"> <p
<div class="field"> class="help is-danger"
<label class="label">Tài khoản:</label> v-if="errors.type"
<div class="control"> >
<span>{{ record.account__code }}</span> {{ errors.type }}
</div> </p>
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p> </div>
</div> </div>
</div> <div class="column is-3">
<div class="column is-3"> <div class="field">
<div class="field"> <label class="label">Tài khoản:</label>
<label class="label">Ngày hạch toán:</label> <div class="control">
<div class="control"> <span>{{ record.account__code }}</span>
{{ $dayjs(record.date).format('DD/MM/YYYY') }} </div>
</div> <p
</div> class="help is-danger"
</div> v-if="errors.type"
<div class="column is-3"> >
<div class="field"> {{ errors.type }}
<label class="label">{{ $lang('amount-only') }}:</label> </p>
<div class="control"> </div>
{{ $numtoString(record.amount) }} </div>
</div> <div class="column is-3">
</div> <div class="field">
</div> <label class="label">Ngày hạch toán:</label>
<div class="column is-3"> <div class="control">
<div class="field"> {{ $dayjs(record.date).format("DD/MM/YYYY") }}
<label class="label">Thu / chi:</label> </div>
<div class="control"> </div>
{{ record.type__name }} </div>
</div> <div class="column is-3">
</div> <div class="field">
</div> <label class="label">{{ $lang("amount-only") }}:</label>
<div class="column is-3"> <div class="control">
<div class="field"> {{ $numtoString(record.amount) }}
<label class="label"> trước:</label> </div>
<div class="control"> </div>
{{ $numtoString(record.balance_before) }} </div>
</div> <div class="column is-3">
</div> <div class="field">
</div> <label class="label">Thu / chi:</label>
<div class="column is-3"> <div class="control">
<div class="field"> {{ record.type__name }}
<label class="label"> sau:</label> </div>
<div class="control"> </div>
{{ $numtoString(record.balance_after) }} </div>
</div> <div class="column is-3">
</div> <div class="field">
</div> <label class="label"> trước:</label>
<div class="column is-3"> <div class="control">
<div class="field"> {{ $numtoString(record.balance_before) }}
<label class="label"> sản phẩm:</label> </div>
<div class="control"> </div>
{{ record.product__trade_code }} </div>
</div> <div class="column is-3">
</div> <div class="field">
</div> <label class="label"> sau:</label>
<div class="column is-3"> <div class="control">
<div class="field"> {{ $numtoString(record.balance_after) }}
<label class="label"> khách hàng:</label> </div>
<div class="control"> </div>
{{ record.customer__code }} </div>
</div> <div class="column is-3">
</div> <div class="field">
</div> <label class="label"> sản phẩm:</label>
<div class="control">
{{ record.product__trade_code }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label"> khách hàng:</label>
<div class="control">
{{ record.customer__code }}
</div>
</div>
</div>
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">Người hạch toán:</label> <label class="label">Người hạch toán:</label>
<div class="control"> <div class="control">
{{ `${record.inputer__fullname}` }} {{ `${record.inputer__fullname}` }}
</div> </div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">{{ $lang('time') }}:</label>
<div class="control">
{{ `${$dayjs(record.create_time).format('DD/MM/YYYY')}` }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">Ref:</label>
<div class="control">
{{ `${record.ref || '/'}` }}
</div>
</div>
</div>
<div class="column is-8">
<div class="field">
<label class="label">{{ $lang('content') }}:</label>
<div class="control">
{{ `${record.content}` }}
</div>
</div>
</div>
</div> </div>
</div>
<!-- PHẦN THÔNG TIN PHÂN BỔ --> <div class="column is-3">
<Caption v-bind="{ title: 'Thông tin phân bổ' }" /> <div class="field">
<!-- BẢNG CHI TIẾT PHÂN BỔ --> <label class="label">{{ $lang("time") }}:</label>
<div v-if="record.allocation_detail && record.allocation_detail.length > 0" class="mt-4"> <div class="control">
<div class="table-container"> {{ `${$dayjs(record.create_time).format("DD/MM/YYYY")}` }}
<table class="table is-fullwidth is-striped is-hoverable is-bordered"> </div>
<thead>
<tr class="">
<th class="has-background-primary has-text-white has-text-centered">STT</th>
<th class="has-background-primary has-text-white has-text-centered"> lịch</th>
<th class="has-background-primary has-text-white has-text-centered">Loại</th>
<th class="has-background-primary has-text-white has-text-centered">Tổng phân bổ</th>
<th class="has-background-primary has-text-white has-text-centered">Gốc</th>
<th class="has-background-primary has-text-white has-text-centered">Phạt</th>
<th class="has-background-primary has-text-white has-text-centered">Miễn lãi</th>
<th class="has-background-primary has-text-white has-text-centered">Ngày phân bổ</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in record.allocation_detail" :key="index">
<td class="has-text-centered">{{ index + 1 }}</td>
<td>
<span class="tag is-link is-light">{{ item.schedule_code || item.schedule_id }}</span>
</td>
<td class="has-text-centered">
<span v-if="item.type === 'REDUCTION'" class="tag is-warning">Miễn lãi</span>
<span v-else class="tag is-success">Thanh toán</span>
</td>
<td class="has-text-right">
<strong>{{ $numtoString(item.amount) }}</strong>
</td>
<td class="has-text-right">
<span v-if="item.principal" class="has-text-info has-text-weight-semibold">
{{ $numtoString(item.principal) }}
</span>
<span v-else class="has-text-grey-light">-</span>
</td>
<td class="has-text-right">
<span v-if="item.penalty" class="has-text-danger has-text-weight-semibold">
{{ $numtoString(item.penalty) }}
</span>
<span v-else class="has-text-grey-light">-</span>
</td>
<td class="has-text-right">
<span v-if="item.penalty" class="has-text-danger has-text-weight-semibold">
{{ $numtoString(item.penalty_reduce) }}
</span>
<span v-else class="has-text-grey-light">-</span>
</td>
<td class="has-text-centered">{{ $dayjs(item.date).format('DD/MM/YYYY HH:mm:ss') }}</td>
</tr>
</tbody>
<tfoot>
<tr class="has-background-light">
<td colspan="3" class="has-text-right has-text-weight-bold">Tổng cộng:</td>
<td class="has-text-right has-text-weight-bold">{{ $numtoString(totalAllocated) }}</td>
<td class="has-text-right has-text-weight-bold has-text-info">{{
$numtoString(totalPrincipal) }}</td>
<td class="has-text-right has-text-weight-bold has-text-danger">{{
$numtoString(totalPenalty) }}</td>
<td class="has-text-right has-text-weight-bold has-text-danger">{{
$numtoString(totalPenaltyReduce) }}</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div> </div>
<div v-else class="notification is-info is-light mt-4"> </div>
<p class="has-text-centered">Chưa dữ liệu phân bổ cho bút toán này.</p> <div class="column is-3">
<div class="field">
<label class="label">Ref:</label>
<div class="control">
{{ `${record.ref || "/"}` }}
</div>
</div> </div>
</div>
<Caption class="mt-5 " v-bind="{ title: 'Chứng từ' }"></Caption> <div class="column is-8">
<FileGallery v-bind="{ row: record, api: 'entryfile' }"></FileGallery> <div class="field">
<div class="mt-5" id="ignore"> <label class="label">{{ $lang("content") }}:</label>
<button class="button is-primary has-text-white mr-2" @click="$exportpdf('printable', record.code, 'a4', 'landscape')">{{ $lang('print') }}</button> <div class="control">
<button v-if="record.category === 2" class="button is-light" @click="viewPhieuThuTienMat">Xem phiếu thu</button> {{ `${record.content}` }}
</div>
</div> </div>
</div>
</div> </div>
<!-- PHẦN THÔNG TIN PHÂN BỔ -->
<Caption v-bind="{ title: 'Thông tin phân bổ' }" />
<!-- BẢNG CHI TIẾT PHÂN BỔ -->
<div
v-if="record.allocation_detail && record.allocation_detail.length > 0"
class="mt-4"
>
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
<thead>
<tr class="">
<th class="has-background-primary has-text-white has-text-centered">STT</th>
<th class="has-background-primary has-text-white has-text-centered"> lịch</th>
<th class="has-background-primary has-text-white has-text-centered">Loại</th>
<th class="has-background-primary has-text-white has-text-centered">Tổng phân bổ</th>
<th class="has-background-primary has-text-white has-text-centered">Gốc</th>
<th class="has-background-primary has-text-white has-text-centered">Phạt</th>
<th class="has-background-primary has-text-white has-text-centered">Miễn lãi</th>
<th class="has-background-primary has-text-white has-text-centered">Ngày phân bổ</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in record.allocation_detail"
:key="index"
>
<td class="has-text-centered">{{ index + 1 }}</td>
<td>
<span class="tag is-link is-light">{{ item.schedule_code || item.schedule_id }}</span>
</td>
<td class="has-text-centered">
<span
v-if="item.type === 'REDUCTION'"
class="tag is-warning"
>Miễn lãi</span
>
<span
v-else
class="tag is-success"
>Thanh toán</span
>
</td>
<td class="has-text-right">
<strong>{{ $numtoString(item.amount) }}</strong>
</td>
<td class="has-text-right">
<span
v-if="item.principal"
class="has-text-info has-text-weight-semibold"
>
{{ $numtoString(item.principal) }}
</span>
<span
v-else
class="has-text-grey-light"
>-</span
>
</td>
<td class="has-text-right">
<span
v-if="item.penalty"
class="has-text-danger has-text-weight-semibold"
>
{{ $numtoString(item.penalty) }}
</span>
<span
v-else
class="has-text-grey-light"
>-</span
>
</td>
<td class="has-text-right">
<span
v-if="item.penalty"
class="has-text-danger has-text-weight-semibold"
>
{{ $numtoString(item.penalty_reduce) }}
</span>
<span
v-else
class="has-text-grey-light"
>-</span
>
</td>
<td class="has-text-centered">
{{ $dayjs(item.date).format("DD/MM/YYYY HH:mm:ss") }}
</td>
</tr>
</tbody>
<tfoot>
<tr class="has-background-light">
<td
colspan="3"
class="has-text-right has-text-weight-bold"
>
Tổng cộng:
</td>
<td class="has-text-right has-text-weight-bold">
{{ $numtoString(totalAllocated) }}
</td>
<td class="has-text-right has-text-weight-bold has-text-info">
{{ $numtoString(totalPrincipal) }}
</td>
<td class="has-text-right has-text-weight-bold has-text-danger">
{{ $numtoString(totalPenalty) }}
</td>
<td class="has-text-right has-text-weight-bold has-text-danger">
{{ $numtoString(totalPenaltyReduce) }}
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div
v-else
class="notification is-info is-light mt-4"
>
<p class="has-text-centered">Chưa dữ liệu phân bổ cho bút toán này.</p>
</div>
<Caption
class="mt-5"
v-bind="{ title: 'Chứng từ' }"
></Caption>
<FileGallery v-bind="{ row: record, api: 'entryfile' }"></FileGallery>
<div
class="mt-5"
id="ignore"
>
<button
class="button is-primary has-text-white mr-2"
@click="$exportpdf('printable', record.code, 'a4', 'landscape')"
>
{{ $lang("print") }}
</button>
<button
v-if="record.category === 2"
class="button is-light"
@click="viewPhieuThuTienMat"
>
Xem phiếu thu
</button>
</div>
</div>
</template> </template>
<script> <script>
export default { export default {
props: ['row'], props: ["row"],
data() { data() {
return { return {
errors: {}, errors: {},
record: undefined record: undefined,
} };
},
async created() {
this.record = await this.$getdata("internalentry", { code: this.row.code }, undefined, true);
},
computed: {
// Tính tổng số tiền đã phân bổ
totalAllocated() {
if (!this.record || !this.record.allocation_detail) return 0;
return this.record.allocation_detail.reduce((sum, item) => sum + (item.amount || 0), 0);
}, },
async created() { // Tính tổng gốc
this.record = await this.$getdata('internalentry', { code: this.row.code }, undefined, true) totalPrincipal() {
if (!this.record || !this.record.allocation_detail) return 0;
return this.record.allocation_detail.reduce((sum, item) => sum + (item.principal || 0), 0);
}, },
computed: { // Tính tổng phạt
totalPenalty() {
// Tính tổng số tiền đã phân bổ if (!this.record || !this.record.allocation_detail) return 0;
totalAllocated() { return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty || 0), 0);
if (!this.record || !this.record.allocation_detail) return 0
return this.record.allocation_detail.reduce((sum, item) => sum + (item.amount || 0), 0)
},
// Tính tổng gốc
totalPrincipal() {
if (!this.record || !this.record.allocation_detail) return 0
return this.record.allocation_detail.reduce((sum, item) => sum + (item.principal || 0), 0)
},
// Tính tổng phạt
totalPenalty() {
if (!this.record || !this.record.allocation_detail) return 0
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty || 0), 0)
},
// Tính tổng phạt đã giảm
totalPenaltyReduce() {
if (!this.record || !this.record.allocation_detail) return 0
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty_reduce || 0), 0)
}
}, },
methods: { // Tính tổng phạt đã giảm
selected(attr, obj) { totalPenaltyReduce() {
this.record[attr] = obj if (!this.record || !this.record.allocation_detail) return 0;
this.record = this.$copy(this.record) return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty_reduce || 0), 0);
if (attr === '_type') this.category = obj.category__code },
}, },
viewPhieuThuTienMat() { methods: {
const url = `${this.$getpath()}static/contract/PHIEU_THU_TIEN_MAT-${this.record.code}.pdf`; selected(attr, obj) {
window.open(url, '_blank'); this.record[attr] = obj;
} this.record = this.$copy(this.record);
} if (attr === "_type") this.category = obj.category__code;
} },
viewPhieuThuTienMat() {
const url = `${this.$getpath()}static/contract/PHIEU_THU_TIEN_MAT-${this.record.code}.pdf`;
window.open(url, "_blank");
},
},
};
</script> </script>
<style scoped> <style scoped>
.column { .column {
padding-inline: 0; padding-inline: 0;
} }
</style> </style>

View File

@@ -1,104 +1,191 @@
<template> <template>
<div> <div>
<div class="columns is-multiline mx-0"> <div class="columns is-multiline mx-0">
<div class="column is-12"> <div class="column is-12">
<div class="field"> <div class="field">
<label class="label">{{$lang('source-account')}}<b class="ml-1 has-text-danger">*</b></label> <label class="label">{{ $lang("source-account") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control"> <div class="control">
<SearchBox v-bind="{api:'internalaccount', field:'label', column:['label'], first: true, optionid: row.id}" <SearchBox
@option="selected('_source', $event)" v-if="!record.id"></SearchBox> v-bind="{
<span v-else>{{record.account__code}}</span> api: 'internalaccount',
</div> field: 'label',
<p class="help is-danger" v-if="errors.source">{{ errors.source }}</p> column: ['label'],
</div> first: true,
</div> optionid: row.id,
<div class="column is-12"> }"
<div class="field"> @option="selected('_source', $event)"
<label class="label">{{$lang('dest-account')}}<b class="ml-1 has-text-danger">*</b></label> v-if="!record.id"
<div class="control"> ></SearchBox>
<SearchBox v-bind="vbind" @option="selected('_target', $event)" v-if="vbind"></SearchBox> <span v-else>{{ record.account__code }}</span>
<span v-else>{{record.account__code}}</span> </div>
</div> <p
<p class="help is-danger" v-if="errors.target">{{ errors.target }}</p> class="help is-danger"
</div> v-if="errors.source"
</div> >
<div class="column is-12"> {{ errors.source }}
<div class="field"> </p>
<label class="label">{{ $lang('amount-only') }}<b class="ml-1 has-text-danger">*</b></label> </div>
<div class="control"> </div>
<InputNumber v-bind="{record: record, attr: 'amount', placeholder: ''}" @number="selected('amount', $event)"></InputNumber> <div class="column is-12">
<div class="field">
<label class="label">{{ $lang("dest-account") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox
v-bind="vbind"
@option="selected('_target', $event)"
v-if="vbind"
></SearchBox>
<span v-else>{{ record.account__code }}</span>
</div>
<p
class="help is-danger"
v-if="errors.target"
>
{{ errors.target }}
</p>
</div>
</div>
<div class="column is-12">
<div class="field">
<label class="label">{{ $lang("amount-only") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<InputNumber
v-bind="{ record: record, attr: 'amount', placeholder: '' }"
@number="selected('amount', $event)"
></InputNumber>
</div>
<p
class="help is-danger"
v-if="errors.amount"
>
{{ errors.amount }}
</p>
</div>
</div>
<div class="column is-12">
<div class="field">
<label class="label">{{ $lang("content") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<textarea
class="textarea"
rows="2"
v-model="record.content"
></textarea>
</div>
<p
class="help is-danger"
v-if="errors.content"
>
{{ errors.content }}
</p>
</div>
</div>
</div> </div>
<p class="help is-danger" v-if="errors.amount">{{errors.amount}}</p> <div class="mt-5">
<button
class="button is-primary has-text-white"
@click="confirm()"
>
{{ $lang("confirm") }}
</button>
</div> </div>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
@confirm="update()"
v-if="showmodal"
></Modal>
</div> </div>
<div class="column is-12">
<div class="field">
<label class="label">{{ $lang('content') }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<textarea class="textarea" rows="2" v-model="record.content"></textarea>
</div>
<p class="help is-danger" v-if="errors.content">{{errors.content}}</p>
</div>
</div>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="confirm()">{{ $lang('confirm') }}</button>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" @confirm="update()" v-if="showmodal"></Modal>
</div>
</template> </template>
<script> <script>
export default { export default {
props: ['pagename', 'row'], props: ["pagename", "row"],
data() { data() {
return { return {
record: {}, record: {},
errors: {}, errors: {},
showmodal: undefined, showmodal: undefined,
vbind: undefined vbind: undefined,
} };
}, },
methods: { methods: {
selected(attr, obj) { selected(attr, obj) {
this.record[attr] = obj this.record[attr] = obj;
this.record = this.$copy(this.record) this.record = this.$copy(this.record);
if(attr==='_source') { if (attr === "_source") {
let currency = obj? obj.currency : undefined let currency = obj ? obj.currency : undefined;
this.vbind = undefined this.vbind = undefined;
setTimeout(()=>this.vbind = {api:'internalaccount', field:'label', column:['label'], first: true, filter: {currency: currency}}) setTimeout(
() =>
(this.vbind = {
api: "internalaccount",
field: "label",
column: ["label"],
first: true,
filter: { currency: currency },
}),
);
} }
}, },
checkError() { checkError() {
this.errors = {} this.errors = {};
if(this.$empty(this.record._source)) this.errors.source = 'Chưa chọn tài khoản nguồn' if (this.$empty(this.record._source)) this.errors.source = "Chưa chọn tài khoản nguồn";
if(this.$empty(this.record._target)) this.errors.target = 'Chưa chọn tài khoản đích' if (this.$empty(this.record._target)) this.errors.target = "Chưa chọn tài khoản đích";
if(Object.keys(this.errors).length===0) { if (Object.keys(this.errors).length === 0) {
if(this.record._source.id===this.record._target.id) this.errors.target = 'Tài khoản nguồn phải khác tài khoản đích' if (this.record._source.id === this.record._target.id)
this.errors.target = "Tài khoản nguồn phải khác tài khoản đích";
} }
if(this.$empty(this.record.amount)) this.errors.amount = 'Chưa nhập số tiền' if (this.$empty(this.record.amount)) this.errors.amount = "Chưa nhập số tiền";
else if(this.$formatNumber(this.record.amount)<=0) this.errors.amount = 'Số tiền phải > 0' else if (this.$formatNumber(this.record.amount) <= 0) this.errors.amount = "Số tiền phải > 0";
else if(this.record._source.balance<this.$formatNumber(this.record.amount)) this.errors.source = 'Tài khoản nguồn không đủ số để điều chuyển' else if (this.record._source.balance < this.$formatNumber(this.record.amount))
if(this.$empty(this.record.content)) this.errors.content = 'Chưa nhập nội dung' this.errors.source = "Tài khoản nguồn không đủ số dư để điều chuyển";
return Object.keys(this.errors).length>0 if (this.$empty(this.record.content)) this.errors.content = "Chưa nhập nội dung";
return Object.keys(this.errors).length > 0;
}, },
confirm() { confirm() {
if(this.checkError()) return if (this.checkError()) return;
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10}, this.showmodal = {
title: this.$lang('confirm'), width: '500px', height: '100px'} component: `dialog/Confirm`,
vbind: { content: this.$lang("confirm-action"), duration: 10 },
title: this.$lang("confirm"),
width: "500px",
height: "100px",
};
}, },
async update() { async update() {
let content = `${this.record.content} (${this.record._source.code} -> ${this.record._target.code})` let content = `${this.record.content} (${this.record._source.code} -> ${this.record._target.code})`;
let obj1 = {code: this.record._source.code, amount: this.record.amount, content: content, type: 'DR', category: 2, user: this.$store.login.id} let obj1 = {
let rs1 = await this.$insertapi('accountentry', obj1, undefined, false) code: this.record._source.code,
if(rs1==='error') return amount: this.record.amount,
let obj2 = {code: this.record._target.code, amount: this.record.amount, content: content, type: 'CR', category: 2, user: this.$store.login.id} content: content,
let rs2 = await this.$insertapi('accountentry', obj2, undefined, false) type: "DR",
if(rs2==='error') return category: 2,
let data = await this.$getdata('internalaccount', {code__in: [this.record._source.code, this.record._target.code]}) user: this.$store.login.id,
this.$updatepage(this.pagename, data) };
this.$dialog(`Điều chuyển vốn <b>${this.$numtoString(this.record.amount)}</b> từ <b>${this.record._source.code}</b> tới <b>${this.record._target.code}</b> thành công.`, 'Thành công', 'Success', 10) let rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
this.$emit('close') if (rs1 === "error") return;
} let obj2 = {
} code: this.record._target.code,
} amount: this.record.amount,
</script> content: content,
type: "CR",
category: 2,
user: this.$store.login.id,
};
let rs2 = await this.$insertapi("accountentry", obj2, undefined, false);
if (rs2 === "error") return;
let data = await this.$getdata("internalaccount", {
code__in: [this.record._source.code, this.record._target.code],
});
this.$updatepage(this.pagename, data);
this.$dialog(
`Điều chuyển vốn <b>${this.$numtoString(this.record.amount)}</b> từ <b>${this.record._source.code}</b> tới <b>${this.record._target.code}</b> thành công.`,
"Thành công",
"Success",
10,
);
this.$emit("close");
},
},
};
</script>

View File

@@ -16,7 +16,10 @@
</div> </div>
<div class="column is-1"></div> <div class="column is-1"></div>
</div> </div>
<div class="columns" v-for="(invoice, index) in invoices"> <div
class="columns"
v-for="(invoice, index) in invoices"
>
<div class="column"> <div class="column">
<input <input
class="input has-text-centered has-text-weight-bold has-text-left" class="input has-text-centered has-text-weight-bold has-text-left"
@@ -32,7 +35,12 @@
}) })
" "
/> />
<p v-if="invoice.errorLink" class="help is-danger">Link phải bắt đầu bằng https</p> <p
v-if="invoice.errorLink"
class="help is-danger"
>
Link phải bắt đầu bằng https
</p>
</div> </div>
<div class="column is-2"> <div class="column is-2">
<input <input
@@ -49,7 +57,12 @@
}) })
" "
/> />
<p v-if="invoice.errorCode" class="help is-danger"> tra cứu không được bỏ trống</p> <p
v-if="invoice.errorCode"
class="help is-danger"
>
tra cứu không được bỏ trống
</p>
</div> </div>
<div class="column is-2"> <div class="column is-2">
<input <input
@@ -66,7 +79,12 @@
}) })
" "
/> />
<p v-if="invoice.errorAmount" class="help is-danger">Số tiền không được bỏ trống</p> <p
v-if="invoice.errorAmount"
class="help is-danger"
>
Số tiền không được bỏ trống
</p>
</div> </div>
<div class="column is-2"> <div class="column is-2">
<select <select
@@ -84,22 +102,44 @@
<option value="principal">Tiền gốc</option> <option value="principal">Tiền gốc</option>
<option value="interest">Tiền lãi</option> <option value="interest">Tiền lãi</option>
</select> </select>
<p v-if="invoice.errorType" class="help is-danger">Loại tiền không được bỏ trống</p> <p
v-if="invoice.errorType"
class="help is-danger"
>
Loại tiền không được bỏ trống
</p>
</div> </div>
<div class="column is-narrow is-1"> <div class="column is-narrow is-1">
<label class="label" v-if="i === 0">&nbsp;</label> <label
<div class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small" style="height: 40px"> class="label"
<button class="button is-dark" @click="handlerRemove(index)"> v-if="i === 0"
>&nbsp;</label
>
<div
class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small"
style="height: 40px"
>
<button
class="button is-dark"
@click="handlerRemove(index)"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
</span> </span>
</button> </button>
<button class="button is-dark" @click="add()"> <button
class="button is-dark"
@click="add()"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
</span> </span>
</button> </button>
<a class="button is-dark" :href="invoice.link" target="_blank"> <a
class="button is-dark"
:href="invoice.link"
target="_blank"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 20 }"></SvgIcon>
</span> </span>
@@ -108,8 +148,18 @@
</div> </div>
</div> </div>
<div class="mt-5 buttons is-right"> <div class="mt-5 buttons is-right">
<button class="button" @click="emit('close')">{{ isVietnamese ? "Hủy" : "Cancel" }}</button> <button
<button class="button is-primary" @click="handlerUpdate">{{ isVietnamese ? "Lưu lại" : "Save" }}</button> class="button"
@click="emit('close')"
>
{{ isVietnamese ? "Hủy" : "Cancel" }}
</button>
<button
class="button is-primary"
@click="handlerUpdate"
>
{{ isVietnamese ? "Lưu lại" : "Save" }}
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -157,7 +207,11 @@ if (resInvoice.length) {
errorAmount: false, errorAmount: false,
errorType: false, errorType: false,
}; };
const formatData = resInvoice.map((invoice) => ({ ...invoice, amount: $formatNumber(invoice.amount), ...error })); const formatData = resInvoice.map((invoice) => ({
...invoice,
amount: $formatNumber(invoice.amount),
...error,
}));
invoices.value = formatData; invoices.value = formatData;
} }

View File

@@ -17,5 +17,4 @@ const props = defineProps({
}); });
const emit = defineEmits(["remove"]); const emit = defineEmits(["remove"]);
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,22 @@
<template> <template>
<div :id="docid"> <div :id="docid">
<div :id="docid1"> <div :id="docid1">
<Caption v-bind="{ title: isVietnamese? 'Thanh toán' : 'Payment', type: 'has-text-warning' }"></Caption> <Caption
v-bind="{
title: isVietnamese ? 'Thanh toán' : 'Payment',
type: 'has-text-warning',
}"
></Caption>
<div class="columns is-multiline mx-0"> <div class="columns is-multiline mx-0">
<div class="column is-4 pb-1 px-0"> <div class="column is-4 pb-1 px-0">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("loan_code")[lang] }}</label> <label class="label">{{ dataLang && findFieldName("loan_code")[lang] }}</label>
<div class="control"> <div class="control">
<span class="hyperlink" @click="$copyToClipboard(record.code)">{{ record?.code || "/" }}</span> <span
class="hyperlink"
@click="$copyToClipboard(record.code)"
>{{ record?.code || "/" }}</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -54,7 +63,7 @@
<div class="control"> <div class="control">
<span>{{ record?.commission ? $numtoString(record.commission) : "/" }}</span> <span>{{ record?.commission ? $numtoString(record.commission) : "/" }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="column is-5 pb-1 px-0"> <div class="column is-5 pb-1 px-0">
@@ -76,14 +85,18 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="mt-2 border-bottom"></div> --> <!-- <div class="mt-2 border-bottom"></div> -->
<div class="buttons mt-5" id="ignore"> <div
<button class="button is-primary has-text-white mt-2" @click="handleUpdate()"> class="buttons mt-5"
id="ignore"
>
<button
class="button is-primary has-text-white mt-2"
@click="handleUpdate()"
>
{{ dataLang && findFieldName("update")[lang] }} {{ dataLang && findFieldName("update")[lang] }}
</button> </button>
</div> </div>

View File

@@ -1,17 +1,28 @@
<template> <template>
<div :id="docid"> <div :id="docid">
<!-- Loading state --> <!-- Loading state -->
<div v-if="isLoading" class="has-text-centered mt-5 mb-5" style="min-height: 500px"> <div
v-if="isLoading"
class="has-text-centered mt-5 mb-5"
style="min-height: 500px"
>
<button class="button is-primary is-loading is-large"></button> <button class="button is-primary is-loading is-large"></button>
<p class="mt-4 has-text-primary has-text-weight-semibold"> <p class="mt-4 has-text-primary has-text-weight-semibold">
{{ isVietnamese ? 'Đang tải hợp đồng...' : 'Loading contracts...' }} {{ isVietnamese ? "Đang tải hợp đồng..." : "Loading contracts..." }}
</p> </p>
</div> </div>
<!-- No contract state --> <!-- No contract state -->
<div v-else-if="!hasContracts" class="has-text-centered mt-5 mb-5" style="min-height: 500px"> <div
v-else-if="!hasContracts"
class="has-text-centered mt-5 mb-5"
style="min-height: 500px"
>
<article class="message is-primary"> <article class="message is-primary">
<div class="message-body" style="font-size: 17px; text-align: left; color: black"> <div
class="message-body"
style="font-size: 17px; text-align: left; color: black"
>
{{ {{
isVietnamese isVietnamese
? "Chưa có hợp đồng. Vui lòng tạo giao dịch và hợp đồng trước." ? "Chưa có hợp đồng. Vui lòng tạo giao dịch và hợp đồng trước."
@@ -24,10 +35,22 @@
<!-- Contracts list --> <!-- Contracts list -->
<template v-else> <template v-else>
<!-- Tabs khi nhiều hợp đồng --> <!-- Tabs khi nhiều hợp đồng -->
<div class="tabs border-bottom" id="ignore" v-if="contractsList.length > 1"> <div
class="tabs border-bottom"
id="ignore"
v-if="contractsList.length > 1"
>
<ul class="tabs-list"> <ul class="tabs-list">
<li class="tabs-item" style="border: none" v-for="(contract, index) in contractsList" :key="index" <li
:class="{ 'bg-primary has-text-white': activeContractIndex === index }" @click="switchContract(index)"> class="tabs-item"
style="border: none"
v-for="(contract, index) in contractsList"
:key="index"
:class="{
'bg-primary has-text-white': activeContractIndex === index,
}"
@click="switchContract(index)"
>
<a class="tabs-link"> <a class="tabs-link">
<span>{{ contract.document[0]?.name || contract.document[0]?.en || `Contract ${index + 1}` }}</span> <span>{{ contract.document[0]?.name || contract.document[0]?.en || `Contract ${index + 1}` }}</span>
</a> </a>
@@ -38,23 +61,42 @@
<!-- Contract content --> <!-- Contract content -->
<div v-if="currentContract && pdfFileUrl && hasValidDocument"> <div v-if="currentContract && pdfFileUrl && hasValidDocument">
<div class="contract-content mt-2"> <div class="contract-content mt-2">
<iframe :src="`https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfFileUrl}`" width="100%" <iframe
height="90vh" scrolling="no" style="border: none; height: 75vh; top: 0; left: 0; right: 0; bottom: 0"> :src="`https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfFileUrl}`"
width="100%"
height="90vh"
scrolling="no"
style="border: none; height: 75vh; top: 0; left: 0; right: 0; bottom: 0"
>
</iframe> </iframe>
</div> </div>
</div> </div>
<!-- Download buttons --> <!-- Download buttons -->
<div class="mt-4" id="ignore"> <div
<button v-if="hasValidDocument" class="button is-primary has-text-white mr-4" @click="downloadDocx"> class="mt-4"
id="ignore"
>
<button
v-if="hasValidDocument"
class="button is-primary has-text-white mr-4"
@click="downloadDocx"
>
{{ isVietnamese ? "Tải file docx" : "Download contract as docx" }} {{ isVietnamese ? "Tải file docx" : "Download contract as docx" }}
</button> </button>
<button v-if="hasValidDocument" class="button is-primary has-text-white mr-4" @click="downloadPdf"> <button
v-if="hasValidDocument"
class="button is-primary has-text-white mr-4"
@click="downloadPdf"
>
{{ isVietnamese ? "Tải file pdf" : "Download contract as pdf" }} {{ isVietnamese ? "Tải file pdf" : "Download contract as pdf" }}
</button> </button>
<p v-if="contractError" class="has-text-danger mt-2"> <p
v-if="contractError"
class="has-text-danger mt-2"
>
{{ contractError }} {{ contractError }}
</p> </p>
</div> </div>
@@ -73,16 +115,16 @@ export default {
props: { props: {
contractId: { contractId: {
type: [Number, String], type: [Number, String],
default: null default: null,
}, },
row: { row: {
type: Object, type: Object,
default: null default: null,
}, },
directDocument: { directDocument: {
type: Object, type: Object,
default: null default: null,
} },
}, },
emits: ["contractCreated", "update", "close", "dataevent"], emits: ["contractCreated", "update", "close", "dataevent"],
data() { data() {
@@ -107,10 +149,12 @@ export default {
}, },
hasValidDocument() { hasValidDocument() {
if (!this.currentContract) return false; if (!this.currentContract) return false;
return this.currentContract.document && return (
this.currentContract.document &&
this.currentContract.document.length > 0 && this.currentContract.document.length > 0 &&
this.currentContract.document[0]?.pdf; this.currentContract.document[0]?.pdf
} );
},
}, },
async created() { async created() {
try { try {
@@ -118,11 +162,9 @@ export default {
this.contractError = null; this.contractError = null;
if (this.directDocument) { if (this.directDocument) {
this.contractsList = [ this.contractsList = [{ document: [this.directDocument] }];
{ document: [this.directDocument] }
];
this.updatePdfUrl(0); this.updatePdfUrl(0);
return; return;
} }
let contracts = []; let contracts = [];
@@ -130,31 +172,22 @@ export default {
if (this.contractId) { if (this.contractId) {
fetchParams = { id: this.contractId }; fetchParams = { id: this.contractId };
} } else if (this.row?.id) {
else if (this.row?.id) {
fetchParams = { transaction: this.row.id }; fetchParams = { transaction: this.row.id };
} }
if (!fetchParams) { if (!fetchParams) {
throw new Error( throw new Error(
this.isVietnamese this.isVietnamese
? 'Không có ID hợp đồng hoặc transaction để tải.' ? "Không có ID hợp đồng hoặc transaction để tải."
: 'No contract ID or transaction provided to load.' : "No contract ID or transaction provided to load.",
); );
} }
contracts = await this.$getdata( contracts = await this.$getdata("contract", fetchParams, undefined);
'contract',
fetchParams,
undefined
);
if (!contracts || contracts.length === 0) { if (!contracts || contracts.length === 0) {
throw new Error( throw new Error(this.isVietnamese ? "Không tìm thấy hợp đồng." : "Contract not found.");
this.isVietnamese
? 'Không tìm thấy hợp đồng.'
: 'Contract not found.'
);
} }
this.contractsList = contracts; this.contractsList = contracts;
@@ -164,12 +197,9 @@ export default {
this.updatePdfUrl(this.activeContractIndex); this.updatePdfUrl(this.activeContractIndex);
} }
} catch (error) { } catch (error) {
console.error('Error loading contracts:', error); console.error("Error loading contracts:", error);
this.contractError = error.message || ( this.contractError =
this.isVietnamese error.message || (this.isVietnamese ? "Lỗi khi tải danh sách hợp đồng." : "Error loading contracts list.");
? 'Lỗi khi tải danh sách hợp đồng.'
: 'Error loading contracts list.'
);
this.contractsList = []; this.contractsList = [];
} finally { } finally {
this.isLoading = false; this.isLoading = false;
@@ -192,10 +222,7 @@ export default {
downloadDocx() { downloadDocx() {
if (!this.hasValidDocument) { if (!this.hasValidDocument) {
this.$snackbar( this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
this.isVietnamese ? "Không có file để tải" : "No file to download",
{ type: 'is-warning' }
);
return; return;
} }
@@ -206,10 +233,7 @@ export default {
downloadPdf() { downloadPdf() {
if (!this.hasValidDocument) { if (!this.hasValidDocument) {
this.$snackbar( this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
this.isVietnamese ? "Không có file để tải" : "No file to download",
{ type: 'is-warning' }
);
return; return;
} }
@@ -259,4 +283,4 @@ export default {
max-width: 100%; max-width: 100%;
} }
} }
</style> </style>

View File

@@ -1,17 +1,29 @@
<template> <template>
<div> <div>
<!-- Loading Overlay --> <!-- Loading Overlay -->
<div v-if="isLoading" class="loading-overlay"> <div
v-if="isLoading"
class="loading-overlay"
>
<div class="loader"></div> <div class="loader"></div>
</div> </div>
<!-- View Mode Toggle --> <!-- View Mode Toggle -->
<div class="mb-5 pb-3" style="border-bottom: 2px solid #e8e8e8;"> <div
<div class="buttons has-addons "> class="mb-5 pb-3"
<button @click="viewMode = 'list'" :class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']"> style="border-bottom: 2px solid #e8e8e8"
>
<div class="buttons has-addons">
<button
@click="viewMode = 'list'"
:class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']"
>
Danh sách Danh sách
</button> </button>
<button @click="viewMode = 'gallery'" :class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']"> <button
@click="viewMode = 'gallery'"
:class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']"
>
Thư viện Thư viện
</button> </button>
</div> </div>
@@ -19,8 +31,11 @@
<!-- Phase Document Types List --> <!-- Phase Document Types List -->
<div v-if="phasedoctypes && phasedoctypes.length > 0"> <div v-if="phasedoctypes && phasedoctypes.length > 0">
<div v-for="doctype in phasedoctypes" :key="doctype.id" class="mb-6"> <div
v-for="doctype in phasedoctypes"
:key="doctype.id"
class="mb-6"
>
<!-- Document Type Header with Upload Button --> <!-- Document Type Header with Upload Button -->
<div class="level is-mobile mb-4"> <div class="level is-mobile mb-4">
<div class="level-left"> <div class="level-left">
@@ -34,9 +49,9 @@
<div class="level-item"> <div class="level-item">
<FileUpload <FileUpload
v-if="$getEditRights()" v-if="$getEditRights()"
:type="['file', 'image', 'pdf']" :type="['file', 'image', 'pdf']"
@files="(files) => handleUpload(files, doctype.doctype)" @files="(files) => handleUpload(files, doctype.doctype)"
position="right" position="right"
/> />
</div> </div>
</div> </div>
@@ -45,112 +60,195 @@
<!-- List View --> <!-- List View -->
<div v-if="viewMode === 'list'"> <div v-if="viewMode === 'list'">
<div v-if="getFilesByDocType(doctype.doctype).length > 0"> <div v-if="getFilesByDocType(doctype.doctype).length > 0">
<div v-for="file in getFilesByDocType(doctype.doctype)" :key="file.id" <div
v-for="file in getFilesByDocType(doctype.doctype)"
:key="file.id"
class="is-flex is-justify-content-space-between is-align-items-center py-3 px-4 has-background-warning has-text-white" class="is-flex is-justify-content-space-between is-align-items-center py-3 px-4 has-background-warning has-text-white"
style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer;" style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer"
@mouseenter="$event.currentTarget.style.opacity = '1'" @mouseenter="$event.currentTarget.style.opacity = '1'"
@mouseleave="$event.currentTarget.style.opacity = '0.95'"> @mouseleave="$event.currentTarget.style.opacity = '0.95'"
>
<div style="flex: 1; min-width: 0;"> <div style="flex: 1; min-width: 0">
<p class="is-size-7 has-text-weight-semibold has-text-white mb-1" style="word-break: break-word;"> <p
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
style="word-break: break-word"
>
{{ file.name || file.file__name }} {{ file.name || file.file__name }}
</p> </p>
<p class="is-size-7 has-text-white-bis"> <p class="is-size-7 has-text-white-bis">
{{ $formatFileSize(file.file__size) }} {{ $dayjs(file.create_time).format("DD/MM/YYYY HH:mm") }} {{ $formatFileSize(file.file__size) }}
{{ $dayjs(file.create_time).format("DD/MM/YYYY HH:mm") }}
</p> </p>
</div> </div>
<div class="buttons are-small ml-3"> <div class="buttons are-small ml-3">
<button @click="viewFile(file)" class="button has-background-white has-text-primary "> <button
@click="viewFile(file)"
class="button has-background-white has-text-primary"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ <SvgIcon
name: 'view.svg', v-bind="{
type: 'success', name: 'view.svg',
size: 18, type: 'success',
}"></SvgIcon> size: 18,
}"
></SvgIcon>
</span> </span>
</button> </button>
<button @click="downloadFile(file)" class="button has-background-white has-text-primary "> <button
@click="downloadFile(file)"
class="button has-background-white has-text-primary"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ <SvgIcon
name: 'download.svg', v-bind="{
type: 'success', name: 'download.svg',
size: 18, type: 'success',
}"></SvgIcon> size: 18,
}"
></SvgIcon>
</span> </span>
</button> </button>
<button @click="deleteFile(file.id)" class="button has-background-white has-text-danger "> <button
@click="deleteFile(file.id)"
class="button has-background-white has-text-danger"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ <SvgIcon
name: 'bin.svg', v-bind="{
type: 'danger', name: 'bin.svg',
size: 18, type: 'danger',
}"></SvgIcon> size: 18,
}"
></SvgIcon>
</span> </span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div v-else class="has-text-grey-light is-size-7 has-text-centered py-5"> <div
v-else
class="has-text-grey-light is-size-7 has-text-centered py-5"
>
Chưa có file nào Chưa có file nào
</div> </div>
</div> </div>
<!-- Gallery View --> <!-- Gallery View -->
<div v-if="viewMode === 'gallery'"> <div v-if="viewMode === 'gallery'">
<div v-if="getFilesByDocType(doctype.doctype).length > 0" class="columns is-multiline is-variable is-2"> <div
<div v-for="file in getFilesByDocType(doctype.doctype)" :key="file.id" v-if="getFilesByDocType(doctype.doctype).length > 0"
class="column is-half-tablet is-one-third-desktop"> class="columns is-multiline is-variable is-2"
<div class="has-background-warning has-text-white" >
style="border-radius: 6px; overflow: hidden; height: 100%; display: flex; flex-direction: column; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);" <div
@mouseenter="$event.currentTarget.style.transform = 'translateY(-4px)'; $event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)'" v-for="file in getFilesByDocType(doctype.doctype)"
@mouseleave="$event.currentTarget.style.transform = 'translateY(0)'; $event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)'"> :key="file.id"
class="column is-half-tablet is-one-third-desktop"
>
<div
class="has-background-warning has-text-white"
style="
border-radius: 6px;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);
"
@mouseenter="
$event.currentTarget.style.transform = 'translateY(-4px)';
$event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)';
"
@mouseleave="
$event.currentTarget.style.transform = 'translateY(0)';
$event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)';
"
>
<div <div
style="flex: 1; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(255, 255, 255, 0.1); min-height: 140px;"> style="
<div v-if="isImage(file.file__name)" flex: 1;
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;"> display: flex;
<img :src="`${$getpath()}static/files/${file.file__file}`" :alt="file.file__name" align-items: center;
style="max-width: 100%; max-height: 100%; object-fit: contain;"> justify-content: center;
padding: 16px;
background: rgba(255, 255, 255, 0.1);
min-height: 140px;
"
>
<div
v-if="isImage(file.file__name)"
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center"
>
<img
:src="`${$getpath()}static/files/${file.file__file}`"
:alt="file.file__name"
style="max-width: 100%; max-height: 100%; object-fit: contain"
/>
</div> </div>
<div v-else class="has-text-white-ter" style="font-size: 48px; line-height: 1;"> <div
v-else
class="has-text-white-ter"
style="font-size: 48px; line-height: 1"
>
FILE FILE
</div> </div>
</div> </div>
<div style="padding: 12px 16px;"> <div style="padding: 12px 16px">
<p class="is-size-7 has-text-weight-semibold has-text-white mb-1" :title="file.file__name" <p
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> class="is-size-7 has-text-weight-semibold has-text-white mb-1"
:title="file.file__name"
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>
{{ file.file__name }} {{ file.file__name }}
</p> </p>
<p class="is-size-7 has-text-white-bis mb-3">{{ $formatFileSize(file.file__size) }}</p> <p class="is-size-7 has-text-white-bis mb-3">
{{ $formatFileSize(file.file__size) }}
</p>
<div class="buttons are-small is-centered"> <div class="buttons are-small is-centered">
<button @click="viewFile(file)" class="button has-background-white has-text-primary "> <button
@click="viewFile(file)"
class="button has-background-white has-text-primary"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ <SvgIcon
name: 'view.svg', v-bind="{
type: 'success', name: 'view.svg',
size: 18, type: 'success',
}"></SvgIcon> size: 18,
}"
></SvgIcon>
</span> </span>
</button> </button>
<button @click="downloadFile(file)" class="button has-background-white has-text-primary "> <button
@click="downloadFile(file)"
class="button has-background-white has-text-primary"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ <SvgIcon
name: 'download.svg', v-bind="{
type: 'success', name: 'download.svg',
size: 18, type: 'success',
}"></SvgIcon> size: 18,
}"
></SvgIcon>
</span> </span>
</button> </button>
<button @click="deleteFile(file.id)" class="button has-background-white has-text-danger "> <button
@click="deleteFile(file.id)"
class="button has-background-white has-text-danger"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ <SvgIcon
name: 'bin.svg', v-bind="{
type: 'danger', name: 'bin.svg',
size: 18, type: 'danger',
}"></SvgIcon> size: 18,
}"
></SvgIcon>
</span> </span>
</button> </button>
</div> </div>
@@ -158,7 +256,10 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else class="has-text-grey-light is-size-7 has-text-centered py-5"> <div
v-else
class="has-text-grey-light is-size-7 has-text-centered py-5"
>
Chưa có file nào Chưa có file nào
</div> </div>
</div> </div>
@@ -166,11 +267,19 @@
</div> </div>
<!-- If no phase doctypes --> <!-- If no phase doctypes -->
<div v-else-if="!isLoading" class="has-text-centered py-6"> <div
v-else-if="!isLoading"
class="has-text-centered py-6"
>
<p class="has-text-grey-light is-size-7">Chưa có loại tài liệu được định nghĩa cho giai đoạn này.</p> <p class="has-text-grey-light is-size-7">Chưa có loại tài liệu được định nghĩa cho giai đoạn này.</p>
</div> </div>
<Modal @close="showmodal = undefined" @modalevent="handleModalEvent" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@close="showmodal = undefined"
@modalevent="handleModalEvent"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div> </div>
</template> </template>
@@ -184,7 +293,7 @@ export default {
props: { props: {
row: { row: {
type: Object, type: Object,
required: true required: true,
}, },
}, },
data() { data() {
@@ -194,7 +303,7 @@ export default {
isLoading: false, isLoading: false,
showmodal: undefined, showmodal: undefined,
phasedoctypes: [], phasedoctypes: [],
viewMode: 'list', viewMode: "list",
}; };
}, },
async created() { async created() {
@@ -221,9 +330,14 @@ export default {
if (!this.transaction?.phase) return; if (!this.transaction?.phase) return;
try { try {
const phasedoctypesData = await $getdata('phasedoctype', { const phasedoctypesData = await $getdata(
phase: this.transaction.phase, "phasedoctype",
}, undefined, false); {
phase: this.transaction.phase,
},
undefined,
false,
);
if (phasedoctypesData) { if (phasedoctypesData) {
this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData]; this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData];
@@ -240,15 +354,27 @@ export default {
if (!this.row.id) return; if (!this.row.id) return;
this.isLoading = true; this.isLoading = true;
try { try {
const detail = await $getdata('reservation', { const detail = await $getdata(
id: this.transaction.txncurrent__detail "reservation",
}, undefined, true) {
const filesArray = await $getdata('transactionfile', { id: this.transaction.txncurrent__detail,
txn_detail: detail.id, },
}, undefined, false); undefined,
true,
);
const filesArray = await $getdata(
"transactionfile",
{
txn_detail: detail.id,
},
undefined,
false,
);
if (filesArray) { if (filesArray) {
this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort((a, b) => new Date(b.create_time) - new Date(a.create_time)); this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort(
(a, b) => new Date(b.create_time) - new Date(a.create_time),
);
} else { } else {
this.files = []; this.files = [];
} }
@@ -260,18 +386,20 @@ export default {
} }
}, },
getFilesByDocType(docTypeId) { getFilesByDocType(docTypeId) {
return this.files.filter(file => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null)); return this.files.filter(
(file) => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null),
);
}, },
getFileExtension(fileName) { getFileExtension(fileName) {
return fileName ? fileName.split('.').pop().toLowerCase() : 'file'; return fileName ? fileName.split(".").pop().toLowerCase() : "file";
}, },
isImage(fileName) { isImage(fileName) {
const ext = this.getFileExtension(fileName); const ext = this.getFileExtension(fileName);
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext); return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
}, },
isViewableDocument(fileName) { isViewableDocument(fileName) {
const ext = this.getFileExtension(fileName); const ext = this.getFileExtension(fileName);
return ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext); return ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(ext);
}, },
async handleUpload(uploadedFiles, docTypeId) { async handleUpload(uploadedFiles, docTypeId) {
if (!uploadedFiles || uploadedFiles.length === 0) return; if (!uploadedFiles || uploadedFiles.length === 0) return;
@@ -282,15 +410,20 @@ export default {
try { try {
for (const fileRecord of uploadedFiles) { for (const fileRecord of uploadedFiles) {
if (docTypeId) { if (docTypeId) {
await $patchapi('file', { await $patchapi("file", {
id: fileRecord.id, id: fileRecord.id,
doc_type: docTypeId doc_type: docTypeId,
}); });
} }
const detail = await $getdata('reservation', { const detail = await $getdata(
id: this.transaction.txncurrent__detail, "reservation",
}, undefined, true) {
id: this.transaction.txncurrent__detail,
},
undefined,
true,
);
const payload = { const payload = {
txn_detail: detail.id, txn_detail: detail.id,
@@ -305,7 +438,7 @@ export default {
} }
await this.fetchFiles(); await this.fetchFiles();
this.$emit('upload-completed'); this.$emit("upload-completed");
} catch (error) { } catch (error) {
console.error("Lỗi khi lưu file:", error); console.error("Lỗi khi lưu file:", error);
alert("Đã xảy ra lỗi khi tải file lên. Vui lòng thử lại."); alert("Đã xảy ra lỗi khi tải file lên. Vui lòng thử lại.");
@@ -318,21 +451,21 @@ export default {
const filePath = file.file__file || file.file; const filePath = file.file__file || file.file;
if (!filePath) return; if (!filePath) return;
const link = document.createElement('a'); const link = document.createElement("a");
link.href = `${$getpath()}static/files/${filePath}`; link.href = `${$getpath()}static/files/${filePath}`;
link.download = file.file__name || 'download'; link.download = file.file__name || "download";
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
}, },
deleteFile(fileId) { deleteFile(fileId) {
this.showmodal = { this.showmodal = {
component: 'dialog/Confirm', component: "dialog/Confirm",
title: 'Xác nhận xóa', title: "Xác nhận xóa",
height: '10vh', height: "10vh",
width: '40%', width: "40%",
vbind: { vbind: {
content: 'Bạn có chắc chắn muốn xóa file này không?' content: "Bạn có chắc chắn muốn xóa file này không?",
}, },
onConfirm: async () => { onConfirm: async () => {
this.isLoading = true; this.isLoading = true;
@@ -350,17 +483,17 @@ export default {
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
} },
}; };
}, },
handleModalEvent(event) { handleModalEvent(event) {
if (event.name === 'confirm' && typeof this.showmodal?.onConfirm === 'function') { if (event.name === "confirm" && typeof this.showmodal?.onConfirm === "function") {
this.showmodal.onConfirm(); this.showmodal.onConfirm();
} }
}, },
viewFile(file) { viewFile(file) {
const { $getpath } = useNuxtApp(); const { $getpath } = useNuxtApp();
const fileName = file.file__name || ''; const fileName = file.file__name || "";
const filePath = file.file__file || file.file; const filePath = file.file__file || file.file;
if (!filePath) return; if (!filePath) return;
@@ -371,23 +504,23 @@ export default {
if (isImageFile) { if (isImageFile) {
this.showmodal = { this.showmodal = {
title: fileName, title: fileName,
component: 'media/ChipImage', component: "media/ChipImage",
vbind: { vbind: {
extend: false, extend: false,
file: file, file: file,
image: fileUrl, image: fileUrl,
show: ['download', 'delete'] show: ["download", "delete"],
} },
}; };
} else if (isViewable) { } else if (isViewable) {
// Mở Google Viewer trực tiếp trong tab mới // Mở Google Viewer trực tiếp trong tab mới
const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`; const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`;
window.open(viewerUrl, '_blank'); window.open(viewerUrl, "_blank");
} else { } else {
this.downloadFile(file); this.downloadFile(file);
} }
}, },
} },
}; };
</script> </script>
@@ -420,4 +553,4 @@ export default {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
</style> </style>

View File

@@ -1,4 +1,4 @@
<!-- Viewer: display when click tem from another dealer --> <!-- Viewer: display when click tem from another dealer -->
<template> <template>
<p>Rất tiếc, bạn hiện chưa quyền xem thông tin sản phẩm này.</p> <p>Rất tiếc, bạn hiện chưa quyền xem thông tin sản phẩm này.</p>
</template> </template>

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,15 @@
<template> <template>
<div v-if="productData" class="grid px-3"> <div
v-if="productData"
class="grid px-3"
>
<div class="cell is-col-span-12"> <div class="cell is-col-span-12">
<div id="schedule-content"> <div id="schedule-content">
<div v-if="selectedPolicy" id="print-area" :class="{ 'is-loading': isLoading }"> <div
v-if="selectedPolicy"
id="print-area"
:class="{ 'is-loading': isLoading }"
>
<!-- Header --> <!-- Header -->
<div class="is-flex is-justify-content-space-between is-align-items-center"> <div class="is-flex is-justify-content-space-between is-align-items-center">
<h3 class="title is-4 has-text-primary mb-1"> <h3 class="title is-4 has-text-primary mb-1">
@@ -12,101 +19,186 @@
<span class="button is-white"> <span class="button is-white">
<span class="has-text-weight-semibold">Đơn vị: VNĐ</span> <span class="has-text-weight-semibold">Đơn vị: VNĐ</span>
</span> </span>
<button class="button is-light" @click="$emit('print')" id="ignore-print"> <button
class="button is-light"
@click="$emit('print')"
id="ignore-print"
>
<span class="is-size-6">In</span> <span class="is-size-6">In</span>
</button> </button>
</div> </div>
</div> </div>
<hr class="my-4" style="background-color: var(--bulma-background)"></hr> <hr
class="my-4"
style="background-color: var(--bulma-background)"
/>
<!-- Summary Information --> <!-- Summary Information -->
<div class="fixed-grid has-4-cols-mobile has-7-cols-desktop"> <div class="fixed-grid has-4-cols-mobile has-7-cols-desktop">
<div class="grid"> <div class="grid">
<div class="cell is-col-span-6-mobile is-col-span-1-desktop"> <div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Sản phẩm</p> <p class="is-size-6 has-text-weight-bold mb-1">Sản phẩm</p>
<p class="has-text-primary has-text-weight-medium">{{ productData.trade_code || productData.code }} <a <p class="has-text-primary has-text-weight-medium">
class="ml-4" id="ignore" @click="$copyToClipboard(productData.trade_code)"> {{ productData.trade_code || productData.code }}
<SvgIcon name="copy.svg" type="primary" :size="18" /> <a
class="ml-4"
id="ignore"
@click="$copyToClipboard(productData.trade_code)"
>
<SvgIcon
name="copy.svg"
type="primary"
:size="18"
/>
</a> </a>
</p> </p>
</div> </div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop"> <div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Giá niêm yết</p> <p class="is-size-6 has-text-weight-bold mb-1">Giá niêm yết</p>
<p class="has-text-primary">{{ $numtoString(calculatorData.originPrice) }}</p> <p class="has-text-primary">
{{ $numtoString(calculatorData.originPrice) }}
</p>
</div> </div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop"> <div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Tổng chiết khấu</p> <p class="is-size-6 has-text-weight-bold mb-1">Tổng chiết khấu</p>
<p class="has-text-danger has-text-weight-bold">{{ $numtoString(calculatorData.totalDiscount) }}</p> <p class="has-text-danger has-text-weight-bold">
{{ $numtoString(calculatorData.totalDiscount) }}
</p>
</div> </div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop"> <div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Giá sau chiết khấu</p> <p class="is-size-6 has-text-weight-bold mb-1">Giá sau chiết khấu</p>
<p class="has-text-black has-text-weight-bold">{{ $numtoString(calculatorData.salePrice) }}</p> <p class="has-text-black has-text-weight-bold">
{{ $numtoString(calculatorData.salePrice) }}
</p>
</div> </div>
<div v-if="selectedPolicy.contract_allocation_percentage < 100" <div
class="cell is-col-span-6-mobile is-col-span-1-desktop"> v-if="selectedPolicy.contract_allocation_percentage < 100"
class="cell is-col-span-6-mobile is-col-span-1-desktop"
>
<p class="is-size-6 has-text-weight-bold mb-1">Giá trị bảo đảm</p> <p class="is-size-6 has-text-weight-bold mb-1">Giá trị bảo đảm</p>
<p class="has-text-primary">{{ $numtoString(calculatorData.allocatedPrice) }}</p> <p class="has-text-primary">
{{ $numtoString(calculatorData.allocatedPrice) }}
</p>
</div> </div>
<div v-if="totalPaid === 0" class="cell is-col-span-6-mobile is-col-span-1-desktop"> <div
v-if="totalPaid === 0"
class="cell is-col-span-6-mobile is-col-span-1-desktop"
>
<p class="is-size-6 has-text-weight-bold mb-1">Đặt cọc</p> <p class="is-size-6 has-text-weight-bold mb-1">Đặt cọc</p>
<p class="has-text-primary">{{ $numtoString(selectedPolicy.deposit) }}</p> <p class="has-text-primary">
{{ $numtoString(selectedPolicy.deposit) }}
</p>
</div> </div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop"> <div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Khách hàng</p> <p class="is-size-6 has-text-weight-bold mb-1">Khách hàng</p>
<p v-if="selectedCustomer" class="has-text-primary has-text-weight-medium"> <p
v-if="selectedCustomer"
class="has-text-primary has-text-weight-medium"
>
{{ selectedCustomer.code }} - {{ selectedCustomer.fullname }} {{ selectedCustomer.code }} - {{ selectedCustomer.fullname }}
</p> </p>
<p v-else class="has-text-grey is-italic is-size-6">Chưa chọn</p> <p
v-else
class="has-text-grey is-italic is-size-6"
>
Chưa chọn
</p>
</div> </div>
</div> </div>
</div> </div>
<hr class="my-4" style="background-color: var(--bulma-background)"></hr> <hr
class="my-4"
style="background-color: var(--bulma-background)"
/>
<!-- Detailed Discounts --> <!-- Detailed Discounts -->
<div v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0" class="mt-4 mb-4"> <div
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined"> v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0"
CHI TIẾT CHIẾT KHẤU: class="mt-4 mb-4"
</p> >
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">CHI TIẾT CHIẾT KHẤU:</p>
<table class="table is-fullwidth is-hoverable is-size-6"> <table class="table is-fullwidth is-hoverable is-size-6">
<thead> <thead>
<tr> <tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;" <th
colspan="2">Diễn giải chiết khấu</th> class="has-background-primary has-text-white has-font-weight-normal"
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right" style="border: none"
style="border: none;" width="15%">Giá trị</th> colspan="2"
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right" >
style="border: none;" width="20%">Thành tiền</th> Diễn giải chiết khấu
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right" </th>
style="border: none;" width="20%">Còn lại</th> <th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
width="15%"
>
Giá trị
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
width="20%"
>
Thành tiền
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
width="20%"
>
Còn lại
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr style="border-bottom: 1px solid #f5f5f5;" class="has-text-grey-light"> <tr
<td colspan="4" class="has-text-right pt-1 pb-1">Giá gốc</td> style="border-bottom: 1px solid #f5f5f5"
<td class="has-text-right has-text-weight-bold pt-1 pb-1">{{ class="has-text-grey-light"
$numtoString(calculatorData.originPrice) }}</td> >
<td
colspan="4"
class="has-text-right pt-1 pb-1"
>
Giá gốc
</td>
<td class="has-text-right has-text-weight-bold pt-1 pb-1">
{{ $numtoString(calculatorData.originPrice) }}
</td>
</tr> </tr>
<tr v-for="(item, idx) in calculatorData.detailedDiscounts" :key="`discount-${idx}`" <tr
style="border-bottom: 1px solid #f5f5f5;"> v-for="(item, idx) in calculatorData.detailedDiscounts"
<td width="5%" class="has-text-centered">{{ idx + 1 }}</td> :key="`discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5"
>
<td
width="5%"
class="has-text-centered"
>
{{ idx + 1 }}
</td>
<td> <td>
<span class="has-text-weight-semibold">{{ item.name }}</span> <span class="has-text-weight-semibold">{{ item.name }}</span>
<span class="tag is-primary has-text-white is-rounded border ml-1">{{ item.code }}</span> <span class="tag is-primary has-text-white is-rounded border ml-1">{{ item.code }}</span>
</td> </td>
<td class="has-text-right">{{ item.customType === 1 ? item.customValue + '%' : <td class="has-text-right">
$numtoString(item.customValue) }}</td> {{ item.customType === 1 ? item.customValue + "%" : $numtoString(item.customValue) }}
</td>
<td class="has-text-right has-text-danger">-{{ $numtoString(item.amount) }}</td> <td class="has-text-right has-text-danger">-{{ $numtoString(item.amount) }}</td>
<td class="has-text-right has-text-primary">{{ $numtoString(item.remaining) }}</td> <td class="has-text-right has-text-primary">
{{ $numtoString(item.remaining) }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Early Payment Details --> <!-- Early Payment Details -->
<div v-if="isEarlyPaymentActive" class="mt-4 mb-4"> <div
v-if="isEarlyPaymentActive"
class="mt-4 mb-4"
>
<!-- Original Schedule --> <!-- Original Schedule -->
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined"> <p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
LỊCH THANH TOÁN GỐC (THEO CHÍNH SÁCH) LỊCH THANH TOÁN GỐC (THEO CHÍNH SÁCH)
@@ -115,26 +207,57 @@
<table class="table is-fullwidth is-hoverable is-size-6"> <table class="table is-fullwidth is-hoverable is-size-6">
<thead> <thead>
<tr> <tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt <th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Đợt
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Tỷ lệ
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Số tiền (VND)
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Ngày bắt đầu
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Ngày đến hạn
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Số ngày
</th> </th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Tỷ lệ</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số tiền (VND)</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
bắt đầu</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
đến hạn</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số ngày</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(plan, index) in calculatorData.originalPaymentSchedule" :key="`orig-plan-${index}`" <tr
style="border-bottom: 1px solid #f5f5f5;"> v-for="(plan, index) in calculatorData.originalPaymentSchedule"
:key="`orig-plan-${index}`"
style="border-bottom: 1px solid #f5f5f5"
>
<td class="has-text-weight-semibold">Đợt {{ plan.cycle }}</td> <td class="has-text-weight-semibold">Đợt {{ plan.cycle }}</td>
<td class="has-text-right">{{ plan.type === 1 ? `${plan.value}%` : '-' }}</td> <td class="has-text-right">
<td class="has-text-right">{{ $numtoString(plan.amount) }}</td> {{ plan.type === 1 ? `${plan.value}%` : "-" }}
</td>
<td class="has-text-right">
{{ $numtoString(plan.amount) }}
</td>
<td>{{ formatDate(plan.from_date) }}</td> <td>{{ formatDate(plan.from_date) }}</td>
<td>{{ formatDate(plan.to_date) }}</td> <td>{{ formatDate(plan.to_date) }}</td>
<td class="has-text-right">{{ plan.days }}</td> <td class="has-text-right">{{ plan.days }}</td>
@@ -151,29 +274,62 @@
<table class="table is-fullwidth is-hoverable is-size-6"> <table class="table is-fullwidth is-hoverable is-size-6">
<thead> <thead>
<tr> <tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt <th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Đợt
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Hạn TT Gốc
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Ngày TT Thực Tế
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Số tiền gốc
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Số ngày TT sớm
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Tỷ lệ CK (%/ngày)
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Tiền chiết khấu
</th> </th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Hạn
TT Gốc</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
TT Thực Tế</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số tiền gốc</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số ngày TT sớm</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Tỷ lệ CK (%/ngày)</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Tiền chiết khấu</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(item, idx) in calculatorData.earlyDiscountDetails" :key="`early-discount-${idx}`" <tr
style="border-bottom: 1px solid #f5f5f5;"> v-for="(item, idx) in calculatorData.earlyDiscountDetails"
:key="`early-discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5"
>
<td>Đợt {{ item.cycle }}</td> <td>Đợt {{ item.cycle }}</td>
<td>{{ formatDate(item.original_payment_date) }}</td> <td>{{ formatDate(item.original_payment_date) }}</td>
<td>{{ formatDate(item.actual_payment_date) }}</td> <td>{{ formatDate(item.actual_payment_date) }}</td>
<td class="has-text-right">{{ $numtoString(item.original_amount) }}</td> <td class="has-text-right">
{{ $numtoString(item.original_amount) }}
</td>
<td class="has-text-right">{{ item.early_days }}</td> <td class="has-text-right">{{ item.early_days }}</td>
<td class="has-text-right">{{ item.discount_rate }}</td> <td class="has-text-right">{{ item.discount_rate }}</td>
<td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td> <td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td>
@@ -181,9 +337,15 @@
</tbody> </tbody>
<tfoot> <tfoot>
<tr class="has-background-light"> <tr class="has-background-light">
<th colspan="6" class="has-text-right has-text-weight-bold">Tổng chiết khấu thanh toán sớm</th> <th
<th class="has-text-right has-text-weight-bold has-text-danger">-{{ colspan="6"
$numtoString(totalEarlyDiscount) }}</th> class="has-text-right has-text-weight-bold"
>
Tổng chiết khấu thanh toán sớm
</th>
<th class="has-text-right has-text-weight-bold has-text-danger">
-{{ $numtoString(totalEarlyDiscount) }}
</th>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
@@ -191,7 +353,10 @@
</div> </div>
<!-- Payment Schedule Table --> <!-- Payment Schedule Table -->
<div v-if="displaySchedule.length > 0" class="mt-4"> <div
v-if="displaySchedule.length > 0"
class="mt-4"
>
<div class="level m-0 mb-2 is-mobile"> <div class="level m-0 mb-2 is-mobile">
<div class="level-left"> <div class="level-left">
<p class="has-text-weight-bold is-size-5 has-text-primary is-underlined"> <p class="has-text-weight-bold is-size-5 has-text-primary is-underlined">
@@ -199,14 +364,23 @@
<span v-else>LỊCH THANH TOÁN</span> <span v-else>LỊCH THANH TOÁN</span>
</p> </p>
</div> </div>
<div class="level-right" id="ignore-print"> <div
class="level-right"
id="ignore-print"
>
<div class="buttons are-small has-addons"> <div class="buttons are-small has-addons">
<button class="button" @click="viewMode = 'table'" <button
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'"> class="button"
@click="viewMode = 'table'"
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'"
>
<span class="is-size-6">Bảng</span> <span class="is-size-6">Bảng</span>
</button> </button>
<button class="button" @click="viewMode = 'list'" <button
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'"> class="button"
@click="viewMode = 'list'"
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'"
>
<span class="is-size-6">Thẻ</span> <span class="is-size-6">Thẻ</span>
</button> </button>
</div> </div>
@@ -214,64 +388,136 @@
</div> </div>
<!-- Table View --> <!-- Table View -->
<div v-if="viewMode === 'table'" class="table-container schedule-container"> <div
v-if="viewMode === 'table'"
class="table-container schedule-container"
>
<table class="table is-fullwidth is-hoverable is-size-6"> <table class="table is-fullwidth is-hoverable is-size-6">
<thead> <thead>
<tr> <tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt <th
thanh toán</th> class="has-background-primary has-text-white has-font-weight-normal"
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right" style="border: none"
style="border: none;">Số tiền (VND)</th> >
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right" Đợt thanh toán
style="border: none;">Đã thanh toán</th> </th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right" <th
style="border: none;">Còn phải TT</th> class="has-background-primary has-text-white has-font-weight-normal has-text-right"
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày style="border: none"
bắt đầu</th> >
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày Số tiền (VND)
đến hạn</th> </th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Trạng <th
thái</th> class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Đã thanh toán
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none"
>
Còn phải TT
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Ngày bắt đầu
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Ngày đến hạn
</th>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Trạng thái
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(plan, index) in displaySchedule" :key="`plan-${index}`" <tr
style="border-bottom: 1px solid #f5f5f5;" v-for="(plan, index) in displaySchedule"
:class="plan.is_merged ? 'has-background-warning-light' : ''"> :key="`plan-${index}`"
<td class="has-text-weight-semibold" :class="plan.is_merged ? 'has-text-warning' : ''"> style="border-bottom: 1px solid #f5f5f5"
:class="plan.is_merged ? 'has-background-warning-light' : ''"
>
<td
class="has-text-weight-semibold"
:class="plan.is_merged ? 'has-text-warning' : ''"
>
Đợt {{ plan.cycle }} Đợt {{ plan.cycle }}
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span> <span
v-if="plan.is_merged"
class="tag is-warning is-light ml-1 is-size-7"
>GỘP SỚM</span
>
</td> </td>
<td class="has-text-right"> <td class="has-text-right">
<div v-if="plan.is_merged" class="has-text-right"> <div
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount) }} v-if="plan.is_merged"
class="has-text-right"
>
<p
class="has-text-grey"
title="Tổng các đợt gốc"
>
{{ $numtoString(totalOriginalEarlyAmount) }}
</p>
<p
class="has-text-danger"
title="Chiết khấu thanh toán sớm"
>
- {{ $numtoString(totalEarlyDiscount) }}
</p>
<hr
class="my-1"
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%"
/>
<p class="has-text-weight-bold">
{{ $numtoString(plan.amount) }}
</p> </p>
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{
$numtoString(totalEarlyDiscount) }}</p>
<hr class="my-1"
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%;">
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
</div> </div>
<span v-else>{{ $numtoString(plan.amount) }}</span> <span v-else>{{ $numtoString(plan.amount) }}</span>
</td> </td>
<td class="has-text-right has-text-success">{{ $numtoString(plan.paid_amount) }}</td> <td class="has-text-right has-text-success">
<td class="has-text-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</td> {{ $numtoString(plan.paid_amount) }}
</td>
<td class="has-text-right has-text-danger">
{{ $numtoString(plan.remain_amount) }}
</td>
<td>{{ formatDate(plan.from_date) }}</td> <td>{{ formatDate(plan.from_date) }}</td>
<td>{{ formatDate(plan.to_date) }}</td> <td>{{ formatDate(plan.to_date) }}</td>
<td> <td>
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span> <span
<span v-else class="tag is-warning">Chờ thanh toán</span> v-if="plan.status === 2"
class="tag is-success"
>Đã thanh toán</span
>
<span
v-else
class="tag is-warning"
>Chờ thanh toán</span
>
</td> </td>
</tr> </tr>
</tbody> </tbody>
<tfoot> <tfoot>
<tr class="has-background-light"> <tr class="has-background-light">
<th class="has-text-right has-text-weight-bold">Tổng cộng</th> <th class="has-text-right has-text-weight-bold">Tổng cộng</th>
<th class="has-text-right has-text-weight-bold">{{ $numtoString(totalAmount) }}</th> <th class="has-text-right has-text-weight-bold">
<th class="has-text-right has-text-weight-bold has-text-success">{{ $numtoString(totalPaid) }} {{ $numtoString(totalAmount) }}
</th>
<th class="has-text-right has-text-weight-bold has-text-success">
{{ $numtoString(totalPaid) }}
</th>
<th class="has-text-right has-text-weight-bold has-text-danger">
{{ $numtoString(calculatorData.totalRemaining) }}
</th> </th>
<th class="has-text-right has-text-weight-bold has-text-danger">{{
$numtoString(calculatorData.totalRemaining) }}</th>
<th colspan="3"></th> <th colspan="3"></th>
</tr> </tr>
</tfoot> </tfoot>
@@ -279,28 +525,57 @@
</div> </div>
<!-- List View (Card) --> <!-- List View (Card) -->
<div v-else-if="viewMode === 'list'" class="schedule-container"> <div
<div v-for="(plan, index) in displaySchedule" :key="`card-${index}`" class="card mb-4" v-else-if="viewMode === 'list'"
:class="plan.is_merged ? 'has-background-warning-light' : ''"> class="schedule-container"
>
<div
v-for="(plan, index) in displaySchedule"
:key="`card-${index}`"
class="card mb-4"
:class="plan.is_merged ? 'has-background-warning-light' : ''"
>
<div class="card-content"> <div class="card-content">
<div class="level is-mobile mb-5"> <div class="level is-mobile mb-5">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
<span class="tag is-primary" :class="plan.is_merged ? 'is-warning' : ''">Đợt {{ plan.cycle <span
}}</span> class="tag is-primary"
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span> :class="plan.is_merged ? 'is-warning' : ''"
>Đợt {{ plan.cycle }}</span
>
<span
v-if="plan.is_merged"
class="tag is-warning is-light ml-1 is-size-7"
>GỘP SỚM</span
>
</div> </div>
</div> </div>
<div class="level-right"> <div class="level-right">
<div class="level-item has-text-weight-bold"> <div class="level-item has-text-weight-bold">
<div v-if="plan.is_merged" class="has-text-right"> <div
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount) v-if="plan.is_merged"
}}</p> class="has-text-right"
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{ >
$numtoString(totalEarlyDiscount) }}</p> <p
<hr class="my-1" class="has-text-grey"
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto"> title="Tổng các đợt gốc"
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p> >
{{ $numtoString(totalOriginalEarlyAmount) }}
</p>
<p
class="has-text-danger"
title="Chiết khấu thanh toán sớm"
>
- {{ $numtoString(totalEarlyDiscount) }}
</p>
<hr
class="my-1"
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto"
/>
<p class="has-text-weight-bold">
{{ $numtoString(plan.amount) }}
</p>
</div> </div>
<span v-else>{{ $numtoString(plan.amount) }}</span> <span v-else>{{ $numtoString(plan.amount) }}</span>
</div> </div>
@@ -308,25 +583,41 @@
</div> </div>
<div class="level is-mobile mb-1"> <div class="level is-mobile mb-1">
<div class="level-left">Đã thanh toán:</div> <div class="level-left">Đã thanh toán:</div>
<div class="level-right has-text-success">{{ $numtoString(plan.paid_amount) }}</div> <div class="level-right has-text-success">
{{ $numtoString(plan.paid_amount) }}
</div>
</div> </div>
<div class="level is-mobile mb-1"> <div class="level is-mobile mb-1">
<div class="level-left">Còn phải TT:</div> <div class="level-left">Còn phải TT:</div>
<div class="level-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</div> <div class="level-right has-text-danger">
{{ $numtoString(plan.remain_amount) }}
</div>
</div> </div>
<div class="level is-mobile mb-1"> <div class="level is-mobile mb-1">
<div class="level-left">Từ ngày:</div> <div class="level-left">Từ ngày:</div>
<div class="level-right">{{ formatDate(plan.from_date) }}</div> <div class="level-right">
{{ formatDate(plan.from_date) }}
</div>
</div> </div>
<div class="level is-mobile mb-1"> <div class="level is-mobile mb-1">
<div class="level-left">Đến hạn:</div> <div class="level-left">Đến hạn:</div>
<div class="level-right">{{ formatDate(plan.to_date) }}</div> <div class="level-right">
{{ formatDate(plan.to_date) }}
</div>
</div> </div>
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left">Trạng thái:</div> <div class="level-left">Trạng thái:</div>
<div class="level-right"> <div class="level-right">
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span> <span
<span v-else class="tag is-warning">Chờ thanh toán</span> v-if="plan.status === 2"
class="tag is-success"
>Đã thanh toán</span
>
<span
v-else
class="tag is-warning"
>Chờ thanh toán</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -335,7 +626,10 @@
</div> </div>
<!-- Summary Footer --> <!-- Summary Footer -->
<div class="" style="border-top: 1px solid #eee;"> <div
class=""
style="border-top: 1px solid #eee"
>
<div class="level is-mobile is-size-6 my-4"> <div class="level is-mobile is-size-6 my-4">
<div class="level-right"> <div class="level-right">
<div class="level-item"> <div class="level-item">
@@ -354,22 +648,22 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from "vue";
import dayjs from 'dayjs'; import dayjs from "dayjs";
// Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN // Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN
const props = defineProps({ const props = defineProps({
productData: { productData: {
type: Object, type: Object,
default: null default: null,
}, },
selectedPolicy: { selectedPolicy: {
type: Object, type: Object,
default: null default: null,
}, },
selectedCustomer: { selectedCustomer: {
type: Object, type: Object,
default: null default: null,
}, },
calculatorData: { calculatorData: {
type: Object, type: Object,
@@ -390,15 +684,15 @@ const props = defineProps({
}, },
isLoading: { isLoading: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}); });
// Emits // Emits
const emit = defineEmits(['print']); const emit = defineEmits(["print"]);
// Local state // Local state
const viewMode = ref('table'); const viewMode = ref("table");
// Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN // Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN
const displaySchedule = computed(() => { const displaySchedule = computed(() => {
@@ -426,8 +720,8 @@ const totalPaid = computed(() => {
}); });
const formatDate = (date) => { const formatDate = (date) => {
if (!date) return '-'; if (!date) return "-";
return dayjs(date).format('DD/MM/YYYY'); return dayjs(date).format("DD/MM/YYYY");
}; };
</script> </script>
@@ -501,4 +795,4 @@ th,
page-break-inside: avoid !important; page-break-inside: avoid !important;
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,3 @@
<script lang="ts" setup> <script lang="ts" setup></script>
</script> <template>Cash Book</template>
<template>
Cash Book
</template>

View File

@@ -25,12 +25,23 @@
</div> </div>
<template v-if="option === 'your'"> <template v-if="option === 'your'">
<template v-if="tab === 'message'"> <template v-if="tab === 'message'">
<div class="field is-grouped" v-for="(v, i) in message"> <div
class="field is-grouped"
v-for="(v, i) in message"
>
<div class="control is-expanded"> <div class="control is-expanded">
<textarea class="textarea" placeholder="" rows="3" v-model="v.text"></textarea> <textarea
class="textarea"
placeholder=""
rows="3"
v-model="v.text"
></textarea>
</div> </div>
<div class="control"> <div class="control">
<a class="mr-3" @click="add()"> <a
class="mr-3"
@click="add()"
>
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 20 }"></SvgIcon>
</a> </a>
<a @click="remove(v, i)"> <a @click="remove(v, i)">
@@ -44,22 +55,40 @@
</div> </div>
</div> </div>
<div class="mt-5"> <div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button> <button
class="button is-primary has-text-white"
@click="update()"
>
Cập nhật
</button>
</div> </div>
</template> </template>
<template v-else-if="tab === 'image'"> <template v-else-if="tab === 'image'">
<div class="field is-grouped mb-0"> <div class="field is-grouped mb-0">
<div class="control is-expanded"></div> <div class="control is-expanded"></div>
<div class="control"> <div class="control">
<FileUpload v-bind="{ position: 'left' }" @files="getImages"></FileUpload> <FileUpload
v-bind="{ position: 'left' }"
@files="getImages"
></FileUpload>
</div> </div>
</div> </div>
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0"> <div
<div class="control mb-2" v-for="(v, i) in image"> class="field is-grouped is-grouped-multiline"
v-if="image.length > 0"
>
<div
class="control mb-2"
v-for="(v, i) in image"
>
<ChipImage <ChipImage
style="width: 128px" style="width: 128px"
@remove="removeImage(v, i)" @remove="removeImage(v, i)"
v-bind="{ show: ['copy', 'download', 'delete'], file: v, image: `${$getpath()}static/files/${v.file}` }" v-bind="{
show: ['copy', 'download', 'delete'],
file: v,
image: `${$getpath()}static/files/${v.file}`,
}"
> >
</ChipImage> </ChipImage>
</div> </div>
@@ -69,24 +98,47 @@
<div class="field is-grouped mb-0"> <div class="field is-grouped mb-0">
<div class="control is-expanded"></div> <div class="control is-expanded"></div>
<div class="control"> <div class="control">
<FileUpload v-bind="{ position: 'left', type: 'file' }" @files="getFiles"></FileUpload> <FileUpload
v-bind="{ position: 'left', type: 'file' }"
@files="getFiles"
></FileUpload>
</div> </div>
</div> </div>
<FileShow @remove="removeFile" v-bind="{ files: file, show: { delete: 1 } }"></FileShow> <FileShow
@remove="removeFile"
v-bind="{ files: file, show: { delete: 1 } }"
></FileShow>
</template> </template>
<template v-else-if="tab === 'link'"> <template v-else-if="tab === 'link'">
<div class="field is-grouped" v-for="(v, i) in link"> <div
class="field is-grouped"
v-for="(v, i) in link"
>
<div class="control is-expanded"> <div class="control is-expanded">
<input class="input" placeholder="" v-model="v.link" /> <input
class="input"
placeholder=""
v-model="v.link"
/>
</div> </div>
<div class="control"> <div class="control">
<a class="mr-3" @click="copyContent(v.link)"> <a
class="mr-3"
@click="copyContent(v.link)"
>
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
</a> </a>
<a class="mr-3" :href="v.link" target="_blank"> <a
class="mr-3"
:href="v.link"
target="_blank"
>
<SvgIcon v-bind="{ name: 'open.svg', type: 'primary', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'open.svg', type: 'primary', size: 20 }"></SvgIcon>
</a> </a>
<a class="mr-3" @click="addLink()"> <a
class="mr-3"
@click="addLink()"
>
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 18 }"></SvgIcon> <SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 18 }"></SvgIcon>
</a> </a>
<a @click="removeLink(v, i)"> <a @click="removeLink(v, i)">
@@ -95,14 +147,23 @@
</div> </div>
</div> </div>
<div class="mt-5"> <div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button> <button
class="button is-primary has-text-white"
@click="update()"
>
Cập nhật
</button>
</div> </div>
</template> </template>
</template> </template>
<template v-else-if="option === 'system'"> <template v-else-if="option === 'system'">
<template v-if="tab === 'message'"> <template v-if="tab === 'message'">
<div v-if="message"> <div v-if="message">
<div class="px-2 py-2 mb-2" style="border: 1px solid #e8e8e8" v-for="(v, i) in message"> <div
class="px-2 py-2 mb-2"
style="border: 1px solid #e8e8e8"
v-for="(v, i) in message"
>
<span class="mr-3">{{ v.text }}</span> <span class="mr-3">{{ v.text }}</span>
<a @click="copyContent(v.text)"> <a @click="copyContent(v.text)">
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
@@ -111,11 +172,21 @@
</div> </div>
</template> </template>
<template v-else-if="tab === 'image'"> <template v-else-if="tab === 'image'">
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0"> <div
<div class="control mb-2" v-for="(v, i) in image"> class="field is-grouped is-grouped-multiline"
v-if="image.length > 0"
>
<div
class="control mb-2"
v-for="(v, i) in image"
>
<ChipImage <ChipImage
style="width: 128px" style="width: 128px"
v-bind="{ show: ['copy', 'download'], file: v, image: `${$getpath()}static/files/${v.file}` }" v-bind="{
show: ['copy', 'download'],
file: v,
image: `${$getpath()}static/files/${v.file}`,
}"
> >
</ChipImage> </ChipImage>
</div> </div>
@@ -125,8 +196,17 @@
<FileShow v-bind="{ files: file }"></FileShow> <FileShow v-bind="{ files: file }"></FileShow>
</template> </template>
<template v-else-if="tab === 'link'"> <template v-else-if="tab === 'link'">
<div class="px-2 py-2 mb-2" style="border: 1px solid #e8e8e8" v-for="(v, i) in link"> <div
<a :href="v.link" target="_blank" class="mr-3">{{ v.link }}</a> class="px-2 py-2 mb-2"
style="border: 1px solid #e8e8e8"
v-for="(v, i) in link"
>
<a
:href="v.link"
target="_blank"
class="mr-3"
>{{ v.link }}</a
>
<a @click="copyContent(v.link)"> <a @click="copyContent(v.link)">
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
</a> </a>

View File

@@ -3,20 +3,26 @@ const props = defineProps({
text: String, text: String,
image: String, image: String,
type: String, type: String,
size: Number size: Number,
}); });
</script> </script>
<template> <template>
<div <div
@click="$emit('justclick')" @click="$emit('justclick')"
class="rounded-full mx-0 px-0 size-10 font-bold is-flex is-justify-content-center is-align-items-center" class="rounded-full mx-0 px-0 size-10 font-bold is-flex is-justify-content-center is-align-items-center"
:style="{ :style="{
border: image ? 'none' : '1px solid var(--bulma-grey-70)' border: image ? 'none' : '1px solid var(--bulma-grey-70)',
}" }"
> >
<figure v-if="image" class="image"> <figure
<img class="is-rounded" :src="`${$path()}download?name=${image}`"> v-if="image"
class="image"
>
<img
class="is-rounded"
:src="`${$path()}download?name=${image}`"
/>
</figure> </figure>
<span v-else>{{text}}</span> <span v-else>{{ text }}</span>
</div> </div>
</template> </template>

View File

@@ -1,41 +1,42 @@
<template> <template>
<div @click="handleClick()" class="is-clickable"> <div
@click="handleClick()"
class="is-clickable"
>
<template v-if="count > 0"> <template v-if="count > 0">
<span class="dot-primary"> <span class="dot-primary">
{{ count }} {{ count }}
</span> </span>
</template> </template>
<template v-else> <template v-else>
<span class="dot-primary"> <span class="dot-primary"> + </span>
+
</span>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['row', 'countField', 'modalConfig'], props: ["row", "countField", "modalConfig"],
computed: { computed: {
count() { count() {
return this.row[this.countField] || 0; return this.row[this.countField] || 0;
} },
}, },
methods: { methods: {
handleClick() { handleClick() {
if (!this.modalConfig) return; if (!this.modalConfig) return;
let config = this.$copy(this.modalConfig); let config = this.$copy(this.modalConfig);
this.$emit('open', { this.$emit("open", {
name: 'dataevent', name: "dataevent",
data: { data: {
modal: config modal: config,
} },
}); });
} },
} },
} };
</script> </script>

View File

@@ -1,135 +1,141 @@
<!-- CountdownTimer.vue --> <!-- CountdownTimer.vue -->
<template> <template>
<div class="countdown-wrapper"> <div class="countdown-wrapper">
<span v-if="isExpired" class="tag is-danger"> <span
{{ isVietnamese ? 'Hết giờ' : 'Expired' }} v-if="isExpired"
class="tag is-danger"
>
{{ isVietnamese ? "Hết giờ" : "Expired" }}
</span> </span>
<span v-else class="tag" :class="tagClass"> <span
v-else
class="tag"
:class="tagClass"
>
<span class="countdown-text">{{ formattedTime }}</span> <span class="countdown-text">{{ formattedTime }}</span>
</span> </span>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue' import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { useStore } from '@/stores/index' import { useStore } from "@/stores/index";
const props = defineProps({ const props = defineProps({
dateValue: { dateValue: {
type: [String, Date], type: [String, Date],
required: true required: true,
}, },
format: { format: {
type: String, type: String,
default: 'HH:mm:ss' default: "HH:mm:ss",
} },
}) });
const store = useStore() const store = useStore();
const { $dayjs } = useNuxtApp() const { $dayjs } = useNuxtApp();
const timeRemaining = ref({ const timeRemaining = ref({
days: 0, days: 0,
hours: 0, hours: 0,
minutes: 0, minutes: 0,
seconds: 0 seconds: 0,
}) });
const isExpired = ref(false) const isExpired = ref(false);
let intervalId = null let intervalId = null;
const isVietnamese = computed(() => store.lang === 'vi') const isVietnamese = computed(() => store.lang === "vi");
const tagClass = computed(() => { const tagClass = computed(() => {
const totalSeconds = timeRemaining.value.days * 86400 + const totalSeconds =
timeRemaining.value.hours * 3600 + timeRemaining.value.days * 86400 +
timeRemaining.value.minutes * 60 + timeRemaining.value.hours * 3600 +
timeRemaining.value.seconds timeRemaining.value.minutes * 60 +
timeRemaining.value.seconds;
if (totalSeconds <= 0) return 'is-danger' if (totalSeconds <= 0) return "is-danger";
if (totalSeconds <= 3600) return 'is-warning' // <= 1 hour if (totalSeconds <= 3600) return "is-warning"; // <= 1 hour
if (totalSeconds <= 86400) return 'is-info' // <= 1 day if (totalSeconds <= 86400) return "is-info"; // <= 1 day
return 'is-primary' // > 1 day return "is-primary"; // > 1 day
}) });
const formattedTime = computed(() => { const formattedTime = computed(() => {
const { days, hours, minutes, seconds } = timeRemaining.value const { days, hours, minutes, seconds } = timeRemaining.value;
if (days > 0) { if (days > 0) {
return isVietnamese return isVietnamese ? `${days}d ${hours}h ${minutes}m` : `${days}d ${hours}h ${minutes}m`;
? `${days}d ${hours}h ${minutes}m`
: `${days}d ${hours}h ${minutes}m`
}
if (hours > 0) {
return isVietnamese
? `${hours}h ${minutes}m ${seconds}s`
: `${hours}h ${minutes}m ${seconds}s`
} }
return isVietnamese if (hours > 0) {
? `${minutes}m ${seconds}s` return isVietnamese ? `${hours}h ${minutes}m ${seconds}s` : `${hours}h ${minutes}m ${seconds}s`;
: `${minutes}m ${seconds}s` }
})
return isVietnamese ? `${minutes}m ${seconds}s` : `${minutes}m ${seconds}s`;
});
const calculateTimeRemaining = () => { const calculateTimeRemaining = () => {
try { try {
const targetDate = $dayjs(props.dateValue) const targetDate = $dayjs(props.dateValue);
const now = $dayjs() const now = $dayjs();
if (now.isAfter(targetDate)) { if (now.isAfter(targetDate)) {
isExpired.value = true isExpired.value = true;
timeRemaining.value = { days: 0, hours: 0, minutes: 0, seconds: 0 } timeRemaining.value = { days: 0, hours: 0, minutes: 0, seconds: 0 };
return return;
} }
isExpired.value = false isExpired.value = false;
const diff = targetDate.diff(now, 'second') const diff = targetDate.diff(now, "second");
const days = Math.floor(diff / 86400) const days = Math.floor(diff / 86400);
const hours = Math.floor((diff % 86400) / 3600) const hours = Math.floor((diff % 86400) / 3600);
const minutes = Math.floor((diff % 3600) / 60) const minutes = Math.floor((diff % 3600) / 60);
const seconds = diff % 60 const seconds = diff % 60;
timeRemaining.value = { days, hours, minutes, seconds } timeRemaining.value = { days, hours, minutes, seconds };
} catch (error) { } catch (error) {
console.error('Error calculating countdown:', error) console.error("Error calculating countdown:", error);
isExpired.value = true isExpired.value = true;
} }
} };
const startCountdown = () => { const startCountdown = () => {
calculateTimeRemaining() calculateTimeRemaining();
if (intervalId) clearInterval(intervalId) if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => { intervalId = setInterval(() => {
calculateTimeRemaining() calculateTimeRemaining();
if (isExpired.value && intervalId) { if (isExpired.value && intervalId) {
clearInterval(intervalId) clearInterval(intervalId);
intervalId = null intervalId = null;
} }
}, 1000) }, 1000);
} };
watch(() => props.dateValue, () => { watch(
startCountdown() () => props.dateValue,
}, { deep: true }) () => {
startCountdown();
},
{ deep: true },
);
onMounted(() => { onMounted(() => {
startCountdown() startCountdown();
}) });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (intervalId) { if (intervalId) {
clearInterval(intervalId) clearInterval(intervalId);
} }
}) });
</script> </script>
<style scoped> <style scoped>
.countdown-wrapper { .countdown-wrapper {
display: inline-block; display: inline-block;
} }
</style> </style>

View File

@@ -37,7 +37,7 @@ const toolbarOptions = [
[{ header: [1, 2, 3, 4, 5, 6, false] }], [{ header: [1, 2, 3, 4, 5, 6, false] }],
// ✍️ Định dạng cơ bản // ✍️ Định dạng cơ bản
['bold', 'italic', 'underline', 'strike'], ["bold", "italic", "underline", "strike"],
// 🎨 Màu chữ & nền // 🎨 Màu chữ & nền
[{ color: [] }, { background: [] }], [{ color: [] }, { background: [] }],
@@ -46,14 +46,13 @@ const toolbarOptions = [
[{ align: [] }], [{ align: [] }],
// 📋 Danh sách // 📋 Danh sách
[{ list: 'ordered' }, { list: 'bullet' }], [{ list: "ordered" }, { list: "bullet" }],
// 🔗 Media // 🔗 Media
['link', 'image', 'video'], ["link", "image", "video"],
['clean'], // Xóa định dạng
]
["clean"], // Xóa định dạng
];
var content = props.text; var content = props.text;

View File

@@ -1,37 +1,43 @@
<template> <template>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input :class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-black' : ''}`" type="text" <input
:placeholder="placeholder || ''" v-model="value" @keyup="doCheck" :disabled="disabled || false"> :class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-black' : ''}`"
<span class="icon is-left"> type="text"
<SvgIcon v-bind="{name: 'email.svg', type: 'gray', size: 21}"></SvgIcon> :placeholder="placeholder || ''"
</span> v-model="value"
</div> @keyup="doCheck"
:disabled="disabled || false"
/>
<span class="icon is-left">
<SvgIcon v-bind="{ name: 'email.svg', type: 'gray', size: 21 }"></SvgIcon>
</span>
</div>
</template> </template>
<script> <script>
export default { export default {
props: ['record', 'attr', 'placeholder', 'disabled'], props: ["record", "attr", "placeholder", "disabled"],
data() { data() {
return { return {
value: this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined, value: this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined,
error: undefined error: undefined,
} };
}, },
watch: { watch: {
record: function(newVal) { record: function (newVal) {
this.value = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined this.value = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
} },
}, },
methods: { methods: {
doCheck() { doCheck() {
if(this.$empty(this.value)) { if (this.$empty(this.value)) {
this.value = undefined this.value = undefined;
this.error = false this.error = false;
return this.$emit('email', null) return this.$emit("email", null);
} }
let check = this.$errEmail(this.value) let check = this.$errEmail(this.value);
this.error = check? true : false this.error = check ? true : false;
this.$emit('email', this.value) this.$emit("email", this.value);
} },
} },
} };
</script> </script>

View File

@@ -10,7 +10,7 @@
autocomplete="tel" autocomplete="tel"
/> />
<span class="icon is-left"> <span class="icon is-left">
<SvgIcon v-bind="{name: 'phone.png', type: 'gray', size: 20}"></SvgIcon> <SvgIcon v-bind="{ name: 'phone.png', type: 'gray', size: 20 }"></SvgIcon>
</span> </span>
</div> </div>
</template> </template>

View File

@@ -1,27 +1,36 @@
<template> <template>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<textarea v-model="record.note" class="textarea" name="note" placeholder="" rows="8"></textarea> <textarea
v-model="record.note"
class="textarea"
name="note"
placeholder=""
rows="8"
></textarea>
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<button class="button is-primary has-text-white" @click="save()"> <button
{{ $store.lang==='vi'? 'Lưu lại' : 'Save' }} class="button is-primary has-text-white"
@click="save()"
>
{{ $store.lang === "vi" ? "Lưu lại" : "Save" }}
</button> </button>
</div> </div>
</template> </template>
<script setup> <script setup>
const emit = defineEmits(["close"]) const emit = defineEmits(["close"]);
const { $store, $getdata, $updateapi, $updatepage } = useNuxtApp(); const { $store, $getdata, $updateapi, $updatepage } = useNuxtApp();
const props = defineProps({ const props = defineProps({
row: Object, row: Object,
pagename: String pagename: String,
}) });
var record = await $getdata('application', {id: props.row.id}, undefined, true) var record = await $getdata("application", { id: props.row.id }, undefined, true);
async function save() { async function save() {
await $updateapi('application', record) await $updateapi("application", record);
record = await $getdata('application', {id: props.row.id}, undefined, true) record = await $getdata("application", { id: props.row.id }, undefined, true);
$updatepage(props.pagename, record) $updatepage(props.pagename, record);
emit('close') emit("close");
} }
</script> </script>

View File

@@ -1,19 +1,25 @@
<template> <template>
<div> <div>
<template v-if="data"> <template v-if="data">
<article class="message is-findata" v-if="data.length===0"> <article
<div class="message-body py-2 fs-16"> class="message is-findata"
Chưa <b>ghi chú</b> nào được lưu v-if="data.length === 0"
</div> >
<div class="message-body py-2 fs-16">Chưa <b>ghi chú</b> nào được lưu</div>
</article> </article>
<template v-else> <template v-else>
<article class="media mt-0 mb-0" v-for="(v,i) in data"> <article
class="media mt-0 mb-0"
v-for="(v, i) in data"
>
<figure class="media-left"> <figure class="media-left">
<Avatarbox v-bind="{ <Avatarbox
text: v.user__fullname[0].toUpperCase(), v-bind="{
size: 'two', text: v.user__fullname[0].toUpperCase(),
type: 'primary' size: 'two',
}" /> type: 'primary',
}"
/>
</figure> </figure>
<div class="media-content"> <div class="media-content">
<div> <div>
@@ -22,14 +28,20 @@
</p> </p>
<p class="mt-1 fs-14 has-text-grey"> <p class="mt-1 fs-14 has-text-grey">
<span class="icon-text"> <span class="icon-text">
<span>{{v.user__fullname}}</span> <span>{{ v.user__fullname }}</span>
<span class="ml-3">{{ $dayjs(v['create_time']).fromNow(true) }}</span> <span class="ml-3">{{ $dayjs(v["create_time"]).fromNow(true) }}</span>
<template v-if="login.id===v.user"> <template v-if="login.id === v.user">
<a class="ml-3" @click="edit(v)"> <a
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 20}"></SvgIcon> class="ml-3"
@click="edit(v)"
>
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'gray', size: 20 }"></SvgIcon>
</a> </a>
<a class="ml-3" @click="askConfirm(v, i)"> <a
<SvgIcon v-bind="{name: 'bin.svg', type: 'gray', size: 20}"></SvgIcon> class="ml-3"
@click="askConfirm(v, i)"
>
<SvgIcon v-bind="{ name: 'bin.svg', type: 'gray', size: 20 }"></SvgIcon>
</a> </a>
</template> </template>
</span> </span>
@@ -39,87 +51,126 @@
</article> </article>
</template> </template>
</template> </template>
<div v-if="$getEditRights()" class="field is-grouped mt-3"> <div
v-if="$getEditRights()"
class="field is-grouped mt-3"
>
<div class="control is-expanded"> <div class="control is-expanded">
<textarea class="textarea" rows="2" placeholder="Viết ghi chú tại đây" v-model="detail"></textarea> <textarea
class="textarea"
rows="2"
placeholder="Viết ghi chú tại đây"
v-model="detail"
></textarea>
</div> </div>
<div class="control"> <div class="control">
<button class="button is-primary has-text-white" @click="save()">Lưu</button> <button
class="button is-primary has-text-white"
@click="save()"
>
Lưu
</button>
</div> </div>
</div> </div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" @confirm="confirm()"></Modal> <Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
@confirm="confirm()"
></Modal>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['row', 'api', 'pagename'], props: ["row", "api", "pagename"],
data() { data() {
return { return {
data: undefined, data: undefined,
detail: undefined, detail: undefined,
vbind2: {image: undefined, text: 'ABC', size: 'two', type: 'findata'}, vbind2: { image: undefined, text: "ABC", size: "two", type: "findata" },
current: undefined, current: undefined,
showmodal: undefined, showmodal: undefined,
obj: undefined obj: undefined,
} };
}, },
async created() { async created() {
if(!this.row) return if (!this.row) return;
this.data = await this.$getdata(this.api, {ref: this.row.id}) this.data = await this.$getdata(this.api, { ref: this.row.id });
}, },
computed: { computed: {
login: { login: {
get: function() {return this.$store.login}, get: function () {
set: function(val) {this.$store.commit("updateLogin", {login: val})} return this.$store.login;
} },
set: function (val) {
this.$store.commit("updateLogin", { login: val });
},
},
}, },
methods: { methods: {
async save() { async save() {
if(this.$empty(this.detail)) return this.$snackbar('Chưa nhập nội dung ghi chú') if (this.$empty(this.detail)) return this.$snackbar("Chưa nhập nội dung ghi chú");
let data = {user: this.$store.login.id, detail: this.detail, ref: this.row.id} let data = {
if(this.current) { user: this.$store.login.id,
data = this.$copy(this.current) detail: this.detail,
data.detail = this.detail ref: this.row.id,
};
if (this.current) {
data = this.$copy(this.current);
data.detail = this.detail;
} }
let rs = data.id? await this.$updateapi(this.api, data) : await this.$insertapi(this.api, data) let rs = data.id ? await this.$updateapi(this.api, data) : await this.$insertapi(this.api, data);
if(!rs) return if (!rs) return;
this.detail = undefined this.detail = undefined;
if(this.current) { if (this.current) {
this.current = undefined this.current = undefined;
let idx = this.$findIndex(this.data, {id: rs.id}) let idx = this.$findIndex(this.data, { id: rs.id });
this.$set(this.data, idx, rs) this.$set(this.data, idx, rs);
} else { } else {
this.data.push(rs) this.data.push(rs);
let rows = this.$copy(this.$store[this.pagename].data) let rows = this.$copy(this.$store[this.pagename].data);
let idx = this.$findIndex(rows, {id: this.row.id}) let idx = this.$findIndex(rows, { id: this.row.id });
let copy = this.$copy(this.row) let copy = this.$copy(this.row);
copy.count_note += 1 copy.count_note += 1;
rows[idx] = copy rows[idx] = copy;
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}}) this.$store.commit("updateState", {
name: this.pagename,
key: "update",
data: { data: rows },
});
} }
}, },
edit(v) { edit(v) {
this.current = this.$copy(v) this.current = this.$copy(v);
this.detail = v.detail this.detail = v.detail;
}, },
askConfirm(v, i) { askConfirm(v, i) {
this.obj = {v: v, i: i} this.obj = { v: v, i: i };
this.showmodal = {component: `dialog/Confirm`,vbind: {content: 'Bạn có muốn xóa ghi chú này không?', duration: 10}, this.showmodal = {
title: 'Xóa ghi chú', width: '500px', height: '100px'} component: `dialog/Confirm`,
vbind: { content: "Bạn muốn xóa ghi chú này không?", duration: 10 },
title: "Xóa ghi chú",
width: "500px",
height: "100px",
};
}, },
async confirm() { async confirm() {
let v = this.obj.v let v = this.obj.v;
let i = this.obj.i let i = this.obj.i;
let rs = await this.$deleteapi(this.api, v.id) let rs = await this.$deleteapi(this.api, v.id);
if(rs==='error') return if (rs === "error") return;
this.$delete(this.data, i) this.$delete(this.data, i);
let rows = this.$copy(this.$store[this.pagename].data) let rows = this.$copy(this.$store[this.pagename].data);
let idx = this.$findIndex(rows, {id: this.row.id}) let idx = this.$findIndex(rows, { id: this.row.id });
let copy = this.$copy(this.row) let copy = this.$copy(this.row);
copy.count_note -= 1 copy.count_note -= 1;
rows[idx] = copy rows[idx] = copy;
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}}) this.$store.commit("updateState", {
} name: this.pagename,
} key: "update",
} data: { data: rows },
</script> });
},
},
};
</script>

View File

@@ -1,18 +1,24 @@
<template> <template>
<span <span
v-if="row.count_note || $getEditRights()" v-if="row.count_note || $getEditRights()"
class="dot-primary" class="dot-primary"
@click="doClick()" @click="doClick()"
>{{ row.count_note || '+' }}</span> >{{ row.count_note || "+" }}</span
>
</template> </template>
<script> <script>
export default { export default {
props: ['row', 'api', 'pagename'], props: ["row", "api", "pagename"],
methods: { methods: {
doClick() { doClick() {
let obj = {component: 'common/NoteInfo', title: 'Ghi chú', width: '50%', vbind: {row: this.row, api: this.api, pagename: this.pagename}} let obj = {
this.$emit('open', {name: 'dataevent', data: {modal: obj}}) component: "common/NoteInfo",
} title: "Ghi chú",
} width: "50%",
} vbind: { row: this.row, api: this.api, pagename: this.pagename },
</script> };
this.$emit("open", { name: "dataevent", data: { modal: obj } });
},
},
};
</script>

View File

@@ -1,54 +1,82 @@
<template> <template>
<div> <div>
<p class="fsb-30">{{ text }} <p class="fsb-30">
<a class="ml-3" @click="copy()"> {{ text }}
<SvgIcon v-bind="{name: 'copy.svg', type: 'primary', size: 24}"></SvgIcon> <a
class="ml-3"
@click="copy()"
>
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 24 }"></SvgIcon>
</a> </a>
</p> </p>
<p class="buttons mt-4"> <p class="buttons mt-4">
<button class="button is-primary" @click="call()">Call</button> <button
<button class="button is-primary" @click="sms()">SMS</button> class="button is-primary"
<button class="button is-primary" @click="openZalo()">Zalo</button> @click="call()"
>
Call
</button>
<button
class="button is-primary"
@click="sms()"
>
SMS
</button>
<button
class="button is-primary"
@click="openZalo()"
>
Zalo
</button>
</p> </p>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['row', 'pagename'], props: ["row", "pagename"],
data() { data() {
return { return {
text: undefined, text: undefined,
phone: this.row.customer__phone || this.row.party__phone || this.row.phone, phone: this.row.customer__phone || this.row.party__phone || this.row.phone,
showmodal: undefined showmodal: undefined,
} };
}, },
created() { created() {
var format = function(s) { var format = function (s) {
return `${s.slice(0,3)} ${s.slice(3,6)} ${s.slice(6, 20)}` return `${s.slice(0, 3)} ${s.slice(3, 6)} ${s.slice(6, 20)}`;
} };
this.text = format(this.phone) this.text = format(this.phone);
}, },
methods: { methods: {
call() { call() {
window.open(`tel:${this.phone}`) window.open(`tel:${this.phone}`);
}, },
sms() { sms() {
window.open(`sms:${this.phone}`) window.open(`sms:${this.phone}`);
}, },
sendSms() { sendSms() {
let api = this.row.code.indexOf('CN')>=0? 'customersms' : undefined let api = this.row.code.indexOf("CN") >= 0 ? "customersms" : undefined;
if(this.row.code.indexOf('LN')>=0) api = 'loansms' if (this.row.code.indexOf("LN") >= 0) api = "loansms";
else if(this.row.code.indexOf('TS')>=0) api = 'collateralsms' else if (this.row.code.indexOf("TS") >= 0) api = "collateralsms";
this.showmodal = {component: 'user/Sms', title: 'Nhắn tin SMS', width: '50%', height: '400px', this.showmodal = {
vbind: {row: this.row, pagename: this.pagename, api: api}} component: "user/Sms",
title: "Nhắn tin SMS",
width: "50%",
height: "400px",
vbind: { row: this.row, pagename: this.pagename, api: api },
};
}, },
copy() { copy() {
this.$copyToClipboard(this.phone) this.$copyToClipboard(this.phone);
}, },
openZalo() { openZalo() {
window.open(`https://zalo.me/${this.phone}`, '_blank') window.open(`https://zalo.me/${this.phone}`, "_blank");
} },
} },
} };
</script> </script>

View File

@@ -1,25 +1,28 @@
<template> <template>
<span class="dot-primary" @click="onClick()">{{ row.count_product }}</span> <span
class="dot-primary"
@click="onClick()"
>{{ row.count_product }}</span
>
</template> </template>
<script> <script>
// use in Khách hàng -> Giao dịch (<DataView :setting='customer-all-transaction'/>) // use in Khách hàng -> Giao dịch (<DataView :setting='customer-all-transaction'/>)
export default { export default {
props: ['row', 'api', 'pagename'], props: ["row", "api", "pagename"],
methods: { methods: {
onClick() { onClick() {
const obj = { const obj = {
component: 'common/ProductInfo', component: "common/ProductInfo",
title: 'Sản phẩm', title: "Sản phẩm",
width: '60%', width: "60%",
height: '400px', height: "400px",
vbind: { vbind: {
row: this.row, row: this.row,
pagename: this.pagename pagename: this.pagename,
} },
} };
this.$emit('open', {name: 'dataevent', data: { modal: obj }}) this.$emit("open", { name: "dataevent", data: { modal: obj } });
} },
} },
} };
</script> </script>

View File

@@ -1,21 +1,24 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
row: Object, row: Object,
pagename: String pagename: String,
}); });
const { $id } = useNuxtApp(); const { $id } = useNuxtApp();
</script> </script>
<template> <template>
<div> <div>
<DataView v-bind="{ <DataView
setting: 'product-info', v-bind="{
pagename: $id(), setting: 'product-info',
api: 'product', pagename: $id(),
params: { api: 'product',
filter: { prdbk__transaction__customer: props.row.id }, params: {
// copied from 02-connection.js filter: { prdbk__transaction__customer: props.row.id },
values: 'price_excluding_vat,prdbk__transaction__txncurrent__detail__status__name,locked_until,note,cart,cart__name,cart__code,cart__dealer,cart__dealer__code,cart__dealer__name,direction,type,zone_type,dealer,link,type__name,dealer__code,dealer__name,prdbk,prdbk__transaction__customer,prdbk__transaction,prdbk__transaction__policy__code,prdbk__transaction__sale_price,prdbk__transaction__discount_amount,prdbk__transaction__code,prdbk__transaction__customer__code,prdbk__transaction__customer__phone,prdbk__transaction__customer__fullname,prdbk__transaction__customer__legal_code,id,code,trade_code,land_lot_code,zone_code,zone_type__name,lot_area,building_area,total_built_area,number_of_floors,land_lot_size,origin_price,direction__name,villa_model,product_type,template_name,project,project__name,status,status__code,status__name,status__color,status__sale_status,status__sale_status__color,create_time,prdbk__transaction__amount_received,prdbk__transaction__amount_remain', // copied from 02-connection.js
} values:
}" /> 'price_excluding_vat,prdbk__transaction__txncurrent__detail__status__name,locked_until,note,cart,cart__name,cart__code,cart__dealer,cart__dealer__code,cart__dealer__name,direction,type,zone_type,dealer,link,type__name,dealer__code,dealer__name,prdbk,prdbk__transaction__customer,prdbk__transaction,prdbk__transaction__policy__code,prdbk__transaction__sale_price,prdbk__transaction__discount_amount,prdbk__transaction__code,prdbk__transaction__customer__code,prdbk__transaction__customer__phone,prdbk__transaction__customer__fullname,prdbk__transaction__customer__legal_code,id,code,trade_code,land_lot_code,zone_code,zone_type__name,lot_area,building_area,total_built_area,number_of_floors,land_lot_size,origin_price,direction__name,villa_model,product_type,template_name,project,project__name,status,status__code,status__name,status__color,status__sale_status,status__sale_status__color,create_time,prdbk__transaction__amount_received,prdbk__transaction__amount_remain',
},
}"
/>
</div> </div>
</template> </template>

View File

@@ -1,27 +1,40 @@
<template> <template>
<div class="has-text-centered"> <div class="has-text-centered">
<div class="mb-4"> <div class="mb-4">
<p v-if="row && row.fullname"><b>{{row.fullname}}</b></p> <p v-if="row && row.fullname">
<b>{{ row.fullname }}</b>
</p>
</div> </div>
<div class="mt-2 px-5 is-flex is-justify-content-center"> <div class="mt-2 px-5 is-flex is-justify-content-center">
<ClientOnly> <ClientOnly>
<Qrcode <Qrcode
v-if="finalLink" v-if="finalLink"
:key="finalLink" :key="finalLink"
id="qrcode" id="qrcode"
:value="finalLink" :value="finalLink"
:size="300" :size="300"
/> />
</ClientOnly> </ClientOnly>
<div v-if="!finalLink" style="width: 300px; height: 300px; border: 1px dashed #ccc; display: flex; align-items: center; justify-content: center; color: #888;"> <div
v-if="!finalLink"
style="
width: 300px;
height: 300px;
border: 1px dashed #ccc;
display: flex;
align-items: center;
justify-content: center;
color: #888;
"
>
Không dữ liệu để tạo QR Code Không dữ liệu để tạo QR Code
</div> </div>
</div> </div>
<div class="mt-2 is-flex is-justify-content-center is-gap-1"> <div class="mt-2 is-flex is-justify-content-center is-gap-1">
<a <a
@click="openLink()" @click="openLink()"
class="button is-light is-link is-rounded" class="button is-light is-link is-rounded"
:title="isVietnamese ? 'Mở đường dẫn liên kết' : 'Open external link'" :title="isVietnamese ? 'Mở đường dẫn liên kết' : 'Open external link'"
v-if="finalLink" v-if="finalLink"
@@ -31,8 +44,8 @@
</span> </span>
</a> </a>
<a <a
@click="download()" @click="download()"
class="button is-light is-link is-rounded" class="button is-light is-link is-rounded"
:title="isVietnamese ? 'Tải Xuống QR Code' : 'Download QR Code'" :title="isVietnamese ? 'Tải Xuống QR Code' : 'Download QR Code'"
v-if="finalLink" v-if="finalLink"
@@ -45,83 +58,85 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { computed } from "vue";
import { useStore } from "@/stores/index"; import { useStore } from "@/stores/index";
const store = useStore();
const isVietnamese = computed(() => store.lang === "vi")
const { $getpath, $snackbar } = useNuxtApp() const store = useStore();
const props = defineProps({ const isVietnamese = computed(() => store.lang === "vi");
row: Object,
link: String
})
const finalLink = computed(() => { const { $getpath, $snackbar } = useNuxtApp();
if (props.link) { const props = defineProps({
return props.link row: Object,
} link: String,
if (props.row && props.row.code) { });
const path = $getpath()
const baseUrl = path ? path.replace('api.', '') : '';
return `${baseUrl}loan/${props.row.code}`
}
return ''
})
function openLink() { const finalLink = computed(() => {
if (finalLink.value) { if (props.link) {
window.open(finalLink.value, "_blank") return props.link;
} }
if (props.row && props.row.code) {
const path = $getpath();
const baseUrl = path ? path.replace("api.", "") : "";
return `${baseUrl}loan/${props.row.code}`;
}
return "";
});
function openLink() {
if (finalLink.value) {
window.open(finalLink.value, "_blank");
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function download() {
if (!finalLink.value) return;
let svg = document.getElementById("qrcode");
let attempts = 0;
while (!svg && attempts < 5) {
await sleep(100);
svg = document.getElementById("qrcode");
attempts++;
} }
function sleep(ms) { if (!svg) {
return new Promise(resolve => setTimeout(resolve, ms)); console.error("QR Code SVG element not found after waiting.");
$snackbar(isVietnamese.value ? "Không tìm thấy mã QR để tải xuống." : "QR Code not found for download.", {
type: "is-danger",
});
return;
} }
async function download() { const serializer = new XMLSerializer();
if (!finalLink.value) return; const svgData = serializer.serializeToString(svg);
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);
let svg = document.getElementById('qrcode'); const image = new Image();
let attempts = 0; image.onload = () => {
const canvas = document.createElement("canvas");
while (!svg && attempts < 5) { canvas.width = 300;
await sleep(100); canvas.height = 300;
svg = document.getElementById('qrcode'); const ctx = canvas.getContext("2d");
attempts++; ctx.drawImage(image, 0, 0, 300, 300);
}
if (!svg) { const pngUrl = canvas.toDataURL("image/png");
console.error("QR Code SVG element not found after waiting.");
$snackbar(isVietnamese.value ? 'Không tìm thấy mã QR để tải xuống.' : 'QR Code not found for download.', { type: 'is-danger' });
return;
}
const serializer = new XMLSerializer() const linkElement = document.createElement("a");
const svgData = serializer.serializeToString(svg) linkElement.href = pngUrl;
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
const url = URL.createObjectURL(svgBlob)
const image = new Image() const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : "qrcode.png";
image.onload = () => { linkElement.download = filename;
const canvas = document.createElement('canvas') linkElement.click();
canvas.width = 300
canvas.height = 300
const ctx = canvas.getContext('2d')
ctx.drawImage(image, 0, 0, 300, 300)
const pngUrl = canvas.toDataURL('image/png') URL.revokeObjectURL(url);
$snackbar(isVietnamese.value ? "Đã tải xuống mã QR!" : "QR Code downloaded!", { type: "is-success" });
const linkElement = document.createElement('a') };
linkElement.href = pngUrl image.src = url;
}
const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : 'qrcode.png' </script>
linkElement.download = filename
linkElement.click()
URL.revokeObjectURL(url)
$snackbar(isVietnamese.value ? 'Đã tải xuống mã QR!' : 'QR Code downloaded!', { type: 'is-success' });
}
image.src = url
}
</script>

View File

@@ -1,10 +1,13 @@
<template> <template>
<div> <div>
<DataView v-if="vbind" v-bind="vbind"></DataView> <DataView
v-if="vbind"
v-bind="vbind"
></DataView>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['vbind'] props: ["vbind"],
} };
</script> </script>

View File

@@ -1,175 +1,279 @@
<template> <template>
<div v-if="record"> <div v-if="record">
<div class="columns is-multiline mx-0"> <div class="columns is-multiline mx-0">
<div :class="`column is-3 ${viewport===1? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label">{{findLang('code')}}<b class="ml-1 has-text-danger">*</b></label> <label class="label">{{ findLang("code") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control"> <div class="control">
<input class="input has-text-black" disabled type="text" placeholder="" v-model="record.code"> <input
</div> class="input has-text-black"
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p> disabled
</div> type="text"
</div> placeholder=""
v-model="record.code"
/>
</div>
<p
class="help is-danger"
v-if="errors.code"
>
{{ errors.code }}
</p>
</div>
</div>
<div :class="`column is-6 ${viewport===1? 'px-0 pb-1' : ''}`"> <div :class="`column is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label> <label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label>
<div class="control"> <div class="control">
<input class="input" type="text" placeholder="" v-model="record.fullname"> <input
</div> class="input"
<p class="help is-danger" v-if="errors.fullname">{{errors.fullname}}</p> type="text"
</div> placeholder=""
v-model="record.fullname"
/>
</div>
<p
class="help is-danger"
v-if="errors.fullname"
>
{{ errors.fullname }}
</p>
</div>
</div>
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{ findLang("shortname") }}</label>
<div class="control">
<input
class="input"
type="text"
placeholder=""
v-model="record.shortname"
/>
</div>
<p
class="help is-danger"
v-if="errors.shortname"
>
{{ errors.shortname }}
</p>
</div>
</div>
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{ findLang("taxcode") }}</label>
<div class="control">
<input
class="input"
type="text"
placeholder=""
v-model="record.legal_code"
/>
</div>
<p
class="help is-danger"
v-if="errors.legal_code"
>
{{ errors.legal_code }}
</p>
</div>
</div>
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">Điện thoại</label>
<div class="control">
<InputPhone
v-bind="{ record: record, attr: 'phone', placeholder: '' }"
@phone="selected('phone', $event)"
></InputPhone>
</div>
<p
class="help is-danger"
v-if="errors.phone"
>
{{ errors.phone }}
<a
v-if="existedCustomer"
@click="showCustomer()"
>Chi tiết</a
>
</p>
</div>
</div>
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">Email</label>
<div class="control">
<InputEmail
v-bind="{ record: record, attr: 'email', placeholder: '' }"
@email="selected('email', $event)"
></InputEmail>
</div>
<p
class="help is-danger"
v-if="errors.email"
>
{{ errors.email }}
</p>
</div>
</div>
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">Website</label>
<div class="control">
<input
class="input"
type="text"
placeholder=""
v-model="record.website"
/>
</div>
</div>
</div>
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{ findLang("country") }}</label>
<div class="control">
<SearchBox
v-bind="{
vdata: store.country,
field: 'name',
column: ['name'],
first: true,
optionid: record.country,
position: 'is-top-left',
}"
@option="selected('_country', $event)"
></SearchBox>
</div>
</div>
</div>
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{ findLang("province") }}</label>
<div class="control">
<input
class="input"
type="text"
placeholder=""
v-model="record.province"
/>
</div>
</div>
</div>
<div :class="`column is-12 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{ findLang("address") }}</label>
<div class="control">
<input
class="input"
type="text"
placeholder=""
v-model="record.address"
/>
</div>
<p
class="help is-danger"
v-if="errors.address"
></p>
</div>
</div>
</div> </div>
<div :class="`column is-3 ${viewport===1? 'px-0 pb-1' : ''}`"> <div class="mt-2">
<div class="field"> <button
<label class="label">{{findLang('shortname')}}</label> class="button is-primary has-text-white"
<div class="control"> @click="update()"
<input class="input" type="text" placeholder="" v-model="record.shortname"> >
</div> {{ findLang("save") }}
<p class="help is-danger" v-if="errors.shortname">{{errors.shortname}}</p> </button>
</div>
</div> </div>
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`"> <Modal
<div class="field"> @close="showmodal = undefined"
<label class="label">{{findLang('taxcode')}}</label> v-bind="showmodal"
<div class="control"> v-if="showmodal"
<input class="input" type="text" placeholder="" v-model="record.legal_code"> ></Modal>
</div> </div>
<p class="help is-danger" v-if="errors.legal_code">{{errors.legal_code}}</p>
</div>
</div>
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">Điện thoại</label>
<div class="control">
<InputPhone v-bind="{record: record, attr: 'phone', placeholder: ''}" @phone="selected('phone', $event)"></InputPhone>
</div>
<p class="help is-danger" v-if="errors.phone">{{ errors.phone }}
<a v-if="existedCustomer" @click="showCustomer()">Chi tiết</a>
</p>
</div>
</div>
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">Email</label>
<div class="control">
<InputEmail v-bind="{record: record, attr: 'email', placeholder: ''}" @email="selected('email', $event)"></InputEmail>
</div>
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
</div>
</div>
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">Website</label>
<div class="control">
<input class="input" type="text" placeholder="" v-model="record.website">
</div>
</div>
</div>
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{findLang('country')}}</label>
<div class="control">
<SearchBox v-bind="{vdata: store.country, field:'name', column:['name'], first:true, optionid: record.country, position: 'is-top-left'}"
@option="selected('_country', $event)"></SearchBox>
</div>
</div>
</div>
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{findLang('province')}}</label>
<div class="control">
<input class="input" type="text" placeholder="" v-model="record.province">
</div>
</div>
</div>
<div :class="`column is-12 ${viewport===1? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{findLang('address')}}</label>
<div class="control">
<input class="input" type="text" placeholder="" v-model="record.address">
</div>
<p class="help is-danger" v-if="errors.address"></p>
</div>
</div>
</div>
<div class="mt-2">
<button class="button is-primary has-text-white" @click="update()">{{ findLang('save') }}</button>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</div>
</template> </template>
<script setup> <script setup>
import InputPhone from '~/components/common/InputPhone' import InputPhone from "~/components/common/InputPhone";
import InputEmail from '~/components/common/InputEmail' import InputEmail from "~/components/common/InputEmail";
import SearchBox from '~/components/SearchBox' import SearchBox from "~/components/SearchBox";
import { useStore } from '@/stores/index' import { useStore } from "@/stores/index";
var props = defineProps({ var props = defineProps({
pagename: String, pagename: String,
row: Object row: Object,
}) });
const store = useStore() const store = useStore();
const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } = useNuxtApp() const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } =
const emit = defineEmits(['update', 'dataevent']) useNuxtApp();
var viewport = store.viewport const emit = defineEmits(["update", "dataevent"]);
var errors = ref({}) var viewport = store.viewport;
var record = ref() var errors = ref({});
var showmodal = undefined var record = ref();
var existedCustomer = undefined var showmodal = undefined;
async function initData() { var existedCustomer = undefined;
if(props.row) { async function initData() {
let conn = $findapi('company') if (props.row) {
conn.params.filter = {id: props.row.company || props.row.customer__company || props.row.id} let conn = $findapi("company");
let rs = await $getapi([conn]) conn.params.filter = {
let found = $find(rs, {name: 'company'}) id: props.row.company || props.row.customer__company || props.row.id,
if(found.data.rows.length>0) record.value = found.data.rows[0] };
} else { let rs = await $getapi([conn]);
record.value = {} let found = $find(rs, { name: "company" });
if (found.data.rows.length > 0) record.value = found.data.rows[0];
} else {
record.value = {};
}
}
function findLang(code) {
let found = $find(store.common, { code: code });
return found ? found[store.lang] : "";
}
function showCustomer() {
showmodal.value = {
component: "customer/CustomerView",
width: "60%",
height: "600px",
title: "Khách hàng",
vbind: { row: existedCustomer },
};
}
function selected(attr, obj) {
record.value[attr] = obj;
}
function checkError() {
existedCustomer = undefined;
errors.value = {};
if ($empty(record.value.fullname)) errors.value.fullname = "Họ tên không được bỏ trống";
if (record.value.phone) {
let text = $errPhone(record.value.phone);
if (text) errors.value.phone = text;
}
return Object.keys(errors.value).length > 0;
}
async function update() {
if (checkError()) return;
if (!record.value.id) {
if (record.value.phone) record.value.phone = record.value.phone.trim();
let obj = await $getdata("company", { phone: record.value.phone }, undefined, true);
if (obj) {
existedCustomer = obj;
errors.phone = "Số điện thoại đã tồn tại trong hệ thống.";
} }
} }
function findLang(code) { record.value = $resetNull(record.value);
let found = $find(store.common, {code: code}) if (record.value._country) record.value.country = record.value._country.id;
return found? found[store.lang] : '' if (!record.value.creator) record.value.creator = store.login.id;
} record.value.updater = store.login.id;
function showCustomer() { record.update_time = new Date();
showmodal.value = {component: 'customer/CustomerView', width: '60%', height: '600px', title: 'Khách hàng', vbind: {row: existedCustomer}} let rs = record.value.id ? await $updateapi("company", record.value) : await $insertapi("company", record.value);
} if (rs === "error") return;
function selected(attr, obj) { if (!record.value.id) $snackbar(`Khách hàng đã được khởi tạo với mã <b>${rs.code}</b>`, "Thành công", "Success");
record.value[attr] = obj record.value.id = rs.id;
} let ele = await $getdata("company", { id: rs.id }, null, true);
function checkError() { emit("update", ele);
existedCustomer = undefined emit("modalevent", { name: "dataevent", data: ele });
errors.value = {} }
if($empty(record.value.fullname)) errors.value.fullname = 'Họ tên không được bỏ trống' initData();
if(record.value.phone) { </script>
let text = $errPhone(record.value.phone)
if(text) errors.value.phone = text
}
return Object.keys(errors.value).length>0
}
async function update() {
if(checkError()) return
if(!record.value.id) {
if(record.value.phone) record.value.phone = record.value.phone.trim()
let obj = await $getdata('company', {phone: record.value.phone}, undefined, true)
if(obj) {
existedCustomer = obj
errors.phone = 'Số điện thoại đã tồn tại trong hệ thống.'
}
}
record.value = $resetNull(record.value)
if(record.value._country) record.value.country = record.value._country.id
if(!record.value.creator) record.value.creator = store.login.id
record.value.updater = store.login.id
record.update_time = new Date()
let rs = record.value.id? await $updateapi('company', record.value)
: await $insertapi('company', record.value)
if(rs==='error') return
if(!record.value.id) $snackbar(`Khách hàng đã được khởi tạo với mã <b>${rs.code}</b>`, 'Thành công', 'Success')
record.value.id = rs.id
let ele = await $getdata('company', {id:rs.id}, null, true)
emit('update', ele)
emit('modalevent', {name: 'dataevent', data: ele})
}
initData()
</script>

View File

@@ -10,14 +10,21 @@
v-for="(v, i) in tabs" v-for="(v, i) in tabs"
:key="i" :key="i"
:class="['is-clickable p-3', i !== 0 && 'mt-2', getStyle(v)]" :class="['is-clickable p-3', i !== 0 && 'mt-2', getStyle(v)]"
style="width: 130px; border-radius: 4px;" style="width: 130px; border-radius: 4px"
@click="changeTab(v)" @click="changeTab(v)"
> >
{{ isVietnamese ? v.name : v.en }} {{ isVietnamese ? v.name : v.en }}
</div> </div>
</template> </template>
<div v-else class="field is-grouped is-grouped-multiline"> <div
<div class="control" v-for="(v, i) in tabs" @click="changeTab(v)"> v-else
class="field is-grouped is-grouped-multiline"
>
<div
class="control"
v-for="(v, i) in tabs"
@click="changeTab(v)"
>
<div style="width: 130px"> <div style="width: 130px">
<div :class="`py-3 px-3 ${getStyle(v)}`"> <div :class="`py-3 px-3 ${getStyle(v)}`">
{{ isVietnamese ? v.name : v.en }} {{ isVietnamese ? v.name : v.en }}
@@ -54,7 +61,11 @@
</div> </div>
</div> </div>
<Modal @close="handleModalClose" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@close="handleModalClose"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template> </template>
<script setup> <script setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
@@ -99,11 +110,7 @@ function getStyle(v) {
function changeTab(v) { function changeTab(v) {
if (tab.value === v.code) return; if (tab.value === v.code) return;
if (!record) if (!record) return $dialog("Vui lòng <b>lưu dữ liệu</b> trước khi chuyển sang mục tiếp theo", "Thông báo");
return $dialog(
"Vui lòng <b>lưu dữ liệu</b> trước khi chuyển sang mục tiếp theo",
"Thông báo"
);
tab.value = v.code; tab.value = v.code;
} }
@@ -115,7 +122,7 @@ function update(v) {
record = { record = {
...v, ...v,
label: `${v.code} / ${v.fullname} / ${v.phone || ""}`, label: `${v.code} / ${v.fullname} / ${v.phone || ""}`,
} };
emit("modalevent", { name: "dataevent", data: record }); emit("modalevent", { name: "dataevent", data: record });
if (!props.isEditMode) emit("close"); if (!props.isEditMode) emit("close");
} }
@@ -123,8 +130,7 @@ function update(v) {
<style scoped> <style scoped>
.title { .title {
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial, font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
sans-serif !important;
} }
.button.is-large.is-fullwidth:hover { .button.is-large.is-fullwidth:hover {
color: white !important; color: white !important;

View File

@@ -4,8 +4,7 @@
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("custcode")[lang] >{{ dataLang && findFieldName("custcode")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<input <input
@@ -15,19 +14,30 @@
placeholder="" placeholder=""
/> />
</div> </div>
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p> <p
class="help is-danger"
v-if="errors.code"
>
{{ errors.code }}
</p>
</div> </div>
</div> </div>
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("name")[lang] >{{ dataLang && findFieldName("name")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<input class="input" type="text" placeholder="" /> <input
class="input"
type="text"
placeholder=""
/>
</div> </div>
<p class="help is-danger" v-if="errors.fullname"> <p
class="help is-danger"
v-if="errors.fullname"
>
{{ errors.fullname }} {{ errors.fullname }}
</p> </p>
</div> </div>
@@ -35,8 +45,7 @@
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("phone_number")[lang] >{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<InputPhone <InputPhone
@@ -45,7 +54,10 @@
> >
</InputPhone> </InputPhone>
</div> </div>
<p class="help is-danger" v-if="errors.phone"> <p
class="help is-danger"
v-if="errors.phone"
>
{{ errors.phone }} {{ errors.phone }}
<a <a
class="has-text-primary" class="has-text-primary"
@@ -66,14 +78,18 @@
> >
</InputEmail> </InputEmail>
</div> </div>
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p> <p
class="help is-danger"
v-if="errors.email"
>
{{ errors.email }}
</p>
</div> </div>
</div> </div>
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("gender")[lang] >{{ dataLang && findFieldName("gender")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<SearchBox <SearchBox
@@ -93,8 +109,7 @@
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("birth_date")[lang] >{{ dataLang && findFieldName("birth_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<Datepicker <Datepicker
@@ -103,14 +118,18 @@
> >
</Datepicker> </Datepicker>
</div> </div>
<p class="help is-danger" v-if="errors.dob">{{ errors.dob }}</p> <p
class="help is-danger"
v-if="errors.dob"
>
{{ errors.dob }}
</p>
</div> </div>
</div> </div>
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("country")[lang] >{{ dataLang && findFieldName("country")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<SearchBox <SearchBox
@@ -130,8 +149,7 @@
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("province")[lang] >{{ dataLang && findFieldName("province")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<input <input
@@ -146,8 +164,7 @@
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("district")[lang] >{{ dataLang && findFieldName("district")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<input <input
@@ -162,8 +179,7 @@
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("address")[lang] >{{ dataLang && findFieldName("address")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<input <input
@@ -173,14 +189,15 @@
v-model="record.address" v-model="record.address"
/> />
</div> </div>
<p class="help is-danger" v-if="errors.address"></p> <p
class="help is-danger"
v-if="errors.address"
></p>
</div> </div>
</div> </div>
<div :class="`column is-5 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-5 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label">{{ <label class="label">{{ dataLang && findFieldName("company")[lang] }}</label>
dataLang && findFieldName("company")[lang]
}}</label>
<div class="control"> <div class="control">
<SearchBox <SearchBox
v-bind="{ v-bind="{
@@ -201,8 +218,7 @@
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("personal_id")[lang] >{{ dataLang && findFieldName("personal_id")[lang] }}<b class="ml-1 has-text-danger">*</b></label
}}<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<SearchBox <SearchBox
@@ -222,8 +238,7 @@
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("idnum")[lang] }} >{{ dataLang && findFieldName("idnum")[lang] }} <b class="ml-1 has-text-danger">*</b></label
<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<input <input
@@ -233,7 +248,10 @@
v-model="record.legal_code" v-model="record.legal_code"
/> />
</div> </div>
<p class="help is-danger" v-if="errors.legal_code"> <p
class="help is-danger"
v-if="errors.legal_code"
>
{{ errors.legal_code }} {{ errors.legal_code }}
</p> </p>
</div> </div>
@@ -241,8 +259,7 @@
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("issued_date")[lang] }} >{{ dataLang && findFieldName("issued_date")[lang] }} <b class="ml-1 has-text-danger">*</b></label
<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<Datepicker <Datepicker
@@ -255,7 +272,10 @@
@date="selected('issued_date', $event)" @date="selected('issued_date', $event)"
></Datepicker> ></Datepicker>
</div> </div>
<p class="help is-danger" v-if="errors.issued_date"> <p
class="help is-danger"
v-if="errors.issued_date"
>
{{ errors.issued_date }} {{ errors.issued_date }}
</p> </p>
</div> </div>
@@ -263,8 +283,7 @@
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label" <label class="label"
>{{ dataLang && findFieldName("issued_place")[lang] }} >{{ dataLang && findFieldName("issued_place")[lang] }} <b class="ml-1 has-text-danger">*</b></label
<b class="ml-1 has-text-danger">*</b></label
> >
<div class="control"> <div class="control">
<input <input
@@ -274,7 +293,10 @@
v-model="record.issued_place" v-model="record.issued_place"
/> />
</div> </div>
<p class="help is-danger" v-if="errors.issued_place"></p> <p
class="help is-danger"
v-if="errors.issued_place"
></p>
</div> </div>
</div> </div>
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
@@ -306,9 +328,7 @@
<div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field"> <div class="field">
<label class="label">{{ <label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
dataLang && findFieldName("note")[lang]
}}</label>
<div class="control"> <div class="control">
<textarea <textarea
class="textarea" class="textarea"
@@ -317,7 +337,12 @@
rows="1" rows="1"
></textarea> ></textarea>
</div> </div>
<p class="help is-danger" v-if="errors.note">{{ errors.note }}</p> <p
class="help is-danger"
v-if="errors.note"
>
{{ errors.note }}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -330,11 +355,15 @@
}" }"
></Caption> ></Caption>
</div> </div>
<div class="columns mb-0 mx-0" v-for="(v, i) in people"> <div
class="columns mb-0 mx-0"
v-for="(v, i) in people"
>
<div :class="`column is-7 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-7 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<label class="label" v-if="i === 0" <label
>{{ findFieldName("select")[lang] class="label"
}}<b class="ml-1 has-text-danger">*</b></label v-if="i === 0"
>{{ findFieldName("select")[lang] }}<b class="ml-1 has-text-danger">*</b></label
> >
<SearchBox <SearchBox
v-bind="{ v-bind="{
@@ -351,12 +380,18 @@
@option="selectPeople($event, v, i)" @option="selectPeople($event, v, i)"
> >
</SearchBox> </SearchBox>
<p class="help is-danger" v-if="v.error">{{ v.error }}</p> <p
class="help is-danger"
v-if="v.error"
>
{{ v.error }}
</p>
</div> </div>
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<label class="label" v-if="i === 0" <label
>{{ findFieldName("relationship")[lang] class="label"
}}<b class="ml-1 has-text-danger">*</b></label v-if="i === 0"
>{{ findFieldName("relationship")[lang] }}<b class="ml-1 has-text-danger">*</b></label
> >
<SearchBox <SearchBox
v-bind="{ v-bind="{
@@ -374,23 +409,30 @@
</SearchBox> </SearchBox>
</div> </div>
<div :class="`column ${viewport === 1 ? 'px-0 pb-1' : ''}`"> <div :class="`column ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<label class="label" v-if="i === 0" <label
class="label"
v-if="i === 0"
>{{ findFieldName("addmore")[lang] }}...</label >{{ findFieldName("addmore")[lang] }}...</label
> >
<button class="button px-2 is-dark mr-3" @click="add()"> <button
<SvgIcon class="button px-2 is-dark mr-3"
v-bind="{ name: 'add1.png', type: 'white', size: 20 }" @click="add()"
></SvgIcon> >
<SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 20 }"></SvgIcon>
</button> </button>
<button class="button px-2 is-warning" @click="remove(v, i)"> <button
<SvgIcon class="button px-2 is-warning"
v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }" @click="remove(v, i)"
></SvgIcon> >
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
</button> </button>
</div> </div>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<button class="button is-primary has-text-white" @click="update()"> <button
class="button is-primary has-text-white"
@click="update()"
>
{{ store.lang === "en" ? "Save" : "Lưu lại" }} {{ store.lang === "en" ? "Save" : "Lưu lại" }}
</button> </button>
</div> </div>
@@ -410,17 +452,7 @@ import Datepicker from "~/components/datepicker/Datepicker";
import { useStore } from "@/stores/index"; import { useStore } from "@/stores/index";
const emit = defineEmits(["close", "update"]); const emit = defineEmits(["close", "update"]);
const nuxtApp = useNuxtApp(); const nuxtApp = useNuxtApp();
const { const { $getdata, $updateapi, $insertapi, $empty, $errPhone, $resetNull, $snackbar, $copy, $dayjs } = nuxtApp;
$getdata,
$updateapi,
$insertapi,
$empty,
$errPhone,
$resetNull,
$snackbar,
$copy,
$dayjs,
} = nuxtApp;
var props = defineProps({ var props = defineProps({
pagename: String, pagename: String,
row: Object, row: Object,
@@ -540,10 +572,7 @@ function showCustomer() {
} }
const selected = (fieldName, value) => { const selected = (fieldName, value) => {
const finalValue = const finalValue = value !== null && typeof value === "object" ? value.id || value.index : value;
value !== null && typeof value === "object"
? value.id || value.index
: value;
record.value[fieldName] = finalValue; record.value[fieldName] = finalValue;
if (errors.value[fieldName]) { if (errors.value[fieldName]) {
@@ -566,9 +595,7 @@ function checkError() {
} }
if ($empty(record.value.dob)) { if ($empty(record.value.dob)) {
errors.value.dob = isVietnamese.value errors.value.dob = isVietnamese.value ? "Ngày sinh không được bỏ trống" : "Date of birth cannot be empty";
? "Ngày sinh không được bỏ trống"
: "Date of birth cannot be empty";
} else { } else {
if (record.value.dob > new Date()) { if (record.value.dob > new Date()) {
errors.value.dob = isVietnamese.value errors.value.dob = isVietnamese.value
@@ -583,21 +610,15 @@ function checkError() {
} }
if ($empty(record.value.legal_code)) { if ($empty(record.value.legal_code)) {
errors.value.legal_code = isVietnamese.value errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code cannot be empty";
? "Mã số không được bỏ trống"
: "Legal code cannot be empty";
} }
if ($empty(record.value.issued_date)) { if ($empty(record.value.issued_date)) {
errors.value.issued_date = isVietnamese.value errors.value.issued_date = isVietnamese.value ? "Ngày cấp không được bỏ trống" : "Date of issue cannot be empty";
? "Ngày cấp không được bỏ trống"
: "Date of issue cannot be empty";
} }
if ($empty(record.value.issued_place)) { if ($empty(record.value.issued_place)) {
errors.value.issued_place = isVietnamese.value errors.value.issued_place = isVietnamese.value ? "Nơi cấp không được bỏ trống" : "Place of issue cannot be empty";
? "Nơi cấp không được bỏ trống"
: "Place of issue cannot be empty";
} }
return Object.keys(errors.value).length > 0; return Object.keys(errors.value).length > 0;
} }
@@ -637,45 +658,26 @@ async function update() {
try { try {
if (record.value.phone) { if (record.value.phone) {
record.value.phone = record.value.phone.trim(); record.value.phone = record.value.phone.trim();
let phoneCheck = await $getdata( let phoneCheck = await $getdata("customer", { phone: record.value.phone }, undefined, true);
"customer",
{ phone: record.value.phone },
undefined,
true
);
if (phoneCheck) { if (phoneCheck) {
existedCustomer = phoneCheck; existedCustomer = phoneCheck;
errors.value.phone = isVietnamese.value errors.value.phone = isVietnamese.value ? "Số điện thoại đã tồn tại." : "Phone number already exists.";
? "Số điện thoại đã tồn tại."
: "Phone number already exists.";
return; return;
} }
} }
// Kiểm tra email // Kiểm tra email
if (record.value.email && record.value.email.trim() !== "") { if (record.value.email && record.value.email.trim() !== "") {
let emailCheck = await $getdata( let emailCheck = await $getdata("customer", { email: record.value.email.trim() }, undefined, true);
"customer",
{ email: record.value.email.trim() },
undefined,
true
);
if (emailCheck) { if (emailCheck) {
errors.value.email = isVietnamese.value errors.value.email = isVietnamese.value ? "Email đã tồn tại." : "Email already exists.";
? "Email đã tồn tại."
: "Email already exists.";
return; return;
} }
} }
// Kiểm tra legal_code // Kiểm tra legal_code
if (record.value.legal_code) { if (record.value.legal_code) {
let legalCheck = await $getdata( let legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true);
"customer",
{ legal_code: record.value.legal_code },
undefined,
true
);
if (legalCheck) { if (legalCheck) {
errors.value.legal_code = isVietnamese.value errors.value.legal_code = isVietnamese.value
? "Số CMND/CCCD đã tồn tại." ? "Số CMND/CCCD đã tồn tại."
@@ -714,34 +716,25 @@ async function update() {
return; return;
} }
record.value.id = rs.id; record.value.id = rs.id;
const customerData = await $getdata( const customerData = await $getdata("customer", { id: rs.id }, undefined, true);
"customer",
{ id: rs.id },
undefined,
true
);
if (isNewCustomer) { if (isNewCustomer) {
// link user=customer // link user=customer
await $getdata("linkusercustomer", undefined, { phone: rs.phone }); await $getdata("linkusercustomer", undefined, { phone: rs.phone });
$snackbar( $snackbar(
`${ `${
isVietnamese.value isVietnamese.value ? "Khách hàng đã được khởi tạo với mã" : "Customer has been created with code"
? "Khách hàng đã được khởi tạo với mã"
: "Customer has been created with code"
} <b>${rs.code}</b>`, } <b>${rs.code}</b>`,
"Thành công", "Thành công",
"Success" "Success",
); );
} else { } else {
$snackbar( $snackbar(
`${ `${
isVietnamese.value isVietnamese.value ? "Khách hàng đã được cập nhật với mã" : "Customer has been updated with code"
? "Khách hàng đã được cập nhật với mã"
: "Customer has been updated with code"
} <b>${rs.code}</b>`, } <b>${rs.code}</b>`,
"Thành công", "Thành công",
"Success" "Success",
); );
} }
@@ -760,9 +753,7 @@ async function update() {
//Xử lý file uploads nếu có //Xử lý file uploads nếu có
if (record.value.image && record.value.image.length > 0) { if (record.value.image && record.value.image.length > 0) {
let arr = []; let arr = [];
record.value.image.map((v) => record.value.image.map((v) => arr.push({ ref: record.value.id, file: v }));
arr.push({ ref: record.value.id, file: v })
);
await $insertapi("customerfile", arr, undefined, false); await $insertapi("customerfile", arr, undefined, false);
} }
emit("update", customerData); emit("update", customerData);

View File

@@ -1,5 +1,8 @@
<template> <template>
<div v-if="!selectedCustomerType && isNewCustomer && !props.customerType" class="p-5"> <div
v-if="!selectedCustomerType && isNewCustomer && !props.customerType"
class="p-5"
>
<h3 class="title is-4 mb-5 has-text-centered"> <h3 class="title is-4 mb-5 has-text-centered">
{{ isVietnamese ? "Chọn loại khách hàng" : "Select Customer Type" }} {{ isVietnamese ? "Chọn loại khách hàng" : "Select Customer Type" }}
</h3> </h3>
@@ -7,8 +10,8 @@
<div class="column is-6"> <div class="column is-6">
<button <button
:disabled="!$getEditRights('edit', { code: 'individual', category: 'submenu' })" :disabled="!$getEditRights('edit', { code: 'individual', category: 'submenu' })"
class="button is-large is-fullwidth" class="button is-large is-fullwidth"
style="height: 120px" style="height: 120px"
@click="selectCustomerType(1)" @click="selectCustomerType(1)"
> >
<div class="has-text-centered"> <div class="has-text-centered">
@@ -22,10 +25,10 @@
</button> </button>
</div> </div>
<div class="column is-6"> <div class="column is-6">
<button <button
:disabled="!$getEditRights('edit', { code: 'org', category: 'submenu' })" :disabled="!$getEditRights('edit', { code: 'org', category: 'submenu' })"
class="button is-large is-fullwidth" class="button is-large is-fullwidth"
style="height: 120px" style="height: 120px"
@click="selectCustomerType(2)" @click="selectCustomerType(2)"
> >
<div class="has-text-centered"> <div class="has-text-centered">
@@ -46,28 +49,60 @@
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-4"> <div class="column is-4">
<div class="field"> <div class="field">
<label class="label">{{ isIndividual ? "Họ và tên" : "Tên tổ chức" }}<b class="ml-1 has-text-danger">*</b></label> <label class="label"
>{{ isIndividual ? "Họ và tên" : "Tên tổ chức" }}<b class="ml-1 has-text-danger">*</b></label
>
<div class="control"> <div class="control">
<input class="input" type="text" v-model="record.fullname" /> <input
class="input"
type="text"
v-model="record.fullname"
/>
</div> </div>
<p class="help is-danger" v-if="errors.fullname">{{ errors.fullname }}</p> <p
class="help is-danger"
v-if="errors.fullname"
>
{{ errors.fullname }}
</p>
</div> </div>
</div> </div>
<div class="column is-4"> <div class="column is-4">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label> <label class="label"
<InputPhone v-bind="{ record: record, attr: 'phone' }" @phone="selected('phone', $event)"></InputPhone> >{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label
<p class="help is-danger" v-if="errors.phone"> >
<InputPhone
v-bind="{ record: record, attr: 'phone' }"
@phone="selected('phone', $event)"
></InputPhone>
<p
class="help is-danger"
v-if="errors.phone"
>
{{ errors.phone }} {{ errors.phone }}
<a class="has-text-primary" v-if="existedCustomer" @click="showCustomer()">Chi tiết</a> <a
class="has-text-primary"
v-if="existedCustomer"
@click="showCustomer()"
>Chi tiết</a
>
</p> </p>
</div> </div>
</div> </div>
<div class="column is-4"> <div class="column is-4">
<div class="field"> <div class="field">
<label class="label">Email</label> <label class="label">Email</label>
<InputEmail v-bind="{ record: record, attr: 'email' }" @email="selected('email', $event)"></InputEmail> <InputEmail
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p> v-bind="{ record: record, attr: 'email' }"
@email="selected('email', $event)"
></InputEmail>
<p
class="help is-danger"
v-if="errors.email"
>
{{ errors.email }}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -75,28 +110,47 @@
<div class="column is-6"> <div class="column is-6">
<div class="field"> <div class="field">
<label class="label">Địa chỉ liên hệ</label> <label class="label">Địa chỉ liên hệ</label>
<input class="input" type="text" v-model="record.contact_address" /> <input
class="input"
type="text"
v-model="record.contact_address"
/>
</div> </div>
</div> </div>
<div class="column is-6"> <div class="column is-6">
<div class="field"> <div class="field">
<label class="label">{{ isIndividual ? 'Địa chỉ thường trú' : 'Địa chỉ đăng ký' }}</label> <label class="label">{{ isIndividual ? "Địa chỉ thường trú" : "Địa chỉ đăng ký" }}</label>
<input class="input" type="text" v-model="record.address" /> <input
class="input"
type="text"
v-model="record.address"
/>
</div> </div>
</div> </div>
</div> </div>
<div v-if="isOrganization" class="columns is-multiline"> <div
v-if="isOrganization"
class="columns is-multiline"
>
<div class="column is-6"> <div class="column is-6">
<div class="field"> <div class="field">
<label class="label">Tài khoản ngân hàng</label> <label class="label">Tài khoản ngân hàng</label>
<input class="input" type="text" v-model="organizationData.bank_account" /> <input
class="input"
type="text"
v-model="organizationData.bank_account"
/>
</div> </div>
</div> </div>
<div class="column is-6"> <div class="column is-6">
<div class="field"> <div class="field">
<label class="label">Tên ngân hàng</label> <label class="label">Tên ngân hàng</label>
<input class="input" type="text" v-model="organizationData.bank_name" /> <input
class="input"
type="text"
v-model="organizationData.bank_name"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -104,46 +158,117 @@
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">{{ isIndividual ? 'Giấy tờ tùy thân' : 'Giấy tờ' }}<b class="ml-1 has-text-danger">*</b></label> <label class="label"
<SearchBox v-bind="{ vdata: filteredLegalTypes, api: 'legaltype', field: isVietnamese ? 'name' : 'en', column: ['name', 'en'], first: true, optionid: record.legal_type }" @option="selected('legal_type', $event)"></SearchBox> >{{ isIndividual ? "Giấy tờ tùy thân" : "Giấy tờ" }}<b class="ml-1 has-text-danger">*</b></label
>
<SearchBox
v-bind="{
vdata: filteredLegalTypes,
api: 'legaltype',
field: isVietnamese ? 'name' : 'en',
column: ['name', 'en'],
first: true,
optionid: record.legal_type,
}"
@option="selected('legal_type', $event)"
></SearchBox>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label> <label class="label"
<input class="input" type="text" v-model="record.legal_code" /> >{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label
<p class="help is-danger" v-if="errors.legal_code">{{ errors.legal_code }}</p> >
<input
class="input"
type="text"
v-model="record.legal_code"
/>
<p
class="help is-danger"
v-if="errors.legal_code"
>
{{ errors.legal_code }}
</p>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label> <label class="label"
<Datepicker v-bind="{ record: record, attr: 'issued_date', maxdate: new Date() }" @date="selected('issued_date', $event)"></Datepicker> >{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p> >
<Datepicker
v-bind="{
record: record,
attr: 'issued_date',
maxdate: new Date(),
}"
@date="selected('issued_date', $event)"
></Datepicker>
<p
class="help is-danger"
v-if="errors.issued_date"
>
{{ errors.issued_date }}
</p>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("issued_place")[lang] }}</label> <label class="label">{{ dataLang && findFieldName("issued_place")[lang] }}</label>
<SearchBox <SearchBox
v-bind="{ api: 'issuedplace', field: 'name', column: ['name'], first: true, position: 'is-bottom-right', optionid: record.issued_place,filter: {id__in: isIndividual ? [2, 3] : [4, 5] } }" v-bind="{
@option="selected('issued_place', $event)"></SearchBox> api: 'issuedplace',
field: 'name',
column: ['name'],
first: true,
position: 'is-bottom-right',
optionid: record.issued_place,
filter: { id__in: isIndividual ? [2, 3] : [4, 5] },
}"
@option="selected('issued_place', $event)"
></SearchBox>
</div> </div>
</div> </div>
</div> </div>
<div class="columns is-multiline" v-if="isIndividual"> <div
class="columns is-multiline"
v-if="isIndividual"
>
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("gender")[lang] }}</label> <label class="label">{{ dataLang && findFieldName("gender")[lang] }}</label>
<SearchBox v-bind="{ vdata: store.sex, api: 'sex', field: isVietnamese ? 'name' : 'en', column: ['name', 'en'], first: true, optionid: individualData.sex }" @option="selectedIndividual('sex', $event)"></SearchBox> <SearchBox
v-bind="{
vdata: store.sex,
api: 'sex',
field: isVietnamese ? 'name' : 'en',
column: ['name', 'en'],
first: true,
optionid: individualData.sex,
}"
@option="selectedIndividual('sex', $event)"
></SearchBox>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("birth_date")[lang] }}</label> <label class="label">{{ dataLang && findFieldName("birth_date")[lang] }}</label>
<Datepicker v-bind="{ record: individualData, attr: 'dob', maxdate: new Date() }" @date="selectedIndividual('dob', $event)"></Datepicker> <Datepicker
<p class="help is-danger" v-if="errors.dob">{{ errors.dob }}</p> v-bind="{
record: individualData,
attr: 'dob',
maxdate: new Date(),
}"
@date="selectedIndividual('dob', $event)"
></Datepicker>
<p
class="help is-danger"
v-if="errors.dob"
>
{{ errors.dob }}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -152,19 +277,33 @@
<div class="column is-12"> <div class="column is-12">
<div class="field"> <div class="field">
<label class="label">{{ dataLang && findFieldName("note")[lang] }}</label> <label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
<textarea class="textarea" v-model="record.note" rows="2"></textarea> <textarea
class="textarea"
v-model="record.note"
rows="2"
></textarea>
</div> </div>
</div> </div>
</div> </div>
<div class="mt-5 mb-4"> <div class="mt-5 mb-4">
<h4 class="title is-6 has-text-warning">{{ isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật' }}</h4> <h4 class="title is-6 has-text-warning">
{{ isIndividual ? "Người liên quan" : "Người đại diện pháp luật" }}
</h4>
</div> </div>
<div class="columns is-multiline mb-0 is-2" v-for="(v, i) in localPeople" :key="i"> <div
class="columns is-multiline mb-0 is-2"
v-for="(v, i) in localPeople"
:key="i"
>
<div class="column"> <div class="column">
<label class="label" v-if="i === 0">{{ findFieldName("select")[lang] }}</label> <label
<SearchBox class="label"
v-bind="{ v-if="i === 0"
>{{ findFieldName("select")[lang] }}</label
>
<SearchBox
v-bind="{
api: 'people', api: 'people',
field: 'label', field: 'label',
column: ['code', 'fullname', 'phone'], column: ['code', 'fullname', 'phone'],
@@ -172,34 +311,54 @@
optionid: v.people, optionid: v.people,
position: 'is-top-left', position: 'is-top-left',
addon: peopleAddon, addon: peopleAddon,
viewaddon: peopleviewAddon viewaddon: peopleviewAddon,
}" }"
@option="selectPeople($event, v, i)"></SearchBox> @option="selectPeople($event, v, i)"
></SearchBox>
</div> </div>
<div class="column is-4"> <div class="column is-4">
<label class="label" v-if="i === 0">{{ isIndividual ? 'Quan hệ' : 'Chức vụ' }}</label> <label
<SearchBox class="label"
v-bind="{ v-if="i === 0"
>{{ isIndividual ? "Quan hệ" : "Chức vụ" }}</label
>
<SearchBox
v-bind="{
api: 'relation', api: 'relation',
field: store.lang === 'en' ? 'en' : 'name', field: store.lang === 'en' ? 'en' : 'name',
column: ['code', 'name', 'en'], column: ['code', 'name', 'en'],
first: true, first: true,
optionid: v.relation, optionid: v.relation,
position: 'is-top-left', position: 'is-top-left',
filter:{ id__in: isIndividual ? [1,2,3,4,5,6,7,8] : [9,10,11,12] } filter: {
}" id__in: isIndividual ? [1, 2, 3, 4, 5, 6, 7, 8] : [9, 10, 11, 12],
},
}"
@option="selectRelation($event, v, i)" @option="selectRelation($event, v, i)"
/> />
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<label class="label" v-if="i === 0">&nbsp;</label> <label
<div class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small" style="height: 40px"> class="label"
<button class="button is-dark" @click="add()"> v-if="i === 0"
>&nbsp;</label
>
<div
class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small"
style="height: 40px"
>
<button
class="button is-dark"
@click="add()"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
</span> </span>
</button> </button>
<button class="button is-dark" @click="remove(v, i)"> <button
class="button is-dark"
@click="remove(v, i)"
>
<span class="icon"> <span class="icon">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
</span> </span>
@@ -209,11 +368,25 @@
</div> </div>
<div class="mt-5 buttons is-right"> <div class="mt-5 buttons is-right">
<button class="button" @click="emit('close')">{{ isVietnamese ? 'Hủy' : 'Cancel' }}</button> <button
<button class="button is-primary" @click="update()">{{ isVietnamese ? 'Lưu lại' : 'Save' }}</button> class="button"
@click="emit('close')"
>
{{ isVietnamese ? "Hủy" : "Cancel" }}
</button>
<button
class="button is-primary"
@click="update()"
>
{{ isVietnamese ? "Lưu lại" : "Save" }}
</button>
</div> </div>
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div> </div>
</template> </template>
</template> </template>
@@ -226,7 +399,7 @@ import InputEmail from "~/components/common/InputEmail";
import SearchBox from "~/components/SearchBox"; import SearchBox from "~/components/SearchBox";
import Datepicker from "~/components/datepicker/Datepicker"; import Datepicker from "~/components/datepicker/Datepicker";
import { useStore } from "~/stores/index"; import { useStore } from "~/stores/index";
import { isEqual, pick } from 'es-toolkit'; import { isEqual, pick } from "es-toolkit";
const emit = defineEmits(["close", "update", "modalevent"]); const emit = defineEmits(["close", "update", "modalevent"]);
const { $getdata, $patchapi, $insertapi, $deleteapi, $empty, $errPhone, $resetNull, $snackbar, $copy } = useNuxtApp(); const { $getdata, $patchapi, $insertapi, $deleteapi, $empty, $errPhone, $resetNull, $snackbar, $copy } = useNuxtApp();
@@ -260,21 +433,32 @@ const isOrganization = computed(() => selectedCustomerType.value === 2);
const filteredLegalTypes = computed(() => { const filteredLegalTypes = computed(() => {
if (!store.legaltype) return []; if (!store.legaltype) return [];
return isOrganization.value ? store.legaltype.filter(lt => lt.id === 4) : store.legaltype.filter(lt => lt.id !== 4); return isOrganization.value
? store.legaltype.filter((lt) => lt.id === 4)
: store.legaltype.filter((lt) => lt.id !== 4);
}); });
const peopleAddon = { const peopleAddon = {
component: "people/People", component: "people/People",
width: "65%", width: "65%",
height: "500px", height: "500px",
title: store.lang === "en" ? "Related person" : "Người liên quan" title: store.lang === "en" ? "Related person" : "Người liên quan",
}; };
const peopleviewAddon = { ...peopleAddon }; const peopleviewAddon = { ...peopleAddon };
function selectCustomerType(type) { function selectCustomerType(type) {
const typeNum = Number(type); const typeNum = Number(type);
selectedCustomerType.value = typeNum; selectedCustomerType.value = typeNum;
record.value = { fullname: "", phone: "", email: "", country: 1, type: typeNum, legal_type: typeNum === 2 ? 4 : null, creator: store.login.id, updater: store.login.id }; record.value = {
fullname: "",
phone: "",
email: "",
country: 1,
type: typeNum,
legal_type: typeNum === 2 ? 4 : null,
creator: store.login.id,
updater: store.login.id,
};
if (typeNum === 1) { if (typeNum === 1) {
individualData.value = { dob: null, sex: 1 }; individualData.value = { dob: null, sex: 1 };
organizationData.value = {}; organizationData.value = {};
@@ -287,40 +471,61 @@ function selectCustomerType(type) {
} }
function findFieldName(code) { function findFieldName(code) {
return dataLang.value.find(item => item.code === code) || { vi: code, en: code }; return dataLang.value.find((item) => item.code === code) || { vi: code, en: code };
} }
function showCustomer() { function showCustomer() {
showmodal.value = { component: "customer/CustomerView", width: "60%", height: "600px", title: "Khách hàng", vbind: { row: existedCustomer.value } }; showmodal.value = {
component: "customer/CustomerView",
width: "60%",
height: "600px",
title: "Khách hàng",
vbind: { row: existedCustomer.value },
};
} }
const selected = (f, v) => { record.value[f] = (v && typeof v === 'object') ? v.id : v; if (errors.value[f]) delete errors.value[f]; }; const selected = (f, v) => {
const selectedIndividual = (f, v) => { individualData.value[f] = (v && typeof v === 'object') ? v.id : v; }; record.value[f] = v && typeof v === "object" ? v.id : v;
const selectedOrg = (f, v) => { organizationData.value[f] = (v && typeof v === 'object') ? v.id : v; }; if (errors.value[f]) delete errors.value[f];
const selectPeople = (opt, _v, i) => { };
localPeople.value[i].people = opt.id; const selectedIndividual = (f, v) => {
individualData.value[f] = v && typeof v === "object" ? v.id : v;
};
const selectedOrg = (f, v) => {
organizationData.value[f] = v && typeof v === "object" ? v.id : v;
};
const selectPeople = (opt, _v, i) => {
localPeople.value[i].people = opt.id;
};
const selectRelation = (opt, _v, i) => {
localPeople.value[i].relation = opt ? opt.id : null;
}; };
const selectRelation = (opt, _v, i) => { localPeople.value[i].relation = opt ? opt.id : null; };
const add = () => localPeople.value.push({}); const add = () => localPeople.value.push({});
const remove = (_v, i) => { const remove = (_v, i) => {
localPeople.value.splice(i, 1); localPeople.value.splice(i, 1);
if (localPeople.value.length === 0) localPeople.value = [{}]; if (localPeople.value.length === 0) localPeople.value = [{}];
}; };
watch(people, (val) => { watch(
localPeople.value = val.map(cp => pick(cp, ['id', 'people', 'relation'])); people,
}, { deep: true }) (val) => {
localPeople.value = val.map((cp) => pick(cp, ["id", "people", "relation"]));
},
{ deep: true },
);
function checkError() { function checkError() {
errors.value = {}; errors.value = {};
if ($empty(record.value.fullname)) errors.value.fullname = isVietnamese.value ? "Họ tên không được bỏ trống" : "Full name is required"; if ($empty(record.value.fullname))
errors.value.fullname = isVietnamese.value ? "Họ tên không được bỏ trống" : "Full name is required";
if ($empty(record.value.phone)) { if ($empty(record.value.phone)) {
errors.value.phone = isVietnamese.value ? "Số điện thoại không được bỏ trống" : "Phone is required"; errors.value.phone = isVietnamese.value ? "Số điện thoại không được bỏ trống" : "Phone is required";
} else { } else {
const text = $errPhone(record.value.phone); const text = $errPhone(record.value.phone);
if (text) errors.value.phone = text; if (text) errors.value.phone = text;
} }
if ($empty(record.value.legal_code)) errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code is required"; if ($empty(record.value.legal_code))
errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code is required";
if ($empty(record.value.issued_date)) errors.value.issued_date = "Ngày cấp không được bỏ trống"; if ($empty(record.value.issued_date)) errors.value.issued_date = "Ngày cấp không được bỏ trống";
return Object.keys(errors.value).length > 0; return Object.keys(errors.value).length > 0;
@@ -349,7 +554,10 @@ async function update() {
} }
if (record.value.legal_code) { if (record.value.legal_code) {
const legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true); const legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true);
if (legalCheck) { errors.value.legal_code = "Số CMND/CCCD đã tồn tại."; return; } if (legalCheck) {
errors.value.legal_code = "Số CMND/CCCD đã tồn tại.";
return;
}
} }
} }
@@ -358,7 +566,9 @@ async function update() {
customerData.updater = store.login.id; customerData.updater = store.login.id;
customerData.update_time = new Date(); customerData.update_time = new Date();
let res = isNewCustomer.value ? await $insertapi("customer", customerData, undefined, false) : await $patchapi("customer", customerData, undefined, false); let res = isNewCustomer.value
? await $insertapi("customer", customerData, undefined, false)
: await $patchapi("customer", customerData, undefined, false);
if (!res || res === "error") return; if (!res || res === "error") return;
const customerId = res.id; const customerId = res.id;
@@ -367,7 +577,8 @@ async function update() {
if (isIndividual.value) { if (isIndividual.value) {
let indPayload = $resetNull({ ...individualData.value }); let indPayload = $resetNull({ ...individualData.value });
indPayload.customer = customerId; indPayload.customer = customerId;
if (individualData.value.id) await $patchapi("individual", { ...indPayload, id: individualData.value.id }, undefined, false); if (individualData.value.id)
await $patchapi("individual", { ...indPayload, id: individualData.value.id }, undefined, false);
else await $insertapi("individual", indPayload, undefined, false); else await $insertapi("individual", indPayload, undefined, false);
} else if (isOrganization.value) { } else if (isOrganization.value) {
let orgPayload = $resetNull({ ...organizationData.value }); let orgPayload = $resetNull({ ...organizationData.value });
@@ -393,22 +604,22 @@ async function update() {
commonPayload = { organization: organizationId }; commonPayload = { organization: organizationId };
} }
const validLocalPeople = localPeople.value.filter(lp => lp.people && lp.relation).map(lp => toRaw(lp)); const validLocalPeople = localPeople.value.filter((lp) => lp.people && lp.relation).map((lp) => toRaw(lp));
const peopleKeys = people.value.map(p => pick(p, ['id', 'people', 'relation'])); const peopleKeys = people.value.map((p) => pick(p, ["id", "people", "relation"]));
// 1. check existing ids, if people or relation changes -> patch // 1. check existing ids, if people or relation changes -> patch
const existingLocalPeople = validLocalPeople.filter(cp => Boolean(cp.id)); const existingLocalPeople = validLocalPeople.filter((cp) => Boolean(cp.id));
existingLocalPeople.forEach(lp => { existingLocalPeople.forEach((lp) => {
const match = peopleKeys.find(p => isEqual(p, lp)); const match = peopleKeys.find((p) => isEqual(p, lp));
const payload = { ...lp, ...commonPayload } const payload = { ...lp, ...commonPayload };
if (!match) { if (!match) {
$patchapi(apiName, payload); $patchapi(apiName, payload);
} }
}); });
// 2. if localPeople has and people doesn't -> insert // 2. if localPeople has and people doesn't -> insert
validLocalPeople.forEach(lp => { validLocalPeople.forEach((lp) => {
if (!lp.id) { if (!lp.id) {
const payload = { ...lp, ...commonPayload }; const payload = { ...lp, ...commonPayload };
$insertapi(apiName, payload); $insertapi(apiName, payload);
@@ -417,8 +628,8 @@ async function update() {
// 3. if people has and localPeople doesn't -> delete // 3. if people has and localPeople doesn't -> delete
if (peopleKeys.length !== 0 && validLocalPeople.length !== 0) { if (peopleKeys.length !== 0 && validLocalPeople.length !== 0) {
peopleKeys.forEach(cp => { peopleKeys.forEach((cp) => {
const match = validLocalPeople.find(lp => cp.id === lp.id); const match = validLocalPeople.find((lp) => cp.id === lp.id);
if (!match) { if (!match) {
$deleteapi(apiName, cp.id); $deleteapi(apiName, cp.id);
} }
@@ -427,17 +638,24 @@ async function update() {
// Ảnh // Ảnh
if (record.value.image && record.value.image.length > 0) { if (record.value.image && record.value.image.length > 0) {
await $insertapi("customerfile", record.value.image.map(v => ({ ref: customerId, file: v })), undefined, false); await $insertapi(
"customerfile",
record.value.image.map((v) => ({ ref: customerId, file: v })),
undefined,
false,
);
} }
const completeData = await $getdata("customer", { id: customerId }, undefined, true); const completeData = await $getdata("customer", { id: customerId }, undefined, true);
$snackbar(`Khách hàng đã được ${isNewCustomer.value ? "khởi tạo" : "cập nhật"} thành công`, "Thành công"); $snackbar(`Khách hàng đã được ${isNewCustomer.value ? "khởi tạo" : "cập nhật"} thành công`, "Thành công");
emit("modalevent", { name: "dataevent", data: completeData }); emit("modalevent", { name: "dataevent", data: completeData });
emit("update", completeData); emit("update", completeData);
setTimeout(() => emit("close"), 100); setTimeout(() => emit("close"), 100);
} catch (e) { console.error(e); } } catch (e) {
console.error(e);
}
} }
async function initData() { async function initData() {
@@ -465,7 +683,12 @@ async function initData() {
const copyData = $copy(props.application); const copyData = $copy(props.application);
const type = props.customerType || copyData.type || 1; const type = props.customerType || copyData.type || 1;
selectCustomerType(type); selectCustomerType(type);
record.value = { ...record.value, ...copyData, id: undefined, code: undefined }; record.value = {
...record.value,
...copyData,
id: undefined,
code: undefined,
};
individualData.value = { ...individualData.value, ...copyData }; individualData.value = { ...individualData.value, ...copyData };
} else if (props.customerType) { } else if (props.customerType) {
selectCustomerType(props.customerType); selectCustomerType(props.customerType);
@@ -473,4 +696,4 @@ async function initData() {
} }
onMounted(() => initData()); onMounted(() => initData());
</script> </script>

View File

@@ -28,9 +28,7 @@
> >
<div class="has-text-centered"> <div class="has-text-centered">
<div class=""> <div class="">
<SvgIcon <SvgIcon v-bind="{ name: 'building.svg', type: 'black', size: 40 }"></SvgIcon>
v-bind="{ name: 'building.svg', type: 'black', size: 40 }"
></SvgIcon>
</div> </div>
<div class="title is-6 mb-0"> <div class="title is-6 mb-0">
{{ isVietnamese ? "Doanh nghiệp" : "Company" }} {{ isVietnamese ? "Doanh nghiệp" : "Company" }}

View File

@@ -1,5 +1,8 @@
<template> <template>
<div :id="docid" v-if="record"> <div
:id="docid"
v-if="record"
>
<div> <div>
<Caption v-bind="{ title: this.data && findFieldName('info')[this.lang] }"></Caption> <Caption v-bind="{ title: this.data && findFieldName('info')[this.lang] }"></Caption>
<div class="columns is-multiline mx-0"> <div class="columns is-multiline mx-0">
@@ -7,7 +10,11 @@
<div class="field"> <div class="field">
<label class="label"> khách hàng</label> <label class="label"> khách hàng</label>
<div class="control"> <div class="control">
<span class="hyperlink" @click="$copyToClipboard(record.code)">{{ record.code }}</span> <span
class="hyperlink"
@click="$copyToClipboard(record.code)"
>{{ record.code }}</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -23,14 +30,21 @@
<div class="field"> <div class="field">
<label class="label">Điện thoại</label> <label class="label">Điện thoại</label>
<div class="control"> <div class="control">
<span class="hyperlink" @click="openPhone()">{{ record.phone }}</span> <span
class="hyperlink"
@click="openPhone()"
>{{ record.phone }}</span
>
</div> </div>
</div> </div>
</div> </div>
<div class="column is-3 pb-1 px-0"> <div class="column is-3 pb-1 px-0">
<div class="field"> <div class="field">
<label class="label">Email</label> <label class="label">Email</label>
<div class="control" style="word-break: break-all"> <div
class="control"
style="word-break: break-all"
>
{{ record.email || "/" }} {{ record.email || "/" }}
</div> </div>
</div> </div>
@@ -103,7 +117,11 @@
<div class="field"> <div class="field">
<label class="label">Người tạo</label> <label class="label">Người tạo</label>
<div class="control"> <div class="control">
<span class="hyperlink" @click="openUser(record.creator)">{{ record.creator__fullname || "/" }}</span> <span
class="hyperlink"
@click="openUser(record.creator)"
>{{ record.creator__fullname || "/" }}</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -122,7 +140,11 @@
<div class="field"> <div class="field">
<label class="label">Người cập nhật</label> <label class="label">Người cập nhật</label>
<div class="control"> <div class="control">
<span class="hyperlink" @click="openUser(record.updater)">{{ record.updater__fullname || "/" }}</span> <span
class="hyperlink"
@click="openUser(record.updater)"
>{{ record.updater__fullname || "/" }}</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -131,7 +153,7 @@
<label class="label">Thời gian cập nhật</label> <label class="label">Thời gian cập nhật</label>
<div class="control"> <div class="control">
<span> <span>
{{ record.update_time ? $dayjs(record.update_time).format("DD/MM/YYYY HH:mm") : '/' }} {{ record.update_time ? $dayjs(record.update_time).format("DD/MM/YYYY HH:mm") : "/" }}
</span> </span>
</div> </div>
</div> </div>
@@ -143,58 +165,137 @@
<ImageGallery v-bind="{ row: record, api: 'customerfile', hideopt: true }"></ImageGallery> <ImageGallery v-bind="{ row: record, api: 'customerfile', hideopt: true }"></ImageGallery>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<Caption v-bind="{ title: this.isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật' }"></Caption> <Caption
v-bind="{
title: this.isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật',
}"
></Caption>
<div class="mt-2"> <div class="mt-2">
<div <div
v-if="this.relatedPeople && this.relatedPeople.length > 0" v-if="this.relatedPeople && this.relatedPeople.length > 0"
v-for="relatedPerson in this.relatedPeople" v-for="relatedPerson in this.relatedPeople"
class="columns is-0 mb-2" class="columns is-0 mb-2"
> >
<span class="column is-2">{{ relatedPerson.people__code }}</span> <span class="column is-2">{{ relatedPerson.people__code }}</span>
<span class="column is-4 "> <span class="column is-4">
<span class="has-text-primary hyperlink" <span
class="has-text-primary hyperlink"
@click="openRelatedPerson(relatedPerson.people)" @click="openRelatedPerson(relatedPerson.people)"
>{{ relatedPerson.people__fullname }}</span> >{{ relatedPerson.people__fullname }}</span
>
<span> ({{ relatedPerson.relation__name }})</span> <span> ({{ relatedPerson.relation__name }})</span>
</span> </span>
<span class="column is-4">{{ relatedPerson.people__phone }}</span> <span class="column is-4">{{ relatedPerson.people__phone }}</span>
</div> </div>
<div v-else class="has-text-grey"> <div
Chưa {{ this.isIndividual ? 'người liên quan' : 'người đại diện pháp luật' }} v-else
class="has-text-grey"
>
Chưa có
{{ this.isIndividual ? "người liên quan" : "người đại diện pháp luật" }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="record.count_product >0" class="mt-3"> <div
<Caption class="mb-2" v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"></Caption> v-if="record.count_product > 0"
<DataView v-bind="{ class="mt-3"
setting: 'customer-all-transaction', >
pagename: this.$id(), <Caption
api: 'customer', class="mb-2"
params: { v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"
filter: { id: this.row.customer || this.row.id }, ></Caption>
/* copied from 02-connection.js */ <DataView
values: v-bind="{
'id,update_time,creator,creator__fullname,country,country__name,country__en,issued_date,issued_place,issued_place__name,code,email,fullname,legal_code,phone,legal_type,legal_type__name,address,contact_address,note,type,type__name,updater,updater__fullname,create_time,update_time', setting: 'customer-all-transaction',
distinct_values: { pagename: this.$id(),
label: { type: 'Concat', field: ['code', 'fullname', 'phone', 'legal_code'] }, api: 'customer',
order: { type: 'RowNumber' }, params: {
image_count: { type: 'Count', field: 'id', subquery: { model: 'Customer_File', column: 'ref' } }, filter: { id: this.row.customer || this.row.id },
count_note: { type: 'Count', field: 'id', subquery: { model: 'Customer_Note', column: 'ref' } }, /* copied from 02-connection.js */
count_product: { type: 'Count', field: 'id', subquery: { model: 'Product_Booked', column: 'transaction__customer' } }, values:
sum_product: { type: 'Sum', field: 'transaction__sale_price', subquery: { model: 'Product_Booked', column: 'transaction__customer' } }, 'id,update_time,creator,creator__fullname,country,country__name,country__en,issued_date,issued_place,issued_place__name,code,email,fullname,legal_code,phone,legal_type,legal_type__name,address,contact_address,note,type,type__name,updater,updater__fullname,create_time,update_time',
sum_receiver: { type: 'Sum', field: 'transaction__amount_received', subquery: { model: 'Product_Booked', column: 'transaction__customer' } }, distinct_values: {
sum_remain: { type: 'Sum', field: 'transaction__amount_remain', subquery: { model: 'Product_Booked', column: 'transaction__customer' } } label: {
type: 'Concat',
field: ['code', 'fullname', 'phone', 'legal_code'],
},
order: { type: 'RowNumber' },
image_count: {
type: 'Count',
field: 'id',
subquery: { model: 'Customer_File', column: 'ref' },
},
count_note: {
type: 'Count',
field: 'id',
subquery: { model: 'Customer_Note', column: 'ref' },
},
count_product: {
type: 'Count',
field: 'id',
subquery: {
model: 'Product_Booked',
column: 'transaction__customer',
},
},
sum_product: {
type: 'Sum',
field: 'transaction__sale_price',
subquery: {
model: 'Product_Booked',
column: 'transaction__customer',
},
},
sum_receiver: {
type: 'Sum',
field: 'transaction__amount_received',
subquery: {
model: 'Product_Booked',
column: 'transaction__customer',
},
},
sum_remain: {
type: 'Sum',
field: 'transaction__amount_remain',
subquery: {
model: 'Product_Booked',
column: 'transaction__customer',
},
},
},
summary: 'annotate',
}, },
summary: 'annotate', }"
}, />
}" />
</div> </div>
<div class="mt-4 border-bottom" id="ignore"></div> <div
<div class="buttons mt-2 is-flex is-gap-1" id="ignore"> class="mt-4 border-bottom"
<button v-if="$getEditRights('edit', { code: 'customer', category: 'topmenu' })" class="button is-primary" @click="edit()">Chỉnh sửa</button> id="ignore"
<button class="button is-light" @click="$exportpdf(docid, record.code)">In thông tin</button> ></div>
<div
class="buttons mt-2 is-flex is-gap-1"
id="ignore"
>
<button
v-if="$getEditRights('edit', { code: 'customer', category: 'topmenu' })"
class="button is-primary"
@click="edit()"
>
Chỉnh sửa
</button>
<button
class="button is-light"
@click="$exportpdf(docid, record.code)"
>
In thông tin
</button>
</div> </div>
<Modal @close="showmodal = undefined" v-bind="showmodal" @dataevent="changeInfo" v-if="showmodal"></Modal> <Modal
@close="showmodal = undefined"
v-bind="showmodal"
@dataevent="changeInfo"
v-if="showmodal"
></Modal>
</div> </div>
</template> </template>
<script> <script>
@@ -225,15 +326,19 @@ export default {
}, },
isIndividual() { isIndividual() {
return this.record.type === 1; return this.record.type === 1;
} },
}, },
async created() { async created() {
this.record = await this.$getdata("customer", { id: this.row.customer || this.row.id }, undefined, true); this.record = await this.$getdata("customer", { id: this.row.customer || this.row.id }, undefined, true);
if (this.isIndividual) { if (this.isIndividual) {
this.relatedPeople = await this.$getdata("customerpeople", { customer: this.row.customer || this.row.id }); this.relatedPeople = await this.$getdata("customerpeople", {
customer: this.row.customer || this.row.id,
});
} else { } else {
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true); const org = await this.$getdata("organization", { customer: this.row.customer || this.row.id }, undefined, true);
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id }); this.relatedPeople = await this.$getdata("legalrep", {
organization: org.id,
});
} }
}, },
methods: { methods: {
@@ -270,10 +375,19 @@ export default {
this.record = this.$copy(v); this.record = this.$copy(v);
// refetch relatedPeople // refetch relatedPeople
if (this.isIndividual) { if (this.isIndividual) {
this.relatedPeople = await this.$getdata("customerpeople", { customer: this.row.customer || this.row.id }); this.relatedPeople = await this.$getdata("customerpeople", {
customer: this.row.customer || this.row.id,
});
} else { } else {
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true); const org = await this.$getdata(
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id }); "organization",
{ customer: this.row.customer || this.row.id },
undefined,
true,
);
this.relatedPeople = await this.$getdata("legalrep", {
organization: org.id,
});
} }
}, },
openUser(userId) { openUser(userId) {
@@ -287,15 +401,15 @@ export default {
}; };
}, },
async openRelatedPerson(peopleId) { async openRelatedPerson(peopleId) {
const peopleRow = await this.$getdata('people', { id: peopleId }, undefined, true); const peopleRow = await this.$getdata("people", { id: peopleId }, undefined, true);
this.showmodal = { this.showmodal = {
component: "people/PeopleView", component: "people/PeopleView",
vbind: { row: peopleRow }, vbind: { row: peopleRow },
title: 'Người liên quan', title: "Người liên quan",
width: '65%', width: "65%",
height: '400px', height: "400px",
} };
} },
}, },
}; };
</script> </script>

View File

@@ -1,17 +1,18 @@
<template> <template>
<div <div
class="fs-13 font-semibold mx-0 px-0 is-flex is-justify-content-center is-align-items-center is-flex-shrink-0 rounded-full size-10" class="fs-13 font-semibold mx-0 px-0 is-flex is-justify-content-center is-align-items-center is-flex-shrink-0 rounded-full size-10"
style="border: 1px solid var(--bulma-grey-80)" style="border: 1px solid var(--bulma-grey-80)"
:style="image && 'border: none'"> :style="image && 'border: none'"
>
<div> <div>
<span>{{text}}</span> <span>{{ text }}</span>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['text', 'image', 'type', 'size'] props: ["text", "image", "type", "size"],
} };
</script> </script>
<style> <style>
.cbox { .cbox {

View File

@@ -1,72 +1,72 @@
<script setup> <script setup>
import DashboardHighlightCard from '@/components/dashboard/DashboardHighlightCard.vue'; import DashboardHighlightCard from "@/components/dashboard/DashboardHighlightCard.vue";
import Delivery from '@/components/dashboard/Delivery.vue'; import Delivery from "@/components/dashboard/Delivery.vue";
import OrderStatus from '@/components/dashboard/OrderStatus.vue'; import OrderStatus from "@/components/dashboard/OrderStatus.vue";
import RevenueChart from '@/components/dashboard/RevenueChart.vue'; import RevenueChart from "@/components/dashboard/RevenueChart.vue";
import TopCustomers from '@/components/dashboard/TopCustomers.vue'; import TopCustomers from "@/components/dashboard/TopCustomers.vue";
import TopProducts from '@/components/dashboard/TopProducts.vue'; import TopProducts from "@/components/dashboard/TopProducts.vue";
import Warnings from '@/components/dashboard/Warnings.vue'; import Warnings from "@/components/dashboard/Warnings.vue";
const highlights = [ const highlights = [
{ {
name: 'Doanh thu hôm nay', name: "Doanh thu hôm nay",
value: '72.5M', value: "72.5M",
color: 'blue', color: "blue",
icon: 'material-symbols:attach-money-rounded', icon: "material-symbols:attach-money-rounded",
subheader: { subheader: {
value: '+12.5%', value: "+12.5%",
fluctuation: 'up', fluctuation: "up",
} },
}, },
{ {
name: 'Số đơn hàng', name: "Số đơn hàng",
value: '73', value: "73",
color: 'purple', color: "purple",
icon: 'material-symbols:shopping-cart-outline-rounded', icon: "material-symbols:shopping-cart-outline-rounded",
subheader: { subheader: {
value: '+8 đơn', value: "+8 đơn",
fluctuation: 'up', fluctuation: "up",
} },
}, },
{ {
name: 'Đơn đang giao', name: "Đơn đang giao",
value: '8', value: "8",
color: 'orange', color: "orange",
icon: 'material-symbols:delivery-truck-speed-outline-rounded', icon: "material-symbols:delivery-truck-speed-outline-rounded",
}, },
{ {
name: 'Đơn hoàn thành', name: "Đơn hoàn thành",
value: '38', value: "38",
color: 'green', color: "green",
icon: 'material-symbols:check-circle-outline-rounded', icon: "material-symbols:check-circle-outline-rounded",
}, },
{ {
name: 'Công nợ phải thu', name: "Công nợ phải thu",
value: '245M', value: "245M",
color: 'red', color: "red",
icon: 'material-symbols:universal-currency-alt-outline-rounded', icon: "material-symbols:universal-currency-alt-outline-rounded",
subheader: { subheader: {
value: '+15M', value: "+15M",
fluctuation: 'down', fluctuation: "down",
} },
}, },
{ {
name: 'Tồn kho', name: "Tồn kho",
value: '1,250', value: "1,250",
color: 'cyan', color: "cyan",
icon: 'material-symbols:box-outline-rounded', icon: "material-symbols:box-outline-rounded",
subheader: { subheader: {
value: '-45 SP', value: "-45 SP",
fluctuation: 'down', fluctuation: "down",
} },
}, },
] ];
</script> </script>
<template> <template>
<div> <div>
<div class="fixed-grid has-2-cols has-3-cols-tablet has-6-cols-desktop"> <div class="fixed-grid has-2-cols has-3-cols-tablet has-6-cols-desktop">
<div class="grid"> <div class="grid">
<DashboardHighlightCard <DashboardHighlightCard
v-for="highlight in highlights" v-for="highlight in highlights"
:key="highlight.name" :key="highlight.name"
v-bind="highlight" v-bind="highlight"
@@ -88,4 +88,4 @@ const highlights = [
<Delivery /> <Delivery />
<Warnings /> <Warnings />
</div> </div>
</template> </template>

View File

@@ -4,8 +4,8 @@ const props = defineProps({
value: String, value: String,
color: String, color: String,
icon: String, icon: String,
subheader: Object subheader: Object,
}) });
</script> </script>
<template> <template>
<div class="cell"> <div class="cell">
@@ -14,14 +14,19 @@ const props = defineProps({
<div> <div>
<p class="fs-14 has-text-grey mb-1">{{ name }}</p> <p class="fs-14 has-text-grey mb-1">{{ name }}</p>
<p class="fsb-26 mb-1 has-text-black">{{ value }}</p> <p class="fsb-26 mb-1 has-text-black">{{ value }}</p>
<div v-if="subheader" <div
v-if="subheader"
:class="[ :class="[
'is-flex is-gap-0.5 is-align-items-center', 'is-flex is-gap-0.5 is-align-items-center',
subheader.fluctuation === 'up' ? 'has-text-green-40' : 'has-text-red-40' subheader.fluctuation === 'up' ? 'has-text-green-40' : 'has-text-red-40',
]" ]"
> >
<Icon <Icon
:name="subheader.fluctuation === 'up' ? 'material-symbols:arrow-upward-rounded' :'material-symbols:arrow-downward-rounded'" :name="
subheader.fluctuation === 'up'
? 'material-symbols:arrow-upward-rounded'
: 'material-symbols:arrow-downward-rounded'
"
color="inherit" color="inherit"
/> />
<p class="fs-14">{{ subheader.value }}</p> <p class="fs-14">{{ subheader.value }}</p>
@@ -30,12 +35,15 @@ const props = defineProps({
<div <div
:class="[ :class="[
'rounded-lg size-12 is-flex-shrink-0 is-flex is-justify-content-center is-align-items-center', 'rounded-lg size-12 is-flex-shrink-0 is-flex is-justify-content-center is-align-items-center',
`has-background-${color}-soft has-text-${color}-40` `has-background-${color}-soft has-text-${color}-40`,
]" ]"
"> >
<Icon :name="icon" :size="28" /> <Icon
:name="icon"
:size="28"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,33 +1,33 @@
<script setup> <script setup>
import DeliveryInteractive from '@/components/dashboard/DeliveryInteractive.vue'; import DeliveryInteractive from "@/components/dashboard/DeliveryInteractive.vue";
import Driver from '@/components/dashboard/Driver.vue'; import Driver from "@/components/dashboard/Driver.vue";
const drivers = [ const drivers = [
{ {
name: 'Nguyễn Văn A', name: "Nguyễn Văn A",
status: 'Đang giao', status: "Đang giao",
deliveries: 3, deliveries: 3,
deliveries_completed: 2, deliveries_completed: 2,
}, },
{ {
name: 'Trần Văn B', name: "Trần Văn B",
status: 'Đang giao', status: "Đang giao",
deliveries: 2, deliveries: 2,
deliveries_completed: 1, deliveries_completed: 1,
}, },
{ {
name: 'Lê Thị C', name: "Lê Thị C",
status: 'Hoàn thành', status: "Hoàn thành",
deliveries: 4, deliveries: 4,
deliveries_completed: 4, deliveries_completed: 4,
}, },
{ {
name: 'Phạm Văn D', name: "Phạm Văn D",
status: 'Đang giao', status: "Đang giao",
deliveries: 1, deliveries: 1,
deliveries_completed: 0, deliveries_completed: 0,
}, },
] ];
</script> </script>
<template> <template>
<div class="card"> <div class="card">
@@ -51,4 +51,4 @@ const drivers = [
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -3,7 +3,47 @@
class="relative w-full has-background-blue-95 rounded-lg is-clipped" class="relative w-full has-background-blue-95 rounded-lg is-clipped"
style="height: 360px" style="height: 360px"
> >
<div class="absolute inset-0 w-full h-full opacity-20"><div class="absolute w-full border-t border-gray-300" style="top: 20%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 40%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 60%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 80%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 100%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 20%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 40%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 60%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 80%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 100%;"></div> <div class="absolute inset-0 w-full h-full opacity-20">
<div
class="absolute w-full border-t border-gray-300"
style="top: 20%"
></div>
<div
class="absolute w-full border-t border-gray-300"
style="top: 40%"
></div>
<div
class="absolute w-full border-t border-gray-300"
style="top: 60%"
></div>
<div
class="absolute w-full border-t border-gray-300"
style="top: 80%"
></div>
<div
class="absolute w-full border-t border-gray-300"
style="top: 100%"
></div>
<div
class="absolute h-full border-l border-gray-300"
style="left: 20%"
></div>
<div
class="absolute h-full border-l border-gray-300"
style="left: 40%"
></div>
<div
class="absolute h-full border-l border-gray-300"
style="left: 60%"
></div>
<div
class="absolute h-full border-l border-gray-300"
style="left: 80%"
></div>
<div
class="absolute h-full border-l border-gray-300"
style="left: 100%"
></div>
</div> </div>
<div> <div>
<div <div
@@ -25,7 +65,11 @@
<path <path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path> ></path>
<circle cx="12" cy="10" r="3"></circle> <circle
cx="12"
cy="10"
r="3"
></circle>
</svg> </svg>
</div> </div>
<div <div
@@ -47,7 +91,11 @@
<path <path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path> ></path>
<circle cx="12" cy="10" r="3"></circle> <circle
cx="12"
cy="10"
r="3"
></circle>
</svg> </svg>
</div> </div>
<div <div
@@ -69,7 +117,11 @@
<path <path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path> ></path>
<circle cx="12" cy="10" r="3"></circle> <circle
cx="12"
cy="10"
r="3"
></circle>
</svg> </svg>
</div> </div>
<div <div
@@ -91,7 +143,11 @@
<path <path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path> ></path>
<circle cx="12" cy="10" r="3"></circle> <circle
cx="12"
cy="10"
r="3"
></circle>
</svg> </svg>
</div> </div>
<div <div
@@ -113,7 +169,11 @@
<path <path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path> ></path>
<circle cx="12" cy="10" r="3"></circle> <circle
cx="12"
cy="10"
r="3"
></circle>
</svg> </svg>
</div> </div>
<div <div
@@ -139,9 +199,7 @@
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon> <polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg> </svg>
</div> </div>
<div <div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
</div> </div>
</div> </div>
<div <div
@@ -167,9 +225,7 @@
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon> <polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg> </svg>
</div> </div>
<div <div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
</div> </div>
</div> </div>
<div <div
@@ -220,9 +276,7 @@
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon> <polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg> </svg>
</div> </div>
<div <div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
</div> </div>
</div> </div>
<div <div
@@ -244,7 +298,12 @@
<path <path
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
></path> ></path>
<circle cx="12" cy="10" r="3"></circle></svg> Nội <circle
cx="12"
cy="10"
r="3"
></circle></svg
> Nội
</div> </div>
</div> </div>
</div> </div>
@@ -314,7 +373,7 @@
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
} }
.animate-ping { .animate-ping {
animation: ping 1s cubic-bezier(0,0,0.2,1) infinite; animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
} }
@keyframes ping { @keyframes ping {
0% { 0% {
@@ -339,67 +398,67 @@
@keyframes move-random-1 { @keyframes move-random-1 {
0% { 0% {
transform: translateX(0) translateY(0) transform: translateX(0) translateY(0);
} }
33% { 33% {
transform: translateX(30px) translateY(24px) transform: translateX(30px) translateY(24px);
} }
66% { 66% {
transform: translateX(60px) translateY(12px) transform: translateX(60px) translateY(12px);
} }
100% { 100% {
transform: translateX(0) translateY(0) transform: translateX(0) translateY(0);
} }
} }
@keyframes move-random-2 { @keyframes move-random-2 {
0% { 0% {
transform: translateX(0) translateY(0) transform: translateX(0) translateY(0);
} }
23% { 23% {
transform: translateX(-20px) translateY(-36px) transform: translateX(-20px) translateY(-36px);
} }
46% { 46% {
transform: translateX(0) translateY(22px) transform: translateX(0) translateY(22px);
} }
75% { 75% {
transform: translateX(30px) translateY(-12px) transform: translateX(30px) translateY(-12px);
} }
100% { 100% {
transform: translateX(0) translateY(0) transform: translateX(0) translateY(0);
} }
} }
@keyframes move-random-3 { @keyframes move-random-3 {
0% { 0% {
transform: translateX(0) translateY(0) transform: translateX(0) translateY(0);
} }
10% { 10% {
transform: translateX(-30px) translateY(-15px) transform: translateX(-30px) translateY(-15px);
} }
30% { 30% {
transform: translateX(-10px) translateY(26px) transform: translateX(-10px) translateY(26px);
} }
50% { 50% {
transform: translateX(48px) translateY(-28px) transform: translateX(48px) translateY(-28px);
} }
80% { 80% {
transform: translateX(17px) translateY(10px) transform: translateX(17px) translateY(10px);
} }
100% { 100% {
transform: translateX(0) translateY(0) transform: translateX(0) translateY(0);
} }
} }
.border-2 { .border-2 {
@@ -442,13 +501,20 @@
opacity: 40%; opacity: 40%;
} }
.shadow { .shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow:
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow:
var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
var(--tw-shadow);
} }
.backdrop-blur { .backdrop-blur {
--tw-backdrop-blur: blur(8px); --tw-backdrop-blur: blur(8px);
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,)
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,)
var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,)
var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,)
var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
} }
.transition-all { .transition-all {
transition-property: all; transition-property: all;
@@ -474,4 +540,4 @@
.border-gray-300 { .border-gray-300 {
border-color: var(--bulma-grey-85); border-color: var(--bulma-grey-85);
} }
</style> </style>

View File

@@ -1,15 +1,15 @@
<script setup> <script setup>
import AvatarBox from '@/components/dashboard/AvatarBox.vue'; import AvatarBox from "@/components/dashboard/AvatarBox.vue";
const props = defineProps({ const props = defineProps({
name: String, name: String,
status: String, status: String,
deliveries: Number, deliveries: Number,
deliveries_completed: Number, deliveries_completed: Number,
}) });
</script> </script>
<template> <template>
<div <div
class="is-flex is-gap-2 fs-14 p-3 rounded-lg" class="is-flex is-gap-2 fs-14 p-3 rounded-lg"
:style="{ :style="{
border: '1px solid var(--bulma-grey-80)', border: '1px solid var(--bulma-grey-80)',
@@ -22,14 +22,13 @@ const props = defineProps({
<span :class="['tag', status === 'Đang giao' ? 'is-warning' : 'is-success']">{{ status }}</span> <span :class="['tag', status === 'Đang giao' ? 'is-warning' : 'is-success']">{{ status }}</span>
</div> </div>
<p class="fs-13 has-text-grey">Đơn: {{ deliveries_completed }}/{{ deliveries }}</p> <p class="fs-13 has-text-grey">Đơn: {{ deliveries_completed }}/{{ deliveries }}</p>
<progress <progress
v-if="deliveries !== deliveries_completed" v-if="deliveries !== deliveries_completed"
class="progress is-small is-primary mt-2" class="progress is-small is-primary mt-2"
style="--bulma-size-small: 0.4rem;" style="--bulma-size-small: 0.4rem"
:value="deliveries_completed" :value="deliveries_completed"
:max="deliveries" :max="deliveries"
> ></progress>
</progress>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,40 +1,40 @@
<script setup> <script setup>
import OrderStatusCard from '@/components/dashboard/OrderStatusCard.vue'; import OrderStatusCard from "@/components/dashboard/OrderStatusCard.vue";
const statuses = [ const statuses = [
{ {
id: 1, id: 1,
code: 'pending', code: "pending",
name: 'Chờ xử lý', name: "Chờ xử lý",
value: 12, value: 12,
color: 'orange', color: "orange",
icon: 'material-symbols:clock-loader-40' icon: "material-symbols:clock-loader-40",
}, },
{ {
id: 2, id: 2,
code: 'delivering', code: "delivering",
name: 'Đang giao', name: "Đang giao",
value: 8, value: 8,
color: 'blue', color: "blue",
icon: 'material-symbols:delivery-truck-speed-outline-rounded' icon: "material-symbols:delivery-truck-speed-outline-rounded",
}, },
{ {
id: 3, id: 3,
code: 'delivered', code: "delivered",
name: 'Đã giao', name: "Đã giao",
value: 15, value: 15,
color: 'purple', color: "purple",
icon: 'material-symbols:bucket-check-outline-rounded' icon: "material-symbols:bucket-check-outline-rounded",
}, },
{ {
id: 4, id: 4,
code: 'completed', code: "completed",
name: 'Hoàn thành', name: "Hoàn thành",
value: 38, value: 38,
color: 'green', color: "green",
icon: 'material-symbols:check-circle-outline-rounded' icon: "material-symbols:check-circle-outline-rounded",
}, },
] ];
</script> </script>
<template> <template>
<div class="card h-full"> <div class="card h-full">
@@ -51,4 +51,4 @@ const statuses = [
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -6,18 +6,16 @@ const props = defineProps({
value: Number, value: Number,
icon: String, icon: String,
color: String, color: String,
}) });
</script> </script>
<template> <template>
<div class="cell"> <div class="cell">
<div class="card" :style="{ border: `1px solid var(--bulma-${color}-70)` }"> <div
class="card"
:style="{ border: `1px solid var(--bulma-${color}-70)` }"
>
<div class="card-content is-flex is-flex-direction-column is-align-items-center is-gap-1"> <div class="card-content is-flex is-flex-direction-column is-align-items-center is-gap-1">
<div <div :class="['p-3 is-flex rounded-full', `has-background-${color}-90`, `has-text-${color}-40`]">
:class="[
'p-3 is-flex rounded-full',
`has-background-${color}-90`,
`has-text-${color}-40`,
]" >
<Icon <Icon
:name="icon" :name="icon"
:size="24" :size="24"
@@ -30,4 +28,4 @@ const props = defineProps({
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -2,7 +2,7 @@
const { $shortenCurrency } = useNuxtApp(); const { $shortenCurrency } = useNuxtApp();
const revenueChartOptions = { const revenueChartOptions = {
chart: { chart: {
type: 'spline' type: "spline",
}, },
credits: { credits: {
enabled: false, enabled: false,
@@ -11,38 +11,38 @@ const revenueChartOptions = {
text: null, text: null,
}, },
xAxis: { xAxis: {
categories: [ categories: ["10/4", "11/4", "12/4", "13/4", "14/4", "15/4", "16/4", "17/4", "18/4"],
'10/4', '11/4', '12/4', '13/4', '14/4', '15/4', '16/4', '17/4', '18/4'
],
accessibility: { accessibility: {
description: 'Dates' description: "Dates",
} },
}, },
yAxis: { yAxis: {
title: { title: {
text: 'Doanh thu' text: "Doanh thu",
}, },
labels: { labels: {
format: '{value}' format: "{value}",
} },
}, },
tooltip: { tooltip: {
crosshairs: true, crosshairs: true,
shared: true, shared: true,
valueSuffix: ' VNĐ', valueSuffix: " VNĐ",
}, },
plotOptions: { plotOptions: {
spline: { spline: {
marker: { marker: {
enabled: false enabled: false,
} },
} },
}, },
series: [{ series: [
name: 'Doanh thu', {
data: [45000000, 52000000, 48000000, 51000000, 58000000, 61000000, 67500000, 72000000, 69000000], name: "Doanh thu",
showInLegend: false, data: [45000000, 52000000, 48000000, 51000000, 58000000, 61000000, 67500000, 72000000, 69000000],
}] showInLegend: false,
},
],
}; };
</script> </script>
<template> <template>
@@ -58,4 +58,4 @@ const revenueChartOptions = {
<highcharts :options="revenueChartOptions" /> <highcharts :options="revenueChartOptions" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,11 +1,11 @@
<script setup> <script setup>
import AvatarBox from '@/components/dashboard/AvatarBox.vue'; import AvatarBox from "@/components/dashboard/AvatarBox.vue";
const props = defineProps({ const props = defineProps({
name: String, name: String,
order_count: Number, order_count: Number,
paid: Number, paid: Number,
}) });
const { $shortenCurrency } = useNuxtApp(); const { $shortenCurrency } = useNuxtApp();
</script> </script>
@@ -20,4 +20,4 @@ const { $shortenCurrency } = useNuxtApp();
</div> </div>
<p class="font-semibold">{{ $shortenCurrency(paid) }}</p> <p class="font-semibold">{{ $shortenCurrency(paid) }}</p>
</div> </div>
</template> </template>

View File

@@ -1,35 +1,33 @@
<script setup> <script setup>
import TopCustomer from '@/components/dashboard/TopCustomer.vue'; import TopCustomer from "@/components/dashboard/TopCustomer.vue";
const customers = [ const customers = [
{ {
name: 'Công ty TNHH ABC', name: "Công ty TNHH ABC",
order_count: 45, order_count: 45,
paid: 125000000, paid: 125000000,
}, },
{ {
name: 'Siêu thị XYZ', name: "Siêu thị XYZ",
order_count: 38, order_count: 38,
paid: 98000000, paid: 98000000,
}, },
{ {
name: 'Nhà hàng Đông Dương', name: "Nhà hàng Đông Dương",
order_count: 32, order_count: 32,
paid: 87000000, paid: 87000000,
}, },
{ {
name: 'Khách sạn Mường Thanh', name: "Khách sạn Mường Thanh",
order_count: 28, order_count: 28,
paid: 76000000, paid: 76000000,
}, },
{ {
name: 'Cửa hàng Bách Hoá', name: "Cửa hàng Bách Hoá",
order_count: 24, order_count: 24,
paid: 64000000, paid: 64000000,
}, },
];
]
</script> </script>
<template> <template>
<div class="card"> <div class="card">
@@ -44,4 +42,4 @@ const customers = [
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -17,7 +17,11 @@ const { $shortenCurrency } = useNuxtApp();
</div> </div>
<p class="font-semibold">{{ $shortenCurrency(revenue) }}</p> <p class="font-semibold">{{ $shortenCurrency(revenue) }}</p>
</div> </div>
<progress class="progress is-small is-primary" :value="revenue" :max="topRevenue"> <progress
class="progress is-small is-primary"
:value="revenue"
:max="topRevenue"
>
15% 15%
</progress> </progress>
</div> </div>
@@ -26,4 +30,4 @@ const { $shortenCurrency } = useNuxtApp();
.progress { .progress {
--bulma-size-small: 0.5rem; --bulma-size-small: 0.5rem;
} }
</style> </style>

View File

@@ -1,33 +1,33 @@
<script setup> <script setup>
import TopProduct from '@/components/dashboard/TopProduct.vue'; import TopProduct from "@/components/dashboard/TopProduct.vue";
const products = [ const products = [
{ {
name: 'Gạo ST25 - Bao 5kg', name: "Gạo ST25 - Bao 5kg",
sold_count: 1250, sold_count: 1250,
revenue: 156000000 revenue: 156000000,
}, },
{ {
name: 'Nước mắm Phú Quốc - Chai 500ml', name: "Nước mắm Phú Quốc - Chai 500ml",
sold_count: 980, sold_count: 980,
revenue: 132000000 revenue: 132000000,
}, },
{ {
name: 'Đường tinh luyện - Bao 1kg', name: "Đường tinh luyện - Bao 1kg",
sold_count: 856, sold_count: 856,
revenue: 98000000 revenue: 98000000,
}, },
{ {
name: 'Dầu ăn Neptune - Chai 1L', name: "Dầu ăn Neptune - Chai 1L",
sold_count: 742, sold_count: 742,
revenue: 87000000 revenue: 87000000,
}, },
{ {
name: 'Bột mì đa dụng - Bao 1kg', name: "Bột mì đa dụng - Bao 1kg",
sold_count: 623, sold_count: 623,
revenue: 72000000 revenue: 72000000,
}, },
] ];
</script> </script>
<template> <template>
<div class="card"> <div class="card">
@@ -35,14 +35,14 @@ const products = [
<p class="fs-17 font-semibold mb-4">Top sản phẩm</p> <p class="fs-17 font-semibold mb-4">Top sản phẩm</p>
<div class="is-flex is-flex-direction-column is-gap-2"> <div class="is-flex is-flex-direction-column is-gap-2">
<TopProduct <TopProduct
v-for="product in products" v-for="product in products"
:key="product.name" :key="product.name"
v-bind="{ v-bind="{
...product, ...product,
topRevenue: products[0].revenue topRevenue: products[0].revenue,
}" }"
/> />
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -3,17 +3,16 @@ const props = defineProps({
name: String, name: String,
details: String, details: String,
level: Number, level: Number,
type: String type: String,
}) });
const color = computed(() => { const color = computed(() => {
if (props.level === 1) return 'yellow'; if (props.level === 1) return "yellow";
if (props.level === 2) return 'orange'; if (props.level === 2) return "orange";
if (props.level === 3) return 'red'; if (props.level === 3) return "red";
}) });
</script> </script>
<template> <template>
<div <div
:class="['card m-0', `has-background-${color}-95`]" :class="['card m-0', `has-background-${color}-95`]"
@@ -22,7 +21,7 @@ const color = computed(() => {
}" }"
> >
<div class="card-content p-4 is-flex is-align-items-center is-gap-2"> <div class="card-content p-4 is-flex is-align-items-center is-gap-2">
<Icon <Icon
:name="type === 'time' ? 'material-symbols:clock-loader-40' : 'material-symbols:box-outline-rounded'" :name="type === 'time' ? 'material-symbols:clock-loader-40' : 'material-symbols:box-outline-rounded'"
:size="20" :size="20"
:class="`has-text-${color}-45`" :class="`has-text-${color}-45`"
@@ -33,4 +32,4 @@ const color = computed(() => {
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,26 +1,26 @@
<script setup> <script setup>
import Warning from '@/components/dashboard/Warning.vue'; import Warning from "@/components/dashboard/Warning.vue";
const warnings = [ const warnings = [
{ {
name: 'Công nợ sắp đến hạn', name: "Công nợ sắp đến hạn",
details: 'Công ty TNHH ABC - 35.000.000đ - Hạn: 25/03/2026', details: "Công ty TNHH ABC - 35.000.000đ - Hạn: 25/03/2026",
level: 1, level: 1,
type: 'time', type: "time",
}, },
{ {
name: 'Đơn giao trễ', name: "Đơn giao trễ",
details: 'Đơn hàng #DH-2156 - Đã trễ 2 giờ - Khách: Siêu thị XYZ', details: "Đơn hàng #DH-2156 - Đã trễ 2 giờ - Khách: Siêu thị XYZ",
level: 3, level: 3,
type: 'time', type: "time",
}, },
{ {
name: 'Tồn kho thấp', name: "Tồn kho thấp",
details: 'Gạo ST25 - Chỉ còn 45 bao - Cần nhập thêm', details: "Gạo ST25 - Chỉ còn 45 bao - Cần nhập thêm",
level: 2, level: 2,
type: 'inventory' type: "inventory",
}, },
] ];
</script> </script>
<template> <template>
<div class="card"> <div class="card">
@@ -35,4 +35,4 @@ const warnings = [
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -5,7 +5,11 @@
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })" @click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })"
> >
<SvgIcon <SvgIcon
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }" v-bind="{
name: 'az.svg',
type: checkFilter() ? 'grey' : 'primary',
size: 22,
}"
></SvgIcon> ></SvgIcon>
</a> </a>
<span <span
@@ -20,7 +24,11 @@
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })" @click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })"
> >
<SvgIcon <SvgIcon
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }" v-bind="{
name: 'az.svg',
type: checkFilter() ? 'grey' : 'primary',
size: 22,
}"
></SvgIcon> ></SvgIcon>
</a> </a>
<span <span
@@ -30,7 +38,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="moveLeft()"> <a
class="mr-4"
@click="moveLeft()"
>
<SvgIcon v-bind="{ name: 'left5.png', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'left5.png', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
<span <span
@@ -40,7 +51,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="moveRight()"> <a
class="mr-4"
@click="moveRight()"
>
<SvgIcon v-bind="{ name: 'right5.png', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'right5.png', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
<span <span
@@ -50,7 +64,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="resizeWidth()"> <a
class="mr-4"
@click="resizeWidth()"
>
<SvgIcon v-bind="{ name: 'thick.svg', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'thick.svg', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
<span <span
@@ -60,7 +77,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="resizeWidth(true)"> <a
class="mr-4"
@click="resizeWidth(true)"
>
<SvgIcon v-bind="{ name: 'thin.svg', type: 'primary', size: 23 }"></SvgIcon> <SvgIcon v-bind="{ name: 'thin.svg', type: 'primary', size: 23 }"></SvgIcon>
</a> </a>
<span <span
@@ -70,7 +90,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="hideField()"> <a
class="mr-4"
@click="hideField()"
>
<SvgIcon v-bind="{ name: 'eye-off.svg', type: 'primary', size: 23 }"></SvgIcon> <SvgIcon v-bind="{ name: 'eye-off.svg', type: 'primary', size: 23 }"></SvgIcon>
</a> </a>
<span <span
@@ -81,7 +104,10 @@
</span> </span>
<!-- <template v-if="store.login ? store.login.is_admin : false"> --> <!-- <template v-if="store.login ? store.login.is_admin : false"> -->
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="currentField.mandatory ? false : doRemove()"> <a
class="mr-4"
@click="currentField.mandatory ? false : doRemove()"
>
<SvgIcon v-bind="{ name: 'bin.svg', type: 'primary', size: 23 }"></SvgIcon> <SvgIcon v-bind="{ name: 'bin.svg', type: 'primary', size: 23 }"></SvgIcon>
</a> </a>
<span <span
@@ -94,11 +120,7 @@
<a <a
class="mr-4" class="mr-4"
:class="currentField.format === 'number' ? null : 'has-text-grey-light'" :class="currentField.format === 'number' ? null : 'has-text-grey-light'"
@click=" @click="currentField.format === 'number' ? $emit('modalevent', { name: 'copyfield', data: currentField }) : false"
currentField.format === 'number'
? $emit('modalevent', { name: 'copyfield', data: currentField })
: false
"
> >
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
@@ -109,7 +131,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="fieldList()"> <a
class="mr-4"
@click="fieldList()"
>
<SvgIcon v-bind="{ name: 'menu4.png', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'menu4.png', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
<span <span
@@ -119,7 +144,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="createField()"> <a
class="mr-4"
@click="createField()"
>
<SvgIcon v-bind="{ name: 'add.png', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'add.png', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
<span <span
@@ -129,7 +157,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="tableOption()"> <a
class="mr-4"
@click="tableOption()"
>
<SvgIcon v-bind="{ name: 'more.svg', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'more.svg', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
<span <span
@@ -139,7 +170,10 @@
> >
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-4" @click="saveSetting()"> <a
class="mr-4"
@click="saveSetting()"
>
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 22 }"></SvgIcon>
</a> </a>
<span <span
@@ -156,7 +190,7 @@
? currentField.formula ? currentField.formula
? true ? true
: x.code !== 'formula' : x.code !== 'formula'
: !['filter', 'formula'].find((y) => y === x.code) : !['filter', 'formula'].find((y) => y === x.code),
)" )"
:key="i" :key="i"
:class="selectTab.code === v.code ? 'is-active' : 'has-text-primary'" :class="selectTab.code === v.code ? 'is-active' : 'has-text-primary'"
@@ -193,35 +227,44 @@
/> />
</div> </div>
<div class="control"> <div class="control">
<button class="button" @click="editLabel()"> <button
class="button"
@click="editLabel()"
>
<SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 19 }"></SvgIcon> <SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 19 }"></SvgIcon>
</button> </button>
</div> </div>
</div> </div>
<p class="help is-danger" v-if="errors.find((v) => v.name === 'label')"> <p
class="help is-danger"
v-if="errors.find((v) => v.name === 'label')"
>
{{ errors.find((v) => v.name === "label").msg }} {{ errors.find((v) => v.name === "label").msg }}
</p> </p>
<div class="field mt-3"> <div class="field mt-3">
<label class="label fs-14" <label class="label fs-14">Kiểu dữ liệu<span class="has-text-danger"> * </span></label>
>Kiểu dữ liệu<span class="has-text-danger"> * </span></label
>
<div class="control fs-14"> <div class="control fs-14">
<span class="mr-4" v-for="(v, i) in datatype"> <span
<span class="icon-text" v-if="radioType === v"> class="mr-4"
<SvgIcon v-for="(v, i) in datatype"
v-bind="{ name: 'radio-checked.svg', type: 'gray', size: 22 }" >
></SvgIcon> <span
class="icon-text"
v-if="radioType === v"
>
<SvgIcon v-bind="{ name: 'radio-checked.svg', type: 'gray', size: 22 }"></SvgIcon>
</span> </span>
{{ v.name }} {{ v.name }}
</span> </span>
</div> </div>
</div> </div>
<div class="field is-horizontal" v-if="currentField.format === 'number'"> <div
class="field is-horizontal"
v-if="currentField.format === 'number'"
>
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<label class="label fs-14" <label class="label fs-14">Đơn vị <span class="has-text-danger"> * </span> </label>
>Đơn vị <span class="has-text-danger"> * </span>
</label>
<div class="control"> <div class="control">
<SearchBox <SearchBox
v-bind="{ v-bind="{
@@ -234,7 +277,10 @@
@option="selected('_account', $event)" @option="selected('_account', $event)"
></SearchBox> ></SearchBox>
</div> </div>
<p class="help has-text-danger" v-if="errors.find((v) => v.name === 'unit')"> <p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'unit')"
>
{{ errors.find((v) => v.name === "unit").msg }} {{ errors.find((v) => v.name === "unit").msg }}
</p> </p>
</div> </div>
@@ -261,7 +307,10 @@
class="mr-4" class="mr-4"
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')" v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
> >
<a class="icon-text" @click="changeTemplate(v)"> <a
class="icon-text"
@click="changeTemplate(v)"
>
<SvgIcon <SvgIcon
v-bind="{ v-bind="{
name: `radio-${radioTemplate === v.code ? '' : 'un'}checked.svg`, name: `radio-${radioTemplate === v.code ? '' : 'un'}checked.svg`,
@@ -277,11 +326,15 @@
</div> </div>
</div> </div>
</div> </div>
<p class="mt-3" v-if="radioTemplate === 'option'"> <p
<button class="button is-primary is-small has-text-white" @click="showSidebar()"> class="mt-3"
<span class="fs-14">{{ v-if="radioTemplate === 'option'"
`${currentField.template ? "Sửa" : "Tạo"} định dạng` >
}}</span> <button
class="button is-primary is-small has-text-white"
@click="showSidebar()"
>
<span class="fs-14">{{ `${currentField.template ? "Sửa" : "Tạo"} định dạng` }}</span>
</button> </button>
</p> </p>
</div> </div>
@@ -308,14 +361,7 @@
import { useStore } from "@/stores/index"; import { useStore } from "@/stores/index";
import ScrollBox from "~/components/datatable/ScrollBox"; import ScrollBox from "~/components/datatable/ScrollBox";
const store = useStore(); const store = useStore();
const { const { $copy, $stripHtml, $clone, $arrayMove, $snackbar, $copyToClipboard } = useNuxtApp();
$copy,
$stripHtml,
$clone,
$arrayMove,
$snackbar,
$copyToClipboard,
} = useNuxtApp();
var props = defineProps({ var props = defineProps({
pagename: String, pagename: String,
field: Object, field: Object,
@@ -337,9 +383,7 @@ const getMenu = function () {
let field = currentField; let field = currentField;
field.disable = "display,tooltip"; field.disable = "display,tooltip";
let arr = field.disable ? field.disable.split(",") : undefined; let arr = field.disable ? field.disable.split(",") : undefined;
let array = arr let array = arr ? store.menuchoice.filter((v) => arr.findIndex((x) => x === v.code) < 0) : store.menuchoice;
? store.menuchoice.filter((v) => arr.findIndex((x) => x === v.code) < 0)
: store.menuchoice;
//if (store.login ? !(store.login.is_admin === false) : true) array = [array[0]]; //if (store.login ? !(store.login.is_admin === false) : true) array = [array[0]];
return array; return array;
}; };
@@ -350,10 +394,7 @@ var value1 = undefined;
var value2 = undefined; var value2 = undefined;
var moneyunit = store.moneyunit; var moneyunit = store.moneyunit;
var radioType = store.datatype.find((v) => v.code === currentField.format); var radioType = store.datatype.find((v) => v.code === currentField.format);
var selectUnit = var selectUnit = currentField.format === "number" ? moneyunit.find((v) => v.detail === currentField.unit) : undefined;
currentField.format === "number"
? moneyunit.find((v) => v.detail === currentField.unit)
: undefined;
var bgcolor = undefined; var bgcolor = undefined;
var radioBGcolor = colorchoice.find((v) => v.code === "none"); var radioBGcolor = colorchoice.find((v) => v.code === "none");
var color = undefined; var color = undefined;
@@ -366,16 +407,12 @@ var radioMaxWidth = colorchoice.find((v) => v.code === "none");
var maxwidth = undefined; var maxwidth = undefined;
var selectAlign = undefined; var selectAlign = undefined;
var radioAlign = colorchoice.find((v) => v.code === "none"); var radioAlign = colorchoice.find((v) => v.code === "none");
var radioTemplate = ref( var radioTemplate = ref(colorchoice.find((v) => v.code === (currentField.template ? "option" : "none"))["code"]);
colorchoice.find((v) => v.code === (currentField.template ? "option" : "none"))["code"]
);
var selectPlacement = store.placement.find((v) => v.code === "is-right"); var selectPlacement = store.placement.find((v) => v.code === "is-right");
var selectScheme = store.colorscheme.find((v) => v.code === "is-primary"); var selectScheme = store.colorscheme.find((v) => v.code === "is-primary");
var radioTooltip = store.colorchoice.find((v) => v.code === "none"); var radioTooltip = store.colorchoice.find((v) => v.code === "none");
var selectField = undefined; var selectField = undefined;
var tags = currentField.tags var tags = currentField.tags ? currentField.tags.map((v) => fields.find((x) => x.name === v)) : [];
? currentField.tags.map((v) => fields.find((x) => x.name === v))
: [];
var formula = currentField.formula ? currentField.formula : undefined; var formula = currentField.formula ? currentField.formula : undefined;
var decimal = currentField.decimal; var decimal = currentField.decimal;
let shortmenu = store.menuchoice.filter((x) => let shortmenu = store.menuchoice.filter((x) =>
@@ -383,7 +420,7 @@ let shortmenu = store.menuchoice.filter((x) =>
? currentField.formula ? currentField.formula
? true ? true
: x.code !== "formula" : x.code !== "formula"
: !["filter", "formula"].find((y) => y === x.code) : !["filter", "formula"].find((y) => y === x.code),
); );
var selectTab = shortmenu.find((v) => selectTab.code === v.code) var selectTab = shortmenu.find((v) => selectTab.code === v.code)
? selectTab ? selectTab
@@ -448,9 +485,7 @@ function tableOption() {
} }
const getFields = function () { const getFields = function () {
fields = pagedata ? $copy(pagedata.fields) : []; fields = pagedata ? $copy(pagedata.fields) : [];
fields.map( fields.map((v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label));
(v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label)
);
}; };
const doSelect = function (evt) { const doSelect = function (evt) {
emit("modalevent", { name: "selected", data: evt[props.field.name] }); emit("modalevent", { name: "selected", data: evt[props.field.name] });
@@ -510,15 +545,16 @@ const saveSetting = function () {
const showSidebar = function () { const showSidebar = function () {
let event = { name: "template", field: currentField }; let event = { name: "template", field: currentField };
let title = "Danh sách cột"; let title = "Danh sách cột";
if (event.name === "bgcolor") if (event.name === "bgcolor") title = `Đổi màu nền: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
title = `Đổi màu nền: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`; else if (event.name === "color") title = `Đổi màu chữ: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
else if (event.name === "color") else if (event.name === "template") title = `Định dạng nâng cao: ${$stripHtml(event.field.label, 30)}`;
title = `Đổi màu chữ: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
else if (event.name === "template")
title = `Định dạng nâng cao: ${$stripHtml(event.field.label, 30)}`;
showmodal.value = { showmodal.value = {
component: "datatable/FormatOption", component: "datatable/FormatOption",
vbind: { event: event, currentField: currentField, pagename: props.pagename }, vbind: {
event: event,
currentField: currentField,
pagename: props.pagename,
},
width: "850px", width: "850px",
height: "700px", height: "700px",
title: title, title: title,

View File

@@ -1,180 +1,312 @@
<template> <template>
<div v-if="docid"> <div v-if="docid">
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-body">
<div class="field">
<label class="label">Đối tượng</label>
<p class="control fs-14">
<b-radio v-for="(v,i) in types" :key="i" v-model="type"
:native-value="v" @input="changeType(v)">
{{v.name}}
</b-radio>
</p>
</div>
<div class="field">
<label class="label">Kích cỡ</label>
<p class="control fs-14">
<b-radio v-for="(v,i) in sizes.filter(v=>type? (type.code==='tag'? v.code!=='is-small' : 1>0) : true)" :key="i" v-model="size"
:native-value="v" @input="changeType(v)">
{{v.name}}
</b-radio>
</p>
</div>
<div class="field">
<label class="label" v-if="['tag'].find(v=>v===type.code)">Hình khối</label>
<p class="control fs-14" v-if="['tag'].find(v=>v===type.code)">
<b-radio v-for="(v,i) in shapes" :key="i" v-model="shape"
:native-value="v" @input="changeType(v)">
{{v.name}}
</b-radio>
</p>
</div>
</div>
</div>
<div class="field is-horizontal" v-if="['tag'].find(v=>v===type.code)">
<div class="field-body"> <div class="field-body">
<div class="field" v-if="type.code!=='tag'"> <div class="field">
<label class="label">Outline</label> <label class="label">Đối tượng</label>
<p class="control fs-14"> <p class="control fs-14">
<b-radio v-for="(v,i) in outlines" :key="i" v-model="outline" <b-radio
:native-value="v" @input="changeType(v)"> v-for="(v, i) in types"
{{v.name}} :key="i"
v-model="type"
:native-value="v"
@input="changeType(v)"
>
{{ v.name }}
</b-radio>
</p>
</div>
<div class="field">
<label class="label">Kích cỡ</label>
<p class="control fs-14">
<b-radio
v-for="(v, i) in sizes.filter((v) =>
type ? (type.code === 'tag' ? v.code !== 'is-small' : 1 > 0) : true,
)"
:key="i"
v-model="size"
:native-value="v"
@input="changeType(v)"
>
{{ v.name }}
</b-radio>
</p>
</div>
<div class="field">
<label
class="label"
v-if="['tag'].find((v) => v === type.code)"
>Hình khối</label
>
<p
class="control fs-14"
v-if="['tag'].find((v) => v === type.code)"
>
<b-radio
v-for="(v, i) in shapes"
:key="i"
v-model="shape"
:native-value="v"
@input="changeType(v)"
>
{{ v.name }}
</b-radio> </b-radio>
</p> </p>
</div> </div>
</div> </div>
</div>
<div class="tags" v-if="type.code==='tag'">
<a :class="getClass(v)" v-for="(v,i) in colorscheme" :key="i"
@click="doSelect(v)" :ref="'tag' + i"> {{v.name}} </a>
</div> </div>
<div class="pt-2" v-else-if="type.code==='span'"> <div
<a class="mr-3" :class="getSpanClass(v)" v-for="(v,i) in colorscheme" :key="i" class="field is-horizontal"
@click="doSelectSpan(v)" :ref="'span' + i"> {{v.name}} </a> v-if="['tag'].find((v) => v === type.code)"
>
<div class="field-body">
<div
class="field"
v-if="type.code !== 'tag'"
>
<label class="label">Outline</label>
<p class="control fs-14">
<b-radio
v-for="(v, i) in outlines"
:key="i"
v-model="outline"
:native-value="v"
@input="changeType(v)"
>
{{ v.name }}
</b-radio>
</p>
</div>
</div>
</div> </div>
<div :class="`tabs is-boxed mt-5 mb-5 ${tab.code==='template'? '' : 'pb-2'}`"> <div
class="tags"
v-if="type.code === 'tag'"
>
<a
:class="getClass(v)"
v-for="(v, i) in colorscheme"
:key="i"
@click="doSelect(v)"
:ref="'tag' + i"
>
{{ v.name }}
</a>
</div>
<div
class="pt-2"
v-else-if="type.code === 'span'"
>
<a
class="mr-3"
:class="getSpanClass(v)"
v-for="(v, i) in colorscheme"
:key="i"
@click="doSelectSpan(v)"
:ref="'span' + i"
>
{{ v.name }}
</a>
</div>
<div :class="`tabs is-boxed mt-5 mb-5 ${tab.code === 'template' ? '' : 'pb-2'}`">
<ul> <ul>
<li :class="tab.code===v.code? 'is-active' : ''" <li
v-for="(v,i) in tabs" :key="i" @click="changeTab(v)"><a class="fs-15">{{v.name}}</a> :class="tab.code === v.code ? 'is-active' : ''"
v-for="(v, i) in tabs"
:key="i"
@click="changeTab(v)"
>
<a class="fs-15">{{ v.name }}</a>
</li> </li>
</ul> </ul>
</div> </div>
<template v-if="tab.code==='selected'"> <template v-if="tab.code === 'selected'">
<a v-for="(v,i) in tags" :key="i" @click="selected=v"> <a
<div class="field is-grouped is-grouped-multiline mt-4"> v-for="(v, i) in tags"
<p class="control"> :key="i"
<a :class="v.class"> @click="selected = v"
{{v.name}} >
<div class="field is-grouped is-grouped-multiline mt-4">
<p class="control">
<a :class="v.class">
{{ v.name }}
</a>
</p>
<p class="control">
<input
class="input is-small"
type="text"
v-model="v.name"
/>
</p>
<p class="control">
<a @click="remove(i)">
<SvgIcon v-bind="{ name: 'close.svg', type: 'danger', size: 22 }"></SvgIcon>
</a>
</p>
<p
class="control has-text-right ml-5"
v-if="selected ? selected.id === v.id : false"
>
<SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 22 }"></SvgIcon>
</p>
</div>
</a> </a>
</p> </template>
<p class="control">
<input class="input is-small" type="text" v-model="v.name">
</p>
<p class="control">
<a @click="remove(i)">
<SvgIcon v-bind="{name: 'close.svg', type: 'danger', size: 22}"></SvgIcon>
</a>
</p>
<p class="control has-text-right ml-5" v-if="selected? selected.id===v.id : false">
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 22}"></SvgIcon>
</p>
</div>
</a>
</template>
<template v-else-if="tab.code==='condition'"> <template v-else-if="tab.code === 'condition'">
<div class="mb-5" v-if="selected"> <div
<b-radio v-for="(v,i) in conditions" :key="i" v-model="condition" class="mb-5"
:native-value="v" @input="changeCondition(v)"> v-if="selected"
{{v.name}} >
</b-radio> <b-radio
</div> v-for="(v, i) in conditions"
:key="i"
<template v-if="condition? condition.code==='yes' : false"> v-model="condition"
<div class="field mt-3"> :native-value="v"
<label class="label fs-14">Chọn trường xây dựng biểu thức <span class="has-text-danger"> * </span> </label> @input="changeCondition(v)"
<div class="control"> >
<b-taginput {{ v.name }}
size="is-small" </b-radio>
v-model="tagsField"
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
type="is-dark is-light"
autocomplete
:open-on-focus="true"
field="name"
icon="plus"
placeholder="Chọn trường"
>
<template slot-scope="props">
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 50)}} </span>
</template>
<template slot="empty">
Không trường thỏa mãn
</template>
</b-taginput>
</div> </div>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
</div> <template v-if="condition ? condition.code === 'yes' : false">
<div class="field mt-1" v-if="tagsField.length>0"> <div class="field mt-3">
<p class="help is-primary"> Click đúp vào để thêm vào biểu thức.</p> <label class="label fs-14"
>Chọn trường xây dựng biểu thức
<span class="has-text-danger"> * </span>
</label>
<div class="control">
<b-taginput
size="is-small"
v-model="tagsField"
:data="pageData ? pageData.fields.filter((v) => v.format === 'number') : []"
type="is-dark is-light"
autocomplete
:open-on-focus="true"
field="name"
icon="plus"
placeholder="Chọn trường"
>
<template slot-scope="props">
<span class="mr-3 has-text-danger has-text-weight-bold"> {{ props.option.name }}</span>
<span :class="tagsField.find((v) => v.id === props.option.id) ? 'has-text-dark' : ''">
{{ $stripHtml(props.option.label, 50) }}
</span>
</template>
<template slot="empty"> Không có trường thỏa mãn </template>
</b-taginput>
</div>
<p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'tagsField')"
>
{{ errors.find((v) => v.name === "tagsField").message }}
</p>
</div>
<div
class="field mt-1"
v-if="tagsField.length > 0"
>
<p class="help is-primary">Click đúp vào để thêm vào biểu thức.</p>
<div class="tagsField"> <div class="tagsField">
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name" <a
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i"> @dblclick="expression = expression ? expression + ' ' + v.name : v.name"
<span class="tooltip"> class="tag is-rounded"
{{v.name}} v-for="(v, i) in tagsField"
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span> :key="i"
</span> >
<span class="tooltip">
{{ v.name }}
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
</span>
</a> </a>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label fs-14">Biểu thức dạng Đúng / Sai <span class="has-text-danger"> * </span> </label> <label class="label fs-14"
>Biểu thức có dạng Đúng / Sai
<span class="has-text-danger"> * </span>
</label>
<p class="control is-expanded"> <p class="control is-expanded">
<input class="input" type="text" v-model="expression" placeholder="Tạo biểu thức tại đây"> <input
</p> class="input"
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p> type="text"
v-model="expression"
placeholder="Tạo biểu thức tại đây"
/>
</p>
<p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'expression')"
>
{{ errors.find((v) => v.name === "expression").message }}
</p>
</div> </div>
</template> </template>
</template> </template>
<template v-else-if="tab.code==='option' && selected"> <template v-else-if="tab.code === 'option' && selected">
<div class="field is-horizontal border-bottom pb-2 mt-1"> <div class="field is-horizontal border-bottom pb-2 mt-1">
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<label class="label fs-14">Màu nền </label> <label class="label fs-14">Màu nền </label>
<p class="control fs-14"> <p class="control fs-14">
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioBGcolor" <b-radio
:native-value="v" @input="changeStyle()"> v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
{{v.name}} :key="i"
</b-radio> v-model="radioBGcolor"
:native-value="v"
@input="changeStyle()"
>
{{ v.name }}
</b-radio>
</p> </p>
</div> </div>
<div class="field" v-if="radioBGcolor? radioBGcolor.code==='option' : false"> <div
class="field"
v-if="radioBGcolor ? radioBGcolor.code === 'option' : false"
>
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label> <label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
<p class="control fs-14"> <p class="control fs-14">
<input type="color" v-model="bgcolor" @change="changeStyle()"> <input
type="color"
v-model="bgcolor"
@change="changeStyle()"
/>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="field is-horizontal border-bottom pb-2"> <div class="field is-horizontal border-bottom pb-2">
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<label class="label fs-14">Màu chữ </label> <label class="label fs-14">Màu chữ </label>
<p class="control fs-14"> <p class="control fs-14">
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioColor" <b-radio
:native-value="v" @input="changeStyle()"> v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
{{v.name}} :key="i"
v-model="radioColor"
:native-value="v"
@input="changeStyle()"
>
{{ v.name }}
</b-radio> </b-radio>
</p> </p>
</div> </div>
<div class="field" v-if="radioColor? radioColor.code==='option' : false"> <div
class="field"
v-if="radioColor ? radioColor.code === 'option' : false"
>
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label> <label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
<p class="control fs-14"> <p class="control fs-14">
<input type="color" v-model="color" @change="changeStyle()"> <input
type="color"
v-model="color"
@change="changeStyle()"
/>
</p> </p>
</div> </div>
</div> </div>
@@ -184,119 +316,189 @@
<div class="field"> <div class="field">
<label class="label fs-14">Cỡ chữ </label> <label class="label fs-14">Cỡ chữ </label>
<p class="control fs-14"> <p class="control fs-14">
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioSize" <b-radio
:native-value="v" @input="changeStyle()"> v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
{{v.name}} :key="i"
</b-radio> v-model="radioSize"
:native-value="v"
@input="changeStyle()"
>
{{ v.name }}
</b-radio>
</p> </p>
</div> </div>
<div class="field" v-if="radioSize? radioSize.code==='option' : false"> <div
class="field"
v-if="radioSize ? radioSize.code === 'option' : false"
>
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label> <label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
<p class="control fs-14"> <p class="control fs-14">
<input class="input is-small" type="text" placeholder="Nhập số" v-model="textsize" @change="changeStyle()"> <input
class="input is-small"
type="text"
placeholder="Nhập số"
v-model="textsize"
@change="changeStyle()"
/>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<template v-else-if="tab.code==='template'"> <template v-else-if="tab.code === 'template'">
<p class="mb-3"> <p class="mb-3">
<a @click="copyContent()" class="mr-6"> <a
<span class="icon-text"> @click="copyContent()"
<SvgIcon class="mr-2" v-bind="{name: 'copy.svg', type: 'primary', siz: 18}"></SvgIcon> class="mr-6"
>
<span class="icon-text">
<SvgIcon
class="mr-2"
v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"
></SvgIcon>
<span class="fs-16">Copy</span> <span class="fs-16">Copy</span>
</span> </span>
</a> </a>
<a @click="paste()" class="mr-6"> <a
@click="paste()"
class="mr-6"
>
<span class="icon-text"> <span class="icon-text">
<SvgIcon class="mr-2" v-bind="{name: 'pen1.svg', type: 'primary', siz: 18}"></SvgIcon> <SvgIcon
class="mr-2"
v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"
></SvgIcon>
<span class="fs-16">Paste</span> <span class="fs-16">Paste</span>
</span> </span>
</a> </a>
</p> </p>
<div> <div>
<textarea class="textarea fs-14" rows="8" v-model="text" @dblclick="doCheck"></textarea> <textarea
</div> class="textarea fs-14"
<p class="mt-5"> rows="8"
v-model="text"
@dblclick="doCheck"
></textarea>
</div>
<p class="mt-5">
<span class="icon-text fsb-18"> <span class="icon-text fsb-18">
Replace Replace
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 22}"></SvgIcon> <SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
</span> </span>
</p> </p>
<div class="field is-grouped mt-4"> <div class="field is-grouped mt-4">
<div class="control"> <div class="control">
<p class="fsb-14 mb-1">Đoạn text</p> <p class="fsb-14 mb-1">Đoạn text</p>
<input class="input" type="text" placeholder="" v-model="source"> <input
class="input"
type="text"
placeholder=""
v-model="source"
/>
</div> </div>
<div class="control"> <div class="control">
<p class="fsb-14 mb-1">Thay bằng</p> <p class="fsb-14 mb-1">Thay bằng</p>
<input class="input" type="text" placeholder="" v-model="target"> <input
class="input"
type="text"
placeholder=""
v-model="target"
/>
</div> </div>
<div class="control pl-5"> <div class="control pl-5">
<button class="button is-primary is-outlined mt-5" @click="replace()">Replace</button> <button
class="button is-primary is-outlined mt-5"
@click="replace()"
>
Replace
</button>
</div> </div>
</div> </div>
<p class="mt-5"> <p class="mt-5">
<button class="button is-primary has-text-white" @click="changeTemplate()">Áp dụng</button> <button
class="button is-primary has-text-white"
@click="changeTemplate()"
>
Áp dụng
</button>
</p> </p>
</template> </template>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from "vue";
import { useStore } from '@/stores/index' import { useStore } from "@/stores/index";
const store = useStore() const store = useStore();
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp() const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp();
var props = defineProps({ var props = defineProps({
pagename: String, pagename: String,
field: Object field: Object,
}) });
var colorscheme = store.colorscheme var colorscheme = store.colorscheme;
var colorchoice = store.colorchoice var colorchoice = store.colorchoice;
var pageData = store[props.pagename] var pageData = store[props.pagename];
var field = props.field var field = props.field;
var type = undefined var type = undefined;
var size = undefined var size = undefined;
var types = [{code: 'span', name: 'span'}, {code: 'tag', name: 'tag'}] var types = [
var sizes = [{code: 'is-small', name: 'Nhỏ', value: 'is-size-6'}, {code: 'is-normal', name: 'Trung bình', value: 'is-size-5'}, { code: "span", name: "span" },
{code: 'is-medium', name: 'Lớn', value: 'is-size-4'}] { code: "tag", name: "tag" },
var shapes = [{code: 'default', name: 'Mặc định'}, {code: 'is-rounded', name: 'Tròn góc'}] ];
var shape = undefined var sizes = [
var outlines = [{code: 'default', name: 'Mặc định'}, {code: 'is-outlined', name: 'Outline'}] { code: "is-small", name: "Nhỏ", value: "is-size-6" },
var outline = undefined { code: "is-normal", name: "Trung bình", value: "is-size-5" },
var conditions = [{code: 'no', name: 'Không áp dụng'}, {code: 'yes', name: 'Có áp dụng'}] { code: "is-medium", name: "Lớn", value: "is-size-4" },
var condition = undefined ];
var tags = [] var shapes = [
var selected = undefined { code: "default", name: "Mặc định" },
var tabs = [{code: 'selected', name: 'Bước 1: Tạo nội dung'}, {code: 'condition', name: 'Bước 2: Đặt điều kiện'}, {code: 'option', name: 'Bước 3: Chọn màu, cỡ chữ'}, { code: "is-rounded", name: "Tròn góc" },
{code: 'template', name: 'Bước 4: Mã lệnh & áp dụng'}] ];
var tab = ref(undefined) var shape = undefined;
var tagsField = [] var outlines = [
var errors = [] { code: "default", name: "Mặc định" },
var expression = '' { code: "is-outlined", name: "Outline" },
var text = ref(null) ];
var radioBGcolor = undefined var outline = undefined;
var radioColor = undefined var conditions = [
var radioSize = undefined { code: "no", name: "Không áp dụng" },
var bgcolor = undefined { code: "yes", name: "Có áp dụng" },
var color = undefined ];
var textsize = undefined var condition = undefined;
var source = undefined var tags = [];
var target = $copy(field.name) var selected = undefined;
var tabs = [
const initData = function() { { code: "selected", name: "Bước 1: Tạo nội dung" },
type = types.find(v=>v.code==='tag') { code: "condition", name: "Bước 2: Đặt điều kiện" },
size = sizes.find(v=>v.code==='is-normal') { code: "option", name: "Bước 3: Chọn màu, cỡ chữ" },
shape = shapes.find(v=>v.code==='is-rounded') { code: "template", name: "Bước 4: Mã lệnh & áp dụng" },
outline = shapes.find(v=>v.code==='default') ];
if($empty(field.template)) tab.value =tabs.find(v=>v.code==='selected') var tab = ref(undefined);
else { var tagsField = [];
text.value =$copy(field.template) var errors = [];
tab.value =tabs.find(v=>v.code==='template') var expression = "";
} var text = ref(null);
condition =conditions.find(v=>v.code==='no') var radioBGcolor = undefined;
var radioColor = undefined;
var radioSize = undefined;
var bgcolor = undefined;
var color = undefined;
var textsize = undefined;
var source = undefined;
var target = $copy(field.name);
const initData = function () {
type = types.find((v) => v.code === "tag");
size = sizes.find((v) => v.code === "is-normal");
shape = shapes.find((v) => v.code === "is-rounded");
outline = shapes.find((v) => v.code === "default");
if ($empty(field.template)) tab.value = tabs.find((v) => v.code === "selected");
else {
text.value = $copy(field.template);
tab.value = tabs.find((v) => v.code === "template");
} }
/*watch: { condition = conditions.find((v) => v.code === "no");
};
/*watch: {
expression: function(newVal) { expression: function(newVal) {
if($empty(newVal)) return if($empty(newVal)) return
elsecheckExpression() elsecheckExpression()
@@ -332,100 +534,98 @@ const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = use
} }
} }
},*/ },*/
function changeTab(v) { function changeTab(v) {
tab.value = v tab.value = v;
}
const paste = async function () {
text.value = await navigator.clipboard.readText();
};
const replace = function () {
if ($empty(text.value)) return;
text.value = text.value.replaceAll(source, target);
};
const doCheck = function () {
let text = window.getSelection().toString();
if ($empty(text)) return;
source = text;
};
const changeStyle = function () {
selected.bgcolor = selected.color = selected.textsize = selected.style = undefined;
let style = "";
if (radioBGcolor.code === "option" ? !$empty(bgcolor) : false) {
selected.bgcolor = bgcolor;
style += "background-color: " + bgcolor + " !important; ";
} }
const paste = async function() { if (radioColor.code === "option" ? !$empty(color) : false) {
text.value = await navigator.clipboard.readText() selected.color = color;
style += "color: " + color + " !important; ";
}
if (radioSize.code === "option" ? $isNumber(textsize) : false) {
selected.textsize = textsize;
style += "font-size: " + textsize + "px !important; ";
}
$empty(style) ? false : (selected.style = style);
};
const changeCondition = function (v) {
if (v.code === "no") selected.expression = undefined;
};
const copyContent = function () {
$copyToClipboard(text.value);
};
const changeTemplate = function () {
let copy = pageData;
let found = copy.fields.find((v) => v.name === field.name);
found.template = text.value;
store.commit(props.pagename, copy);
};
const checkExpression = function () {
errors = [];
let val = $copy(expression);
let exp = $copy(expression);
tagsField.forEach((v) => {
let myRegExp = new RegExp(v.name, "g");
val = val.replace(myRegExp, Math.random());
exp = exp.replace(myRegExp, "formatNumber(row['" + v.name + "'])");
});
try {
let value = $calc(val);
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
} else if (!(eval(value) === true || eval(value) === false)) {
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
} else if (selected) {
selected.expression = exp;
selected.formula = expression;
selected.tags = $copy(tagsField);
} }
const replace = function() { } catch (err) {
if($empty(text.value)) return errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
text.value =text.value.replaceAll(source,target) }
} returnerrors.length > 0 ? false : true;
const doCheck = function() { };
let text = window.getSelection().toString() const changeType = function (v) {};
if($empty(text)) return const doSelect = function (v) {
source = text tags.push({ id: $id(), name: v.name, class: getClass(v) });
} tab = tabs.find((v) => v.code === "selected");
const changeStyle = function() { selected = tags[tags.length - 1];
selected.bgcolor =selected.color =selected.textsize =selected.style = undefined };
let style = '' const doSelectSpan = function (v) {
if(radioBGcolor.code==='option'? !$empty(bgcolor) : false) { tags.push({ id: $id(), name: v.name, class: getSpanClass(v) });
selected.bgcolor =bgcolor tab = tabs.find((v) => v.code === "selected");
style += 'background-color: ' +bgcolor + ' !important; ' selected = tags[tags.length - 1];
} };
if(radioColor.code==='option'? !$empty(color) : false) { const remove = function (i) {
selected.color =color $remove(tags, i);
style += 'color: ' +color + ' !important; ' };
} const getClass = function (v) {
if(radioSize.code==='option'?$isNumber(textsize) : false) { let value = type.code + " " + v.code + " " + size.code + (shape.code === "default" ? "" : " " + shape.code);
selected.textsize =textsize value += outline.code === "default" ? "" : " " + outline.code;
style += 'font-size: ' +textsize + 'px !important; ' return value;
} };
$empty(style)? false :selected.style = style const getSpanClass = function (v) {
} let value = "has-text-" + v.name.toLowerCase() + " " + size.value;
const changeCondition = function(v) { return value;
if(v.code==='no')selected.expression = undefined };
} initData();
const copyContent = function() { var docid = $id();
$copyToClipboard(text.value) </script>
}
const changeTemplate = function() {
let copy = pageData
let found = copy.fields.find(v=>v.name===field.name)
found.template = text.value
store.commit(props.pagename, copy)
}
const checkExpression = function() {
errors = []
let val =$copy(expression)
let exp =$copy(expression)
tagsField.forEach(v => {
let myRegExp = new RegExp(v.name, 'g')
val = val.replace(myRegExp, Math.random())
exp = exp.replace(myRegExp, "formatNumber(row['" + v.name + "'])")
})
try {
let value =$calc(val)
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
} else if(!(eval(value)===true || eval(value)===false)) {
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
} else if(selected) {
selected.expression = exp
selected.formula =expression
selected.tags =$copy(tagsField)
}
}
catch(err) {
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
}
returnerrors.length>0? false : true
}
const changeType = function(v) {
}
const doSelect = function(v) {
tags.push({id:$id(), name: v.name, class:getClass(v)})
tab =tabs.find(v=>v.code==='selected')
selected =tags[tags.length-1]
}
const doSelectSpan = function(v) {
tags.push({id:$id(), name: v.name, class:getSpanClass(v)})
tab =tabs.find(v=>v.code==='selected')
selected =tags[tags.length-1]
}
const remove = function(i) {
$remove(tags, i)
}
const getClass = function(v) {
let value =type.code + ' ' + v.code + ' ' +size.code + (shape.code==='default'? '' : ' ' +shape.code)
value += (outline.code==='default'? '' : ' ' +outline.code)
return value
}
const getSpanClass = function(v) {
let value = 'has-text-' + v.name.toLowerCase() + ' ' +size.value
return value
}
initData()
var docid = $id()
</script>

View File

@@ -1,193 +1,314 @@
<template> <template>
<div class="columns mx-0"> <div class="columns mx-0">
<div class="column is-2"> <div class="column is-2">
<Caption class="mb-2" v-bind="{title: 'Tên model (bảng)', type: 'has-text-warning'}"></Caption> <Caption
<div class="mb-2"> class="mb-2"
<input class="input" v-model="text" placeholder="Tìm model" @change="findModel()"> v-bind="{ title: 'Tên model (bảng)', type: 'has-text-warning' }"
></Caption>
<div class="mb-2">
<input
class="input"
v-model="text"
placeholder="Tìm model"
@change="findModel()"
/>
</div>
<div style="max-height: 80vh; overflow: auto">
<div
:class="`py-1 border-bottom is-clickable ${current.model === v.model ? 'has-background-primary has-text-white' : ''}`"
v-for="v in displayData"
@click="changeMenu(v)"
>
{{ v.model }}
</div>
</div>
</div> </div>
<div style="max-height: 80vh; overflow: auto;"> <div class="column is-10 py-0 px-0">
<div :class="`py-1 border-bottom is-clickable ${current.model===v.model? 'has-background-primary has-text-white' : ''}`" <div class="tabs mb-3">
v-for="v in displayData" @click="changeMenu(v)"> <ul>
{{ v.model}} <li
</div> :class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`"
</div> v-for="v in tabs"
</div> >
<div class="column is-10 py-0 px-0"> <a @click="changeTab(v)">{{ v.name }}</a>
<div class="tabs mb-3"> </li>
<ul> </ul>
<li :class="`${v.code===tab? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs"> </div>
<a @click="changeTab(v)">{{v.name}}</a> <div v-if="tab === 'datatype'">
</li> <Caption
</ul> class="mb-2"
</div> v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"
<div v-if="tab==='datatype'"> ></Caption>
<Caption class="mb-2" v-bind="{title: 'Kiểu dữ liệu (type)', type: 'has-text-warning'}"></Caption> <div style="max-height: 75vh; overflow-y: auto">
<div style="max-height:75vh; overflow-y: auto;"> <div
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields"> class="py-1 border-bottom is-clickable"
{{ x.name}} v-for="x in current.fields"
<span class="ml-6 has-text-grey">{{ x.type }}</span> >
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a> {{ x.name }}
<span class="ml-6 has-text-grey">{{ x.type }}</span>
<a
class="ml-6 has-text-primary"
v-if="x.model"
@click="openModel(x)"
>{{ x.model }}</a
>
</div>
</div>
</div>
<template v-else-if="tab === 'table'">
<div class="columns mx-0 mb-0 pb-0">
<div class="column is-5">
<Caption
class="mb-1"
v-bind="{ title: 'Values', type: 'has-text-warning' }"
></Caption>
<input
class="input"
rows="1"
v-model="values"
placeholder="Tên trường không chứa dấu cách, vd: code,name"
/>
</div>
<div class="column is-4">
<Caption
class="mb-1"
v-bind="{ title: 'Filter', type: 'has-text-warning' }"
></Caption>
<input
class="input"
rows="1"
v-model="filter"
placeholder="{'code': 'xyz'}"
/>
</div>
<div class="column is-2">
<Caption
class="mb-1"
v-bind="{ title: 'Sort', type: 'has-text-warning' }"
></Caption>
<input
class="input"
rows="1"
v-model="sort"
placeholder="vd: -code,name"
/>
</div>
<div class="column is-1">
<Caption
class="mb-1"
v-bind="{ title: 'Load', type: 'has-text-warning' }"
></Caption>
<div>
<button
class="button is-primary has-text-white"
@click="loadData()"
>
Load
</button>
</div>
</div>
</div>
<Caption
class="mb-1"
v-bind="{ title: 'Query', type: 'has-text-warning' }"
></Caption>
<div class="mb-2">
{{ query }}
<a
class="has-text-primary ml-5"
@click="copy()"
>copy</a
>
<p>
{{ apiUrl }}
<a
class="has-text-primary ml-5"
@click="$copyToClipboard(apiUrl)"
>copy</a
>
<a
class="has-text-primary ml-5"
target="_blank"
:href="apiUrl"
>open</a
>
</p>
</div>
<div>
<DataTable
v-bind="{ pagename: pagename }"
v-if="pagedata"
/>
</div>
</template>
<div v-else>
<img
id="image"
:src="filePath"
alt=""
/>
<p class="pl-5">
<a
class="mr-5"
@click="downloadFile()"
>
<SvgIcon v-bind="{ name: 'download.svg', type: 'black', size: 24 }"></SvgIcon>
</a>
<a
target="_blank"
:href="filePath"
>
<SvgIcon v-bind="{ name: 'open.svg', type: 'black', size: 24 }"></SvgIcon>
</a>
</p>
</div>
</div> </div>
</div> </div>
</div> <Modal
<template v-else-if="tab==='table'"> @close="showmodal = undefined"
<div class="columns mx-0 mb-0 pb-0"> v-bind="showmodal"
<div class="column is-5"> v-if="showmodal"
<Caption class="mb-1" v-bind="{title: 'Values', type: 'has-text-warning'}"></Caption> ></Modal>
<input class="input" rows="1" v-model="values" placeholder="Tên trường không chứa dấu cách, vd: code,name">
</div>
<div class="column is-4">
<Caption class="mb-1" v-bind="{title: 'Filter', type: 'has-text-warning'}"></Caption>
<input class="input" rows="1" v-model="filter" placeholder="{'code': 'xyz'}">
</div>
<div class="column is-2">
<Caption class="mb-1" v-bind="{title: 'Sort', type: 'has-text-warning'}"></Caption>
<input class="input" rows="1" v-model="sort" placeholder="vd: -code,name">
</div>
<div class="column is-1">
<Caption class="mb-1" v-bind="{title: 'Load', type: 'has-text-warning'}"></Caption>
<div>
<button class="button is-primary has-text-white" @click="loadData()">Load</button>
</div>
</div>
</div>
<Caption class="mb-1" v-bind="{title: 'Query', type: 'has-text-warning'}"></Caption>
<div class="mb-2">
{{ query }}
<a class="has-text-primary ml-5" @click="copy()">copy</a>
<p>{{apiUrl}}
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a>
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a>
</p>
</div>
<div>
<DataTable v-bind="{pagename: pagename}" v-if="pagedata" />
</div>
</template>
<div v-else>
<img id="image" :src="filePath" alt="">
<p class="pl-5">
<a class="mr-5" @click="downloadFile()">
<SvgIcon v-bind="{name: 'download.svg', type: 'black', size: 24}"></SvgIcon>
</a>
<a target="_blank" :href="filePath">
<SvgIcon v-bind="{name: 'open.svg', type: 'black', size: 24}"></SvgIcon>
</a>
</p>
</div>
</div>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</template> </template>
<script setup> <script setup>
import { useStore } from '@/stores/index' import { useStore } from "@/stores/index";
const { $getdata, $getapi, $createField, $clone, $getpage, $empty, $copyToClipboard, $find, $multiSort, $download, $getpath } = useNuxtApp() const {
const store = useStore() $getdata,
var pagename = 'pagedata3' $getapi,
var pagedata = ref() $createField,
pagedata.value = $getpage() $clone,
pagedata.value.perPage = 10 $getpage,
store.commit(pagename, pagedata) $empty,
let list = ['LogEntry', 'Permission', 'ContentType', 'Session', 'Group'] $copyToClipboard,
var data = (await $getdata('getmodel')).filter(v=>list.findIndex(x=>x===v.model)<0) $find,
data = $multiSort(data, {model: 'asc'}) $multiSort,
var current = ref({fields: []}) $download,
var tabs = [{code: 'datatype', name: 'Kiểu dữ liệu'}, {code: 'table', name: 'Dữ liệu'}, {code: 'datamodel', name: 'Data model'}] $getpath,
var tab = ref('datatype') } = useNuxtApp();
var datatable = ref() const store = useStore();
var query = ref() var pagename = "pagedata3";
var values, filter var pagedata = ref();
var apiUrl = ref() pagedata.value = $getpage();
var showmodal = ref() pagedata.value.perPage = 10;
var text = null store.commit(pagename, pagedata);
var displayData = ref(data) let list = ["LogEntry", "Permission", "ContentType", "Session", "Group"];
var filePath = `${$getpath()}static/files/datamodel.png` var data = (await $getdata("getmodel")).filter((v) => list.findIndex((x) => x === v.model) < 0);
var sort = "-id" data = $multiSort(data, { model: "asc" });
current.value = data[0] var current = ref({ fields: [] });
var tabs = [
{ code: "datatype", name: "Kiểu dữ liệu" },
{ code: "table", name: "Dữ liệu" },
{ code: "datamodel", name: "Data model" },
];
var tab = ref("datatype");
var datatable = ref();
var query = ref();
var values, filter;
var apiUrl = ref();
var showmodal = ref();
var text = null;
var displayData = ref(data);
var filePath = `${$getpath()}static/files/datamodel.png`;
var sort = "-id";
current.value = data[0];
function changeMenu(v) { function changeMenu(v) {
values = undefined values = undefined;
filter = undefined filter = undefined;
sort = undefined sort = undefined;
current.value = v current.value = v;
if(tab.value==='table') loadData() if (tab.value === "table") loadData();
} }
async function changeTab(v) { async function changeTab(v) {
tab.value = v.code tab.value = v.code;
if(v.code==='table') loadData() if (v.code === "table") loadData();
} }
async function loadData() { async function loadData() {
let vfilter = filter? filter.trim() : undefined let vfilter = filter ? filter.trim() : undefined;
if(vfilter) { if (vfilter) {
try { try {
vfilter = JSON.parse(vfilter) vfilter = JSON.parse(vfilter);
} catch (error) { } catch (error) {
alert('Cấu trúc filter lỗi') alert("Cấu trúc filter có lỗi");
vfilter = undefined vfilter = undefined;
} }
} }
let params = {values: $empty(values)? undefined : values.trim(), filter: filter, sort: $empty(sort)? undefined : sort.trim()} let params = {
let modelName = current.value.model values: $empty(values) ? undefined : values.trim(),
let found = {name: modelName.toLowerCase().replace('_', ''), url: `data/${modelName}/`, url_detail: `data-detail/${modelName}/`, params: params} filter: filter,
query.value = $clone(found) sort: $empty(sort) ? undefined : sort.trim(),
let rs = await $getapi([found]) };
if(rs==='error') return alert('Đã xảy ra lỗi, hãy xem lại câu lệnh.') let modelName = current.value.model;
datatable.value = rs[0].data.rows let found = {
showData() name: modelName.toLowerCase().replace("_", ""),
url: `data/${modelName}/`,
url_detail: `data-detail/${modelName}/`,
params: params,
};
query.value = $clone(found);
let rs = await $getapi([found]);
if (rs === "error") return alert("Đã xảy ra lỗi, hãy xem lại câu lệnh.");
datatable.value = rs[0].data.rows;
showData();
// api query // api query
const baseUrl = $getpath() + `${query.value.url}` const baseUrl = $getpath() + `${query.value.url}`;
apiUrl.value = baseUrl apiUrl.value = baseUrl;
let vparams = !$empty(values)? {values: values} : null let vparams = !$empty(values) ? { values: values } : null;
if(!$empty(filter)) { if (!$empty(filter)) {
vparams = vparams? {values: values, filter:filter} : {filter:filter} vparams = vparams ? { values: values, filter: filter } : { filter: filter };
} }
if(!$empty(sort)) { if (!$empty(sort)) {
if(vparams) { if (vparams) {
vparams.sort = sort.trim() vparams.sort = sort.trim();
} else { } else {
vparams = {sort: sort.trim()} vparams = { sort: sort.trim() };
} }
} }
if(vparams) { if (vparams) {
let url = new URL(baseUrl); let url = new URL(baseUrl);
let searchParams = new URLSearchParams(vparams); let searchParams = new URLSearchParams(vparams);
url.search = searchParams.toString(); url.search = searchParams.toString();
apiUrl.value = baseUrl + url.search apiUrl.value = baseUrl + url.search;
} }
} }
function showData() { function showData() {
let arr = [] let arr = [];
if(!$empty(values)) { if (!$empty(values)) {
let arr1 = values.trim().split(',') let arr1 = values.trim().split(",");
arr1.map(v=>{ arr1.map((v) => {
let val = v.trim() let val = v.trim();
let field = $createField(val, val, 'string', true) let field = $createField(val, val, "string", true);
arr.push(field) arr.push(field);
}) });
} else { } else {
current.value.fields.map(v=>{ current.value.fields.map((v) => {
let field = $createField(v.name, v.name, 'string', true) let field = $createField(v.name, v.name, "string", true);
arr.push(field) arr.push(field);
}) });
} }
let clone = $clone(pagedata.value) let clone = $clone(pagedata.value);
clone.fields = arr clone.fields = arr;
clone.data = datatable.value clone.data = datatable.value;
pagedata.value = undefined pagedata.value = undefined;
setTimeout(()=>pagedata.value = clone) setTimeout(() => (pagedata.value = clone));
} }
function copy() { function copy() {
$copyToClipboard(JSON.stringify(query.value)) $copyToClipboard(JSON.stringify(query.value));
} }
function openModel(x) { function openModel(x) {
showmodal.value = {component: 'datatable/ModelInfo', title: x.model, width: '70%', height: '600px', showmodal.value = {
vbind: {data: data, info: $find(data, {model: x.model})}} component: "datatable/ModelInfo",
title: x.model,
width: "70%",
height: "600px",
vbind: { data: data, info: $find(data, { model: x.model }) },
};
} }
function downloadFile() { function downloadFile() {
$download(`${$getpath()}download/?name=datamodel.png&type=file`, 'datamodel.png') $download(`${$getpath()}download/?name=datamodel.png&type=file`, "datamodel.png");
} }
function findModel() { function findModel() {
if($empty(text)) return displayData.value = data if ($empty(text)) return (displayData.value = data);
displayData.value = data.filter(v=>v.model.toLowerCase().indexOf(text.toLowerCase())>=0) displayData.value = data.filter((v) => v.model.toLowerCase().indexOf(text.toLowerCase()) >= 0);
} }
</script> </script>

View File

@@ -1,57 +1,104 @@
<template> <template>
<div class="field is-grouped is-grouped-multiline pl-2" v-if="filters? filters.length>0 : false"> <div
<div class="control mr-5"> class="field is-grouped is-grouped-multiline pl-2"
<a class="button is-primary is-small has-text-white has-text-weight-bold" @click="updateData({filters: []})"> v-if="filters ? filters.length > 0 : false"
<span class="fs-14">Xóa lọc</span> >
</a> <div class="control mr-5">
</div> <a
<div class="control pr-2 mr-5"> class="button is-primary is-small has-text-white has-text-weight-bold"
<span class="icon-text"> @click="updateData({ filters: [] })"
<SvgIcon v-bind="{name: 'sigma.svg', type: 'primary', size: 20}"></SvgIcon> >
<span class="fsb-18 has-text-primary">{{totalRows}}</span> <span class="fs-14">Xóa lọc</span>
</span> </a>
</div> </div>
<div class="control" v-for="(v,i) in filters" :key="i"> <div class="control pr-2 mr-5">
<div class="tags has-addons is-marginless"> <span class="icon-text">
<a class="tag is-primary has-text-white is-marginless" @click="showCondition(v)">{{v.label.indexOf('>')>=0? $stripHtml(v.label,30) : v.label}}</a> <SvgIcon v-bind="{ name: 'sigma.svg', type: 'primary', size: 20 }"></SvgIcon>
<a class="tag is-delete is-marginless has-text-black-bis" @click="removeFilter(i)"></a> <span class="fsb-18 has-text-primary">{{ totalRows }}</span>
</div> </span>
<span class="help has-text-black-bis"> </div>
{{v.sort? v.sort : (v.select? ('[' + (v.select.length>0? $stripHtml(v.select[0],20) : '') + '...&#931;' + v.select.length + ']') : <div
(v.condition))}}</span> class="control"
</div> v-for="(v, i) in filters"
:key="i"
>
<div class="tags has-addons is-marginless">
<a
class="tag is-primary has-text-white is-marginless"
@click="showCondition(v)"
>{{ v.label.indexOf(">") >= 0 ? $stripHtml(v.label, 30) : v.label }}</a
>
<a
class="tag is-delete is-marginless has-text-black-bis"
@click="removeFilter(i)"
></a>
</div> </div>
<div class="table-container mb-0" ref="container" id="docid"> <span class="help has-text-black-bis">
<table class="table is-fullwidth is-bordered is-narrow is-hoverable" :style="tableStyle"> {{
<thead> v.sort
<tr> ? v.sort
<th v-for="(field,i) in displayFields" :key="i" :style="field.headerStyle"> : v.select
<div @click="showField(field)" :style="field.dropStyle"> ? "[" + (v.select.length > 0 ? $stripHtml(v.select[0], 20) : "") + "...&#931;" + v.select.length + "]"
<a v-if="field.label.indexOf('<')<0">{{field.label}}</a> : v.condition
<a v-else> }}</span
<component :is="dynamicComponent(field.label)" :row="v" @clickevent="clickEvent($event, v, field)" /> >
</a> </div>
</div> </div>
</th> <div
</tr> class="table-container mb-0"
</thead> ref="container"
<tbody> id="docid"
<tr v-for="(v,i) in displayData" :key="i"> >
<td <table
v-for="(field, j) in displayFields" class="table is-fullwidth is-bordered is-narrow is-hoverable"
:key="j" :style="tableStyle"
:id="field.name" >
:style="v[`${field.name}color`]" <thead>
style=" <tr>
overflow: hidden; <th
text-overflow: ellipsis; v-for="(field, i) in displayFields"
" :key="i"
@dblclick="doubleClick(field, v)"> :style="field.headerStyle"
<component :is="dynamicComponent(field.template)" :row="v" v-if="field.template" @clickevent="clickEvent($event, v, field)" /> >
<span v-else>{{ v[field.name] }}</span> <div
</td> @click="showField(field)"
</tr> :style="field.dropStyle"
</tbody> >
<a v-if="field.label.indexOf('<') < 0">{{ field.label }}</a>
<a v-else>
<component
:is="dynamicComponent(field.label)"
:row="v"
@clickevent="clickEvent($event, v, field)"
/>
</a>
</div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(v, i) in displayData"
:key="i"
>
<td
v-for="(field, j) in displayFields"
:key="j"
:id="field.name"
:style="v[`${field.name}color`]"
style="overflow: hidden; text-overflow: ellipsis"
@dblclick="doubleClick(field, v)"
>
<component
:is="dynamicComponent(field.template)"
:row="v"
v-if="field.template"
@clickevent="clickEvent($event, v, field)"
/>
<span v-else>{{ v[field.name] }}</span>
</td>
</tr>
</tbody>
</table> </table>
<DatatablePagination <DatatablePagination
v-bind="{ data: data, perPage: perPage }" v-bind="{ data: data, perPage: perPage }"
@@ -124,7 +171,7 @@ watch(
() => store[props.pagename], () => store[props.pagename],
(newVal, oldVal) => { (newVal, oldVal) => {
updateChange(); updateChange();
} },
); );
function updateChange() { function updateChange() {
pagedata = store[props.pagename]; pagedata = store[props.pagename];
@@ -141,24 +188,21 @@ function updateChange() {
const updateShow = function (full_data) { const updateShow = function (full_data) {
// allowed JS expressions - should return a boolean // allowed JS expressions - should return a boolean
const allowedFns = { const allowedFns = {
'$getEditRights()': $getEditRights, "$getEditRights()": $getEditRights,
}; };
const arr = pagedata.fields.filter(({ show }) => { const arr = pagedata.fields.filter(({ show }) => {
if (typeof show === 'boolean') return show; if (typeof show === "boolean") return show;
else { else {
// show is a string // show is a string
if (show === 'true') return true; if (show === "true") return true;
if (show === 'false') return false; if (show === "false") return false;
return allowedFns[show]?.() || false; return allowedFns[show]?.() || false;
} }
}); });
if (full_data === false) displayData = $copy(data); if (full_data === false) displayData = $copy(data);
else else
displayData = $copy( displayData = $copy(
data.filter( data.filter((ele, index) => index >= (currentPage - 1) * perPage && index < currentPage * perPage),
(ele, index) =>
index >= (currentPage - 1) * perPage && index < currentPage * perPage
)
); );
displayData.map((v) => { displayData.map((v) => {
arr.map((x) => (v[`${x.name}color`] = getStyle(x, v))); arr.map((x) => (v[`${x.name}color`] = getStyle(x, v)));
@@ -222,9 +266,7 @@ const getStyle = function (field, record) {
field.bgcolor.map((v) => { field.bgcolor.map((v) => {
if (v.type === "search") { if (v.type === "search") {
if ( if (
record[field.name] && !stop record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
: false
) { ) {
val += ` background-color:${v.color}; `; val += ` background-color:${v.color}; `;
stop = true; stop = true;
@@ -245,9 +287,7 @@ const getStyle = function (field, record) {
field.color.map((v) => { field.color.map((v) => {
if (v.type === "search") { if (v.type === "search") {
if ( if (
record[field.name] && !stop record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
: false
) { ) {
val += ` color:${v.color}; `; val += ` color:${v.color}; `;
stop = true; stop = true;
@@ -268,9 +308,7 @@ const getStyle = function (field, record) {
field.textsize.map((v) => { field.textsize.map((v) => {
if (v.type === "search") { if (v.type === "search") {
if ( if (
record[field.name] && !stop record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
: false
) { ) {
val += ` font-size:${v.size}px; `; val += ` font-size:${v.size}px; `;
stop = true; stop = true;
@@ -283,10 +321,7 @@ const getStyle = function (field, record) {
} }
} }
}); });
} else } else val += ` font-size:${tablesetting.find((v) => v.code === "table-font-size").detail}px;`;
val += ` font-size:${
tablesetting.find((v) => v.code === "table-font-size").detail
}px;`;
if (field.textalign) val += ` text-align:${field.textalign}; `; if (field.textalign) val += ` text-align:${field.textalign}; `;
if (field.minwidth) val += ` min-width:${field.minwidth}px; `; if (field.minwidth) val += ` min-width:${field.minwidth}px; `;
if (field.maxwidth) val += ` max-width:${field.maxwidth}px; `; if (field.maxwidth) val += ` max-width:${field.maxwidth}px; `;
@@ -295,56 +330,28 @@ const getStyle = function (field, record) {
const getSettingStyle = function (name, field) { const getSettingStyle = function (name, field) {
let value = ""; let value = "";
if (name === "container") { if (name === "container") {
value = value = "min-height:" + tablesetting.find((v) => v.code === "container-height").detail + "rem; ";
"min-height:" +
tablesetting.find((v) => v.code === "container-height").detail +
"rem; ";
} else if (name === "table") { } else if (name === "table") {
value += value += "background-color:" + tablesetting.find((v) => v.code === "table-background").detail + "; ";
"background-color:" + value += "font-size:" + tablesetting.find((v) => v.code === "table-font-size").detail + "px;";
tablesetting.find((v) => v.code === "table-background").detail + value += "color:" + tablesetting.find((v) => v.code === "table-font-color").detail + "; ";
"; ";
value +=
"font-size:" +
tablesetting.find((v) => v.code === "table-font-size").detail +
"px;";
value +=
"color:" + tablesetting.find((v) => v.code === "table-font-color").detail + "; ";
} else if (name === "header") { } else if (name === "header") {
value += value += "background-color:" + tablesetting.find((v) => v.code === "header-background").detail + "; ";
"background-color:" +
tablesetting.find((v) => v.code === "header-background").detail +
"; ";
if (field.minwidth) value += " min-width: " + field.minwidth + "px; "; if (field.minwidth) value += " min-width: " + field.minwidth + "px; ";
if (field.maxwidth) value += " max-width: " + field.maxwidth + "px; "; if (field.maxwidth) value += " max-width: " + field.maxwidth + "px; ";
} else if (name === "menu") { } else if (name === "menu") {
let arg = tablesetting.find((v) => v.code === "menu-width").detail; let arg = tablesetting.find((v) => v.code === "menu-width").detail;
arg = field ? (field.menuwidth ? field.menuwidth : arg) : arg; arg = field ? (field.menuwidth ? field.menuwidth : arg) : arg;
value += "width:" + arg + "rem; "; value += "width:" + arg + "rem; ";
value += value += "min-height:" + tablesetting.find((v) => v.code === "menu-min-height").detail + "rem; ";
"min-height:" + value += "max-height:" + tablesetting.find((v) => v.code === "menu-max-height").detail + "rem; ";
tablesetting.find((v) => v.code === "menu-min-height").detail +
"rem; ";
value +=
"max-height:" +
tablesetting.find((v) => v.code === "menu-max-height").detail +
"rem; ";
value += "overflow:auto; "; value += "overflow:auto; ";
} else if (name === "dropdown") { } else if (name === "dropdown") {
value += value += "font-size:" + tablesetting.find((v) => v.code === "header-font-size").detail + "px; ";
"font-size:" +
tablesetting.find((v) => v.code === "header-font-size").detail +
"px; ";
let found = filters.find((v) => v.name === field.name); let found = filters.find((v) => v.name === field.name);
found found
? (value += ? (value += "color:" + tablesetting.find((v) => v.code === "header-filter-color").detail + "; ")
"color:" + : (value += "color:" + tablesetting.find((v) => v.code === "header-font-color").detail + "; ");
tablesetting.find((v) => v.code === "header-filter-color").detail +
"; ")
: (value +=
"color:" +
tablesetting.find((v) => v.code === "header-font-color").detail +
"; ");
} }
return value; return value;
}; };
@@ -369,12 +376,8 @@ const frontendFilter = function (newVal) {
else { else {
let text = ""; let text = "";
filter.map((y, k) => { filter.map((y, k) => {
text += `${ text += `${k > 0 ? (filter[k - 1].operator === "and" ? " &&" : " ||") : ""} ${$formatNumber(x[name])}
k > 0 ? (filter[k - 1].operator === "and" ? " &&" : " ||") : "" ${y.condition === "=" ? "==" : y.condition === "<>" ? "!==" : y.condition} ${$formatNumber(y.value)}`;
} ${$formatNumber(x[name])}
${
y.condition === "=" ? "==" : y.condition === "<>" ? "!==" : y.condition
} ${$formatNumber(y.value)}`;
}); });
return $calc(text); return $calc(text);
} }
@@ -385,11 +388,7 @@ const frontendFilter = function (newVal) {
.filter((m) => m.select || m.filter) .filter((m) => m.select || m.filter)
.map((v) => { .map((v) => {
if (v.select) { if (v.select) {
data = data.filter( data = data.filter((x) => v.select.findIndex((y) => ($empty(y) ? $empty(x[v.name]) : y === x[v.name])) > -1);
(x) =>
v.select.findIndex((y) => ($empty(y) ? $empty(x[v.name]) : y === x[v.name])) >
-1
);
} else if (v.filter) { } else if (v.filter) {
data = data.filter((x) => checkValid(v.name, x, v.filter)); data = data.filter((x) => checkValid(v.name, x, v.filter));
} }
@@ -503,9 +502,7 @@ const updateData = async function (newVal) {
} }
tablesetting = $copy(pagedata.tablesetting || gridsetting); tablesetting = $copy(pagedata.tablesetting || gridsetting);
if (tablesetting) { if (tablesetting) {
perPage = pagedata.perPage perPage = pagedata.perPage ? pagedata.perPage : Number(tablesetting.find((v) => v.code === "per-page").detail);
? pagedata.perPage
: Number(tablesetting.find((v) => v.code === "per-page").detail);
} }
if (newVal.fields) { if (newVal.fields) {
fields = $copy(newVal.fields); fields = $copy(newVal.fields);

View File

@@ -1,20 +1,46 @@
<template> <template>
<TimeOption <TimeOption
v-bind="{ pagename: vpagename, api: api, timeopt: timeopt, filter: optfilter, importdata: props.importdata, newDataAvailable: newDataAvailable, params: vparams }" v-bind="{
ref="timeopt" @option="timeOption" @excel="exportExcel" @add="insert" @manual-refresh="manualRefresh" @refresh-data="refreshData" pagename: vpagename,
@import="openImportModal" class="mb-3" v-if="timeopt"></TimeOption> api: api,
<DataTable v-bind="{ pagename: vpagename }" @edit="edit" @insert="insert" @dataevent="dataEvent" v-if="pagedata" /> timeopt: timeopt,
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal" /> filter: optfilter,
importdata: props.importdata,
newDataAvailable: newDataAvailable,
params: vparams,
}"
ref="timeopt"
@option="timeOption"
@excel="exportExcel"
@add="insert"
@manual-refresh="manualRefresh"
@refresh-data="refreshData"
@import="openImportModal"
class="mb-3"
v-if="timeopt"
></TimeOption>
<DataTable
v-bind="{ pagename: vpagename }"
@edit="edit"
@insert="insert"
@dataevent="dataEvent"
v-if="pagedata"
/>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
/>
</template> </template>
<script setup> <script setup>
import TimeOption from '~/components/datatable/TimeOption' import TimeOption from "~/components/datatable/TimeOption";
import { useStore } from '~/stores/index' import { useStore } from "~/stores/index";
// [FIX] Thêm onActivated, onDeactivated để xử lý KeepAlive // [FIX] Thêm onActivated, onDeactivated để xử lý KeepAlive
import { ref, watch, onBeforeUnmount, onActivated, onDeactivated } from 'vue' import { ref, watch, onBeforeUnmount, onActivated, onDeactivated } from "vue";
const emit = defineEmits(['modalevent', 'dataevent', 'dataUpdated']) const emit = defineEmits(["modalevent", "dataevent", "dataUpdated"]);
const store = useStore() const store = useStore();
const props = defineProps({ const props = defineProps({
pagename: String, pagename: String,
@@ -26,43 +52,46 @@ const props = defineProps({
modal: Object, modal: Object,
timeopt: Object, timeopt: Object,
realtime: Object, realtime: Object,
importdata: Object importdata: Object,
}) });
const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp() const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp();
const showmodal = ref() const showmodal = ref();
const pagedata = ref() const pagedata = ref();
const newDataAvailable = ref(false) const newDataAvailable = ref(false);
const pendingNewData = ref(null) const pendingNewData = ref(null);
const lastDataHash = ref(null) const lastDataHash = ref(null);
const pollingInterval = ref(null) const pollingInterval = ref(null);
let vpagename = props.pagename let vpagename = props.pagename;
let vfilter = props.filter ? $copy(props.filter) : undefined let vfilter = props.filter ? $copy(props.filter) : undefined;
let vparams = props.params ? $copy(props.params) : undefined let vparams = props.params ? $copy(props.params) : undefined;
let connection = undefined let connection = undefined;
let optfilter = props.filter || (props.params ? props.params.filter : undefined) let optfilter = props.filter || (props.params ? props.params.filter : undefined);
const realtimeConfig = ref({ time: 0, update: "true" }) const realtimeConfig = ref({ time: 0, update: "true" });
if (props.realtime) { if (props.realtime) {
realtimeConfig.value = { time: props.realtime.time || 0, update: props.realtime.update } realtimeConfig.value = {
time: props.realtime.time || 0,
update: props.realtime.update,
};
} }
if (vparams?.filter) { if (vparams?.filter) {
for (const [key, value] of Object.entries(vparams.filter)) { for (const [key, value] of Object.entries(vparams.filter)) {
if (value.toString().indexOf('$') >= 0) { if (value.toString().indexOf("$") >= 0) {
vparams.filter[key] = store[value.replace('$', '')].id vparams.filter[key] = store[value.replace("$", "")].id;
} }
} }
} }
const generateDataHash = (data) => { const generateDataHash = (data) => {
if (!data) return null if (!data) return null;
try { try {
const replacer = (key, value) => const replacer = (key, value) =>
value && typeof value === 'object' && !Array.isArray(value) value && typeof value === "object" && !Array.isArray(value)
? Object.keys(value) ? Object.keys(value)
.sort() .sort()
.reduce((sorted, key) => { .reduce((sorted, key) => {
@@ -73,341 +102,356 @@ const generateDataHash = (data) => {
const stringToHash = JSON.stringify(data, replacer); const stringToHash = JSON.stringify(data, replacer);
return stringToHash.split('').reduce((a, b) => { return stringToHash.split("").reduce((a, b) => {
a = ((a << 5) - a) + b.charCodeAt(0) a = (a << 5) - a + b.charCodeAt(0);
return a & a return a & a;
}, 0) }, 0);
} catch (e) { } catch (e) {
console.error('Error generating data hash:', e); console.error("Error generating data hash:", e);
return null return null;
} }
} };
// [FIX] Tách hàm dừng polling ra riêng để tái sử dụng // [FIX] Tách hàm dừng polling ra riêng để tái sử dụng
const stopAutoCheck = () => { const stopAutoCheck = () => {
if (pollingInterval.value) { if (pollingInterval.value) {
clearInterval(pollingInterval.value) clearInterval(pollingInterval.value);
pollingInterval.value = null pollingInterval.value = null;
} }
} };
const startAutoCheck = () => { const startAutoCheck = () => {
// [FIX] Dừng interval cũ trước khi tạo mới, tránh tạo nhiều interval chồng nhau // [FIX] Dừng interval cũ trước khi tạo mới, tránh tạo nhiều interval chồng nhau
stopAutoCheck() stopAutoCheck();
if (realtimeConfig.value.time && realtimeConfig.value.time > 0) { if (realtimeConfig.value.time && realtimeConfig.value.time > 0) {
pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000) pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000);
} }
} };
const checkDataChanges = async () => { const checkDataChanges = async () => {
try { try {
const connlist = [] const connlist = [];
const conn1 = $findapi(props.api) const conn1 = $findapi(props.api);
if (vfilter) { if (vfilter) {
const filter = $copy(conn1.params.filter) || {} const filter = $copy(conn1.params.filter) || {};
for (const [key, value] of Object.entries(vfilter)) { for (const [key, value] of Object.entries(vfilter)) {
filter[key] = value filter[key] = value;
} }
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf('$') >= 0) { if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace('$', '')].id filter[key] = store[value.replace("$", "")].id;
} }
} }
conn1.params.filter = filter conn1.params.filter = filter;
} }
if (vparams) conn1.params = $copy(vparams) if (vparams) conn1.params = $copy(vparams);
delete conn1.params.sort delete conn1.params.sort;
delete conn1.params.values delete conn1.params.values;
conn1.params.summary = 'aggregate' conn1.params.summary = "aggregate";
conn1.params.distinct_values = JSON.stringify({ conn1.params.distinct_values = JSON.stringify({
total_count: { type: 'Count', field: 'id' }, total_count: { type: "Count", field: "id" },
last_updated: { type: 'Max', field: 'update_time' }, last_updated: { type: "Max", field: "update_time" },
last_created: { type: 'Max', field: 'create_time' } last_created: { type: "Max", field: "create_time" },
}) });
connlist.push(conn1) connlist.push(conn1);
const rs = await $getapi(connlist) const rs = await $getapi(connlist);
const obj = $find(rs, { name: props.api }) const obj = $find(rs, { name: props.api });
const newMetadata = obj ? obj.data.rows : {} const newMetadata = obj ? obj.data.rows : {};
const newHash = generateDataHash(newMetadata) const newHash = generateDataHash(newMetadata);
if (lastDataHash.value === null) { if (lastDataHash.value === null) {
lastDataHash.value = newHash lastDataHash.value = newHash;
return return;
} }
if (newHash !== lastDataHash.value) { if (newHash !== lastDataHash.value) {
lastDataHash.value = newHash lastDataHash.value = newHash;
if (realtimeConfig.value.update === "true") { if (realtimeConfig.value.update === "true") {
await loadFullDataAsync() await loadFullDataAsync();
emit('dataUpdated', { newData: store[vpagename].data, autoUpdate: true, hasChanges: true }) emit("dataUpdated", {
newData: store[vpagename].data,
autoUpdate: true,
hasChanges: true,
});
} else { } else {
newDataAvailable.value = true newDataAvailable.value = true;
emit('dataUpdated', { newData: null, autoUpdate: false, hasChanges: true }) emit("dataUpdated", {
newData: null,
autoUpdate: false,
hasChanges: true,
});
} }
} }
} catch (error) { } catch (error) {
console.error('Error checking data:', error) console.error("Error checking data:", error);
} }
} };
const loadFullDataAsync = async () => { const loadFullDataAsync = async () => {
try { try {
const connlist = [] const connlist = [];
const conn1 = $findapi(props.api) const conn1 = $findapi(props.api);
if (vfilter) { if (vfilter) {
const filter = $copy(conn1.params.filter) || {} const filter = $copy(conn1.params.filter) || {};
for (const [key, value] of Object.entries(vfilter)) { for (const [key, value] of Object.entries(vfilter)) {
filter[key] = value filter[key] = value;
} }
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf('$') >= 0) { if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace('$', '')].id filter[key] = store[value.replace("$", "")].id;
} }
} }
conn1.params.filter = filter conn1.params.filter = filter;
} }
if (vparams) conn1.params = $copy(vparams) if (vparams) conn1.params = $copy(vparams);
delete conn1.params.summary delete conn1.params.summary;
delete conn1.params.distinct_values delete conn1.params.distinct_values;
connlist.push(conn1) connlist.push(conn1);
const rs = await $getapi(connlist) const rs = await $getapi(connlist);
const obj = $find(rs, { name: props.api }) const obj = $find(rs, { name: props.api });
const newData = obj ? $copy(obj.data.rows) : [] const newData = obj ? $copy(obj.data.rows) : [];
updateDataDisplay(newData) updateDataDisplay(newData);
} catch (error) { } catch (error) {
console.error('Error loading full data:', error) console.error("Error loading full data:", error);
} }
} };
const openImportModal = () => { const openImportModal = () => {
const copy = $copy(props.importdata) const copy = $copy(props.importdata);
showmodal.value = copy showmodal.value = copy;
} };
const updateDataDisplay = (newData) => { const updateDataDisplay = (newData) => {
const copy = $clone(store[vpagename]) const copy = $clone(store[vpagename]);
copy.data = newData copy.data = newData;
copy.update = { data: newData } copy.update = { data: newData };
store.commit(vpagename, copy) store.commit(vpagename, copy);
newDataAvailable.value = false newDataAvailable.value = false;
pendingNewData.value = null pendingNewData.value = null;
} };
const manualRefresh = () => { const manualRefresh = () => {
if (pendingNewData.value) { if (pendingNewData.value) {
updateDataDisplay(pendingNewData.value) updateDataDisplay(pendingNewData.value);
} }
} };
const refreshData = async () => { const refreshData = async () => {
stopAutoCheck() stopAutoCheck();
newDataAvailable.value = false; newDataAvailable.value = false;
pendingNewData.value = null; pendingNewData.value = null;
await getApi(); await getApi();
lastDataHash.value = null; lastDataHash.value = null;
await checkDataChanges(); await checkDataChanges();
newDataAvailable.value = false; newDataAvailable.value = false;
startAutoCheck(); startAutoCheck();
} };
watch(() => props.realtime, (newVal) => { watch(
if (newVal) { () => props.realtime,
realtimeConfig.value.time = newVal.time || 0 (newVal) => {
realtimeConfig.value.update = newVal.update === true if (newVal) {
startAutoCheck() realtimeConfig.value.time = newVal.time || 0;
} realtimeConfig.value.update = newVal.update === true;
}, { deep: true }) startAutoCheck();
}
},
{ deep: true },
);
onDeactivated(() => { onDeactivated(() => {
stopAutoCheck() stopAutoCheck();
}) });
onActivated(() => { onActivated(() => {
startAutoCheck() startAutoCheck();
}) });
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopAutoCheck() stopAutoCheck();
}) });
const timeOption = (v) => { const timeOption = (v) => {
if (!v) return getApi() if (!v) return getApi();
if (v.filter_or) { if (v.filter_or) {
if (vfilter) vfilter = undefined if (vfilter) vfilter = undefined;
if (vparams) { if (vparams) {
vparams.filter_or = v.filter_or vparams.filter_or = v.filter_or;
} else { } else {
const found = $copy($findapi(props.api)) const found = $copy($findapi(props.api));
found.params.filter_or = v.filter_or found.params.filter_or = v.filter_or;
if (props.filter) { if (props.filter) {
const filter = $copy(props.filter) const filter = $copy(props.filter);
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf('$') >= 0) { if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace('$', '')].id filter[key] = store[value.replace("$", "")].id;
} }
} }
found.params.filter = filter found.params.filter = filter;
} }
vparams = found.params vparams = found.params;
} }
return getApi() return getApi();
} }
let filter = vfilter ? vfilter : (props.params ? props.params.filter || {} : {}) let filter = vfilter ? vfilter : props.params ? props.params.filter || {} : {};
for (const [key, value] of Object.entries(v.filter)) { for (const [key, value] of Object.entries(v.filter)) {
filter[key] = value filter[key] = value;
} }
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf('$') >= 0) { if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace('$', '')].id filter[key] = store[value.replace("$", "")].id;
} }
} }
if (vfilter) { if (vfilter) {
vfilter = filter vfilter = filter;
vparams = undefined vparams = undefined;
} else if (vparams) { } else if (vparams) {
vparams.filter = filter vparams.filter = filter;
vparams.filter_or = undefined vparams.filter_or = undefined;
} }
if (!vfilter && !vparams) vfilter = filter if (!vfilter && !vparams) vfilter = filter;
getApi() getApi();
} };
const edit = (v) => { const edit = (v) => {
const copy = props.modal ? $copy(props.modal) : {} const copy = props.modal ? $copy(props.modal) : {};
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v } const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v };
f.pagename = vpagename f.pagename = vpagename;
f.row = v f.row = v;
copy.vbind = f copy.vbind = f;
showmodal.value = copy showmodal.value = copy;
} };
const insert = () => { const insert = () => {
const copy = props.modal ? $copy(props.modal) : {} const copy = props.modal ? $copy(props.modal) : {};
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api } const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api };
f.pagename = vpagename f.pagename = vpagename;
copy.vbind = f copy.vbind = f;
showmodal.value = copy showmodal.value = copy;
} };
const getApi = async () => { const getApi = async () => {
const connlist = [] const connlist = [];
let row = props.setting?.id ? $copy(props.setting) : undefined let row = props.setting?.id ? $copy(props.setting) : undefined;
if (!row) { if (!row) {
const found = $find(store.settings.filter(v => v), props.setting > 0 ? { id: props.setting } : { name: props.setting }) const found = $find(
if (found) row = $copy(found) store.settings.filter((v) => v),
props.setting > 0 ? { id: props.setting } : { name: props.setting },
);
if (found) row = $copy(found);
} }
if (!row) { if (!row) {
const conn = $findapi('usersetting') const conn = $findapi("usersetting");
conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting } conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting };
connlist.push(conn) connlist.push(conn);
} }
let data = props.data ? $copy(props.data) : undefined let data = props.data ? $copy(props.data) : undefined;
if (!data) { if (!data) {
const conn1 = $findapi(props.api) const conn1 = $findapi(props.api);
if (vfilter) { if (vfilter) {
const filter = conn1.params.filter || {} const filter = conn1.params.filter || {};
for (const [key, value] of Object.entries(vfilter)) { for (const [key, value] of Object.entries(vfilter)) {
filter[key] = value filter[key] = value;
} }
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf('$') >= 0) { if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace('$', '')].id filter[key] = store[value.replace("$", "")].id;
} }
} }
conn1.params.filter = filter conn1.params.filter = filter;
} }
if (vparams) conn1.params = vparams if (vparams) conn1.params = vparams;
connection = conn1 connection = conn1;
connlist.push(conn1) connlist.push(conn1);
} }
let obj = undefined let obj = undefined;
if (connlist.length > 0) { if (connlist.length > 0) {
const rs = await $getapi(connlist) const rs = await $getapi(connlist);
const ele = $find(rs, { name: 'usersetting' }) const ele = $find(rs, { name: "usersetting" });
if (ele) { if (ele) {
row = $find(ele.data.rows, { name: props.setting.name || props.setting }) row = $find(ele.data.rows, { name: props.setting.name || props.setting });
const copy = $copy(store.settings) const copy = $copy(store.settings);
copy.push(row) copy.push(row);
store.commit('settings', copy) store.commit("settings", copy);
} }
obj = $find(rs, { name: props.api }) obj = $find(rs, { name: props.api });
if (obj) data = $copy(obj.data.rows) if (obj) data = $copy(obj.data.rows);
} }
pagedata.value = $setpage(vpagename, row, obj) pagedata.value = $setpage(vpagename, row, obj);
const copy = $clone(pagedata.value) const copy = $clone(pagedata.value);
copy.data = data copy.data = data;
copy.update = { data: data } copy.update = { data: data };
store.commit(vpagename, copy) store.commit(vpagename, copy);
} };
const dataEvent = (v, field, data) => { const dataEvent = (v, field, data) => {
if (data?.modal) { if (data?.modal) {
const copy = $copy(data.modal) const copy = $copy(data.modal);
const f = copy.vbind ? copy.vbind : {} const f = copy.vbind ? copy.vbind : {};
if (!f.api) f.api = props.api if (!f.api) f.api = props.api;
f.pagename = vpagename f.pagename = vpagename;
if (!f.row) f.row = v if (!f.row) f.row = v;
copy.vbind = f copy.vbind = f;
copy.field = field copy.field = field;
showmodal.value = copy showmodal.value = copy;
} }
emit('modalevent', { name: 'dataevent', data: { row: v, field: field } }) emit("modalevent", { name: "dataevent", data: { row: v, field: field } });
emit('dataevent', v, field) emit("dataevent", v, field);
} };
const exportExcel = async () => { const exportExcel = async () => {
if (!props.api) return if (!props.api) return;
const found = $findapi('exportcsv') const found = $findapi("exportcsv");
found.params = connection.params found.params = connection.params;
const fields = pagedata.value.fields const fields = pagedata.value.fields
.filter(v => (v.show && v.export !== 'no') || v.export === 'yes') .filter((v) => (v.show && v.export !== "no") || v.export === "yes")
.map(x => ({ name: x.name, label: $stripHtml(x.label) })) .map((x) => ({ name: x.name, label: $stripHtml(x.label) }));
found.params.fields = JSON.stringify(fields) found.params.fields = JSON.stringify(fields);
found.url = connection.url.replace('data/', 'exportcsv/') found.url = connection.url.replace("data/", "exportcsv/");
const rs = await $getapi([found]) const rs = await $getapi([found]);
if (rs === 'error') { if (rs === "error") {
$snackbar('Đã xảy ra lỗi. Vui lòng thử lại.') $snackbar("Đã xảy ra lỗi. Vui lòng thử lại.");
return return;
} }
const url = window.URL.createObjectURL(new Blob([rs[0].data])) const url = window.URL.createObjectURL(new Blob([rs[0].data]));
const link = document.createElement('a') const link = document.createElement("a");
const fileName = `${$dayjs(new Date()).format('YYYYMMDDhhmmss')}-data.csv` const fileName = `${$dayjs(new Date()).format("YYYYMMDDhhmmss")}-data.csv`;
link.href = url link.href = url;
link.setAttribute('download', fileName) link.setAttribute("download", fileName);
document.body.appendChild(link) document.body.appendChild(link);
link.click() link.click();
link.remove() link.remove();
} };
if (!props.timeopt) await getApi() if (!props.timeopt) await getApi();
startAutoCheck() startAutoCheck();
</script> </script>

View File

@@ -1,82 +1,111 @@
<template> <template>
<div> <div>
<p class="fsb-20 mb-5">Điều chỉnh tiêu đề</p> <p class="fsb-20 mb-5">Điều chỉnh tiêu đề</p>
<div v-for="(v, i) in arr" :key="i" :class="(i>0? 'mt-4' : null)"> <div
<p class="fsb-14">Dòng thứ {{(i+1)}}<span class="has-text-danger"> *</span></p> v-for="(v, i) in arr"
<div class="field has-addons mt-1"> :key="i"
<div class="control is-expanded"> :class="i > 0 ? 'mt-4' : null"
<input class="input" type="text" v-model="v.label"> >
</div> <p class="fsb-14">Dòng thứ {{ i + 1 }}<span class="has-text-danger"> *</span></p>
<div class="control"> <div class="field has-addons mt-1">
<a class="button px-2 is-primary" @click="add()"> <div class="control is-expanded">
<span> <input
<SvgIcon v-bind="{name: 'add1.png', type: 'white', size: 17}"></SvgIcon></span> class="input"
</a> type="text"
</div> v-model="v.label"
<div class="control" @click="remove(i)" v-if="(i>0)"> />
<a class="button px-2 is-dark"> </div>
<span> <div class="control">
<SvgIcon v-bind="{name: 'bin.svg', type: 'white', size: 17}"></SvgIcon> <a
</span> class="button px-2 is-primary"
</a> @click="add()"
</div> >
</div> <span> <SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 17 }"></SvgIcon></span>
<p class="help has-text-danger" v-if="v.error"> {{v.error}} </p> </a>
</div> </div>
<div class="buttons mt-5"> <div
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button> class="control"
<button class="button is-dark" @click="$emit('close')">Hủy bỏ</button> @click="remove(i)"
</div> v-if="i > 0"
>
<a class="button px-2 is-dark">
<span>
<SvgIcon v-bind="{ name: 'bin.svg', type: 'white', size: 17 }"></SvgIcon>
</span>
</a>
</div>
</div>
<p
class="help has-text-danger"
v-if="v.error"
>
{{ v.error }}
</p>
</div>
<div class="buttons mt-5">
<button
class="button is-primary has-text-white"
@click="update()"
>
Cập nhật
</button>
<button
class="button is-dark"
@click="$emit('close')"
>
Hủy bỏ
</button>
</div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['label'], props: ["label"],
data() { data() {
return { return {
arr: [] arr: [],
} };
}, },
created() { created() {
let arr1 = this.label.replace('<div>', '').replace('</div>', '').split("</p>") let arr1 = this.label.replace("<div>", "").replace("</div>", "").split("</p>");
arr1.map(v=>{ arr1.map((v) => {
if(!this.$empty(v)) { if (!this.$empty(v)) {
let label = v + '</p>' let label = v + "</p>";
label = this.$stripHtml(label) label = this.$stripHtml(label);
this.arr.push({label: label}) this.arr.push({ label: label });
} }
}) });
}, },
methods: { methods: {
add() { add() {
this.arr.push({label: undefined}) this.arr.push({ label: undefined });
}, },
remove(i) { remove(i) {
this.$remove(this.arr, i) this.$remove(this.arr, i);
}, },
checkError() { checkError() {
let error = false let error = false;
this.arr.map(v=>{ this.arr.map((v) => {
if(this.$empty(v.label)) { if (this.$empty(v.label)) {
v.error = 'Nội dung không được bỏ trống' v.error = "Nội dung không được bỏ trống";
error = true error = true;
} }
}) });
if(error) this.arr = this.$copy(this.arr) if (error) this.arr = this.$copy(this.arr);
return error return error;
}, },
update() { update() {
if(this.checkError()) return if (this.checkError()) return;
let label = '' let label = "";
if(this.arr.length>1) { if (this.arr.length > 1) {
this.arr.map((v,i)=>{ this.arr.map((v, i) => {
label += `<p${i<this.arr.length-1? ' style="border-bottom: 1px solid white;"' : ''}>${v.label.trim()}</p>` label += `<p${i < this.arr.length - 1 ? ' style="border-bottom: 1px solid white;"' : ""}>${v.label.trim()}</p>`;
}) });
label = `<div>${label}</div>` label = `<div>${label}</div>`;
} else label = this.arr[0].label.trim() } else label = this.arr[0].label.trim();
this.$emit('modalevent', {name: 'label', data: label}) this.$emit("modalevent", { name: "label", data: label });
this.$emit('close') this.$emit("close");
} },
} },
} };
</script> </script>

View File

@@ -1,88 +1,127 @@
<template> <template>
<div> <div>
<template v-if="keys.length>0"> <template v-if="keys.length > 0">
<div class="field is-horizontal" v-for="(v,i) in keys" :key="i"> <div
<div class="field-body"> class="field is-horizontal"
<div class="field is-narrow"> v-for="(v, i) in keys"
<div class="control"> :key="i"
<input class="input fs-14" type="text" placeholder="" v-model="keys[i]"> >
</div> <div class="field-body">
</div> <div class="field is-narrow">
<div class="field"> <div class="control">
<div class="control"> <input
<input class="input fs-14" type="text" placeholder="" v-model="values[i]"> class="input fs-14"
type="text"
placeholder=""
v-model="keys[i]"
/>
</div>
</div>
<div class="field">
<div class="control">
<input
class="input fs-14"
type="text"
placeholder=""
v-model="values[i]"
/>
</div>
</div>
<div class="field is-narrow">
<p class="control">
<a @click="addAttr()">
<SvgIcon v-bind="{ name: 'add1.png', type: 'gray', size: 18 }"></SvgIcon>
</a>
<a
class="ml-2"
@click="remove(i)"
>
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
<a
class="ml-2"
@click="jsonData(v, i)"
>
<SvgIcon v-bind="{ name: 'apps.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
</p>
</div>
</div>
</div> </div>
</div>
<div class="field is-narrow">
<p class="control">
<a @click="addAttr()">
<SvgIcon v-bind="{name: 'add1.png', type: 'gray', size: 18}"></SvgIcon>
</a>
<a class="ml-2" @click="remove(i)">
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 18}"></SvgIcon>
</a>
<a class="ml-2" @click="jsonData(v, i)">
<SvgIcon v-bind="{name: 'apps.svg', type: 'gray', size: 18}"></SvgIcon>
</a>
</p>
</div>
</div>
</div>
</template> </template>
<div class="mb-6" v-else> <div
<button class="button is-primary has-text-white" @click="addAttr()">Thêm thuộc tính</button> class="mb-6"
v-else
>
<button
class="button is-primary has-text-white"
@click="addAttr()"
>
Thêm thuộc tính
</button>
</div> </div>
<div class="buttons mt-5"> <div class="buttons mt-5">
<a class="button is-primary has-text-white" @click="update()">Cập nhật</a> <a
class="button is-primary has-text-white"
@click="update()"
>Cập nhật</a
>
</div> </div>
<Modal @close="comp=undefined" @update="doUpdate" <Modal
v-bind="{component: comp, width: '40%', height: '300px', vbind: vbind}" v-if="comp"></Modal> @close="comp = undefined"
@update="doUpdate"
v-bind="{ component: comp, width: '40%', height: '300px', vbind: vbind }"
v-if="comp"
></Modal>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['field', 'close'], props: ["field", "close"],
data() { data() {
return { return {
keys: [], keys: [],
values: [], values: [],
comp: undefined, comp: undefined,
vbind: undefined, vbind: undefined,
current: undefined current: undefined,
} };
},
created() {
Object.keys(this.field).map((v) => {
this.keys.push(v);
this.values.push(this.field[v]);
});
},
methods: {
doUpdate(v) {
this.values[this.current.i] = v;
}, },
created() { jsonData(v, i) {
Object.keys(this.field).map(v=>{ this.current = { v: v, i: i };
this.keys.push(v) this.vbind = {
this.values.push(this.field[v]) field: this.$empty(this.values[i]) || typeof this.values[i] === "string" ? {} : this.values[i],
}) close: true,
};
this.comp = "datatable/FieldAttribute";
}, },
methods: { addAttr() {
doUpdate(v) { this.keys.push(undefined);
this.values[this.current.i] = v this.values.push(undefined);
}, },
jsonData(v, i) { remove(i) {
this.current = {v: v, i: i} this.$remove(this.keys, i);
this.vbind = {field: this.$empty(this.values[i]) || typeof this.values[i] === 'string'? {} : this.values[i], close: true} this.$remove(this.values, i);
this.comp = 'datatable/FieldAttribute' },
}, update() {
addAttr() { let obj = {};
this.keys.push(undefined) this.keys.map((v, i) => {
this.values.push(undefined) if (!this.$empty(v)) obj[v] = v.indexOf("__in") > 0 ? this.values[i].split(",") : this.values[i];
}, });
remove(i) { this.$emit("update", obj);
this.$remove(this.keys, i) this.$emit("modalevent", { name: "update", data: obj });
this.$remove(this.values, i) if (this.close) this.$emit("close");
}, },
update() { },
let obj = {} };
this.keys.map((v,i)=>{ </script>
if(!this.$empty(v)) obj[v] = v.indexOf('__in')>0? this.values[i].split(',') : this.values[i]
})
this.$emit('update', obj)
this.$emit('modalevent', {name: 'update', data: obj})
if(this.close) this.$emit('close')
}
}
}
</script>

View File

@@ -1,52 +1,88 @@
<template> <template>
<div> <div>
<div class="field mt-3 mb-1" v-if="field.format==='number'"> <div
class="field mt-3 mb-1"
v-if="field.format === 'number'"
>
<label class="label fs-14">Chọn trường<span class="has-text-danger"> * </span> </label> <label class="label fs-14">Chọn trường<span class="has-text-danger"> * </span> </label>
<div class="control"> <div class="control">
<b-taginput <b-taginput
size="is-small" size="is-small"
v-model="tagsField" v-model="tagsField"
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []" :data="pageData ? pageData.fields.filter((v) => v.format === 'number') : []"
type="is-dark is-light" type="is-dark is-light"
autocomplete autocomplete
:open-on-focus="true" :open-on-focus="true"
field="name" field="name"
icon="plus" icon="plus"
placeholder="Chọn trường" placeholder="Chọn trường"
> >
<template slot-scope="props"> <template slot-scope="props">
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span> <span class="mr-3 has-text-danger has-text-weight-bold"> {{ props.option.name }}</span>
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 60)}} </span> <span :class="tagsField.find((v) => v.id === props.option.id) ? 'has-text-dark' : ''">
</template> {{ $stripHtml(props.option.label, 60) }}
<template slot="empty"> </span>
Không trường thỏa mãn
</template> </template>
<template slot="empty"> Không trường thỏa mãn </template>
</b-taginput> </b-taginput>
</div> </div>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p> <p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'tagsField')"
>
{{ errors.find((v) => v.name === "tagsField").message }}
</p>
</div> </div>
<div class="mt-2" v-if="tagsField.length>0"> <div
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name" class="mt-2"
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i"> v-if="tagsField.length > 0"
<span class="tooltip"> >
{{ v.name }} <a
<span class="tooltiptext" style="top: 60%; bottom: unset; min-width: max-content; left: 25px;">{{ $stripHtml(v.label) }}</span> @dblclick="expression = expression ? expression + ' ' + v.name : v.name"
</span> class="tag is-rounded"
v-for="(v, i) in tagsField"
:key="i"
>
<span class="tooltip">
{{ v.name }}
<span
class="tooltiptext"
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
>{{ $stripHtml(v.label) }}</span
>
</span>
</a> </a>
</div> </div>
<div class="field is-horizontal mt-3"> <div class="field is-horizontal mt-3">
<div class="field-body"> <div class="field-body">
<div class="field" v-if="field.format==='number'"> <div
<label class="label fs-14">Biểu thức dạng Đúng / Sai <span class="has-text-danger"> * </span> </label> class="field"
v-if="field.format === 'number'"
>
<label class="label fs-14"
>Biểu thức dạng Đúng / Sai
<span class="has-text-danger"> * </span>
</label>
<p class="control is-expanded"> <p class="control is-expanded">
<input class="input is-small" type="text" v-model="expression"> <input
</p> class="input is-small"
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p> type="text"
v-model="expression"
/>
</p>
<p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'expression')"
>
{{ errors.find((v) => v.name === "expression").message }}
</p>
</div> </div>
<div class="field" v-else> <div
<label class="label"> Chuỗi tự <span class="has-text-danger"> * </span> class="field"
</label> v-else
>
<label class="label"> Chuỗi tự <span class="has-text-danger"> * </span> </label>
<p class="control"> <p class="control">
<input <input
class="input is-small" class="input is-small"
@@ -63,27 +99,53 @@
{{ errors.find((v) => v.name === "searchText").msg }} {{ errors.find((v) => v.name === "searchText").msg }}
</p> </p>
</div> </div>
<div class="field is-narrow" v-if="filterType==='color'"> <div
<label class="label fs-14"> màu <span class="has-text-danger"> * </span> </label> class="field is-narrow"
<p class="control fs-14"> v-if="filterType === 'color'"
<input type="color" v-model="color" @change="changeStyle()"> >
</p> <label class="label fs-14"> màu <span class="has-text-danger"> * </span> </label>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='color')"> {{errors.find(v=>v.name==='color').message}} </p> <p class="control fs-14">
</div> <input
<div class="field is-narrow" v-else-if="filterType==='size'"> type="color"
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label> v-model="color"
<p class="control fs-14"> @change="changeStyle()"
<input class="input is-small" type="text" placeholder="Nhập số" v-model="size" @change="changeStyle()"> />
</p> </p>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='size')"> {{errors.find(v=>v.name==='size').message}} </p> <p
</div> class="help has-text-danger"
v-if="errors.find((v) => v.name === 'color')"
>
{{ errors.find((v) => v.name === "color").message }}
</p>
</div>
<div
class="field is-narrow"
v-else-if="filterType === 'size'"
>
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
class="input is-small"
type="text"
placeholder="Nhập số"
v-model="size"
@change="changeStyle()"
/>
</p>
<p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'size')"
>
{{ errors.find((v) => v.name === "size").message }}
</p>
</div> </div>
</div> </div>
</div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['filterObj', 'filterType', 'pagename', 'field'], props: ["filterObj", "filterType", "pagename", "field"],
data() { data() {
return { return {
tagsField: [], tagsField: [],
@@ -92,79 +154,113 @@ export default {
color: undefined, color: undefined,
size: undefined, size: undefined,
errors: [], errors: [],
searchText: undefined searchText: undefined,
} };
}, },
created() { created() {
this.color = this.filterObj.color this.color = this.filterObj.color;
this.size = this.filterObj.size this.size = this.filterObj.size;
this.expression = this.filterObj.expression? this.filterObj.expression : this.field.name this.expression = this.filterObj.expression ? this.filterObj.expression : this.field.name;
if(this.filterObj.tags) { if (this.filterObj.tags) {
this.filterObj.tags.map(v=>{ this.filterObj.tags.map((v) => {
this.tagsField.push(this.pageData.fields.find(x=>x.name===v)) this.tagsField.push(this.pageData.fields.find((x) => x.name === v));
}) });
} else if(this.field.format==='number') this.tagsField.push(this.pageData.fields.find(v=>v.name===this.field.name)) } else if (this.field.format === "number")
this.tagsField.push(this.pageData.fields.find((v) => v.name === this.field.name));
}, },
watch: { watch: {
expression: function(newVal) { expression: function (newVal) {
if(this.$empty(newVal)) return if (this.$empty(newVal)) return;
else this.changeStyle() else this.changeStyle();
} },
}, },
computed: { computed: {
colorscheme: { colorscheme: {
get: function() {return this.$store.state.colorscheme}, get: function () {
set: function(val) {this.$store.commit("updateColorScheme", {colorscheme: val})} return this.$store.state.colorscheme;
},
set: function (val) {
this.$store.commit("updateColorScheme", { colorscheme: val });
},
}, },
pageData: { pageData: {
get: function() {return this.$store.state[this.pagename]}, get: function () {
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})} return this.$store.state[this.pagename];
},
set: function (val) {
this.$store.commit("updateStore", { name: this.pagename, data: val });
},
}, },
colorchoice: { colorchoice: {
get: function() {return this.$store.state.colorchoice}, get: function () {
set: function(val) {this.$store.commit("updateColorChoice", {colorchoice: val})} return this.$store.state.colorchoice;
} },
set: function (val) {
this.$store.commit("updateColorChoice", { colorchoice: val });
},
},
}, },
methods: { methods: {
changeStyle() { changeStyle() {
let check = this.field.format==='number'? this.checkExpression() : this.checkCondition() let check = this.field.format === "number" ? this.checkExpression() : this.checkCondition();
if(!check) return if (!check) return;
var row = this.field.format==='number'? {expression: this.expression, tags: this.tagsField.map(v=>v.name)} var row =
: {keyword: this.searchText, type: 'search'} this.field.format === "number"
this.filterType==='color'? row.color = this.color : row.size = this.size ? {
this.$emit('databack', row) expression: this.expression,
tags: this.tagsField.map((v) => v.name),
}
: { keyword: this.searchText, type: "search" };
this.filterType === "color" ? (row.color = this.color) : (row.size = this.size);
this.$emit("databack", row);
}, },
checkCondition() { checkCondition() {
this.errors = [] this.errors = [];
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'}) if (this.filterType === "color" && this.$empty(this.color))
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'}) this.errors.push({ name: "color", message: "Chọn màu" });
if(this.$empty(this.searchText)) this.errors.push({name: 'searchText', message: 'Chưa nhập chuỗi kí tự'}) if (this.filterType === "size" && this.$empty(this.size))
return this.errors.length>0? false : true this.errors.push({ name: "size", message: "Nhập cỡ chữ" });
if (this.$empty(this.searchText))
this.errors.push({
name: "searchText",
message: "Chưa nhập chuỗi kí tự",
});
return this.errors.length > 0 ? false : true;
}, },
checkExpression() { checkExpression() {
this.errors = [] this.errors = [];
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'}) if (this.filterType === "color" && this.$empty(this.color))
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'}) this.errors.push({ name: "color", message: "Chọn màu" });
let val = this.$copy(this.expression) if (this.filterType === "size" && this.$empty(this.size))
let exp = this.$copy(this.expression) this.errors.push({ name: "size", message: "Nhập cỡ chữ" });
this.tagsField.forEach(v => { let val = this.$copy(this.expression);
let myRegExp = new RegExp(v.name, 'g') let exp = this.$copy(this.expression);
val = val.replace(myRegExp, Math.random()) this.tagsField.forEach((v) => {
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])") let myRegExp = new RegExp(v.name, "g");
}) val = val.replace(myRegExp, Math.random());
try { exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])");
let value = this.$calc(val) });
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) { try {
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'}) let value = this.$calc(val);
} else if(!(eval(value)===true || eval(value)===false)) { if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'}) this.errors.push({
name: "expression",
message: "Biểu thức không hợp lệ",
});
} else if (!(eval(value) === true || eval(value) === false)) {
this.errors.push({
name: "expression",
message: "Biểu thức không hợp lệ",
});
} }
} } catch (err) {
catch(err) { this.errors.push({
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'}) name: "expression",
message: "Biểu thức không hợp lệ",
});
} }
return this.errors.length>0? false : true return this.errors.length > 0 ? false : true;
} },
} },
} };
</script> </script>

View File

@@ -5,7 +5,12 @@
</p> </p>
<div class="tabs is-boxed"> <div class="tabs is-boxed">
<ul> <ul>
<li v-for="(v, i) in tabs" :key="i" :class="tab.code === v.code ? 'is-active' : ''" @click="tab = v"> <li
v-for="(v, i) in tabs"
:key="i"
:class="tab.code === v.code ? 'is-active' : ''"
@click="tab = v"
>
<a>{{ v.name }}</a> <a>{{ v.name }}</a>
</li> </li>
</ul> </ul>
@@ -14,17 +19,37 @@
<template v-if="tab.code === 'expression' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)"> <template v-if="tab.code === 'expression' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
<template v-if="radio ? radio.code === 'condition' && sideBar === 'bgcolor' : false"> <template v-if="radio ? radio.code === 'condition' && sideBar === 'bgcolor' : false">
<div v-for="(v, i) in bgcolorFilter" :key="v.id" class="px-4"> <div
v-for="(v, i) in bgcolorFilter"
:key="v.id"
class="px-4"
>
<FilterOption <FilterOption
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }" v-bind="{
filterObj: v,
filterType: 'color',
pagename: pagename,
field: openField,
}"
:ref="v.id" :ref="v.id"
@databack="doConditionFilter($event, 'bgcolor', v.id)" @databack="doConditionFilter($event, 'bgcolor', v.id)"
/> />
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"> <p
<a class="has-text-primary mr-5" @click="addCondition(bgcolorFilter)" v-if="bgcolorFilter.length <= 30"> class="fs-14 mt-1"
:class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
>
<a
class="has-text-primary mr-5"
@click="addCondition(bgcolorFilter)"
v-if="bgcolorFilter.length <= 30"
>
Thêm Thêm
</a> </a>
<a class="has-text-danger" @click="removeCondition(bgcolorFilter, i)" v-if="bgcolorFilter.length > 1"> <a
class="has-text-danger"
@click="removeCondition(bgcolorFilter, i)"
v-if="bgcolorFilter.length > 1"
>
Bớt Bớt
</a> </a>
</p> </p>
@@ -32,29 +57,77 @@
</template> </template>
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'color' : false"> <template v-else-if="radio ? radio.code === 'condition' && sideBar === 'color' : false">
<div v-for="(v, i) in colorFilter" :key="v.id" class="px-4"> <div
v-for="(v, i) in colorFilter"
:key="v.id"
class="px-4"
>
<FilterOption <FilterOption
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }" v-bind="{
filterObj: v,
filterType: 'color',
pagename: pagename,
field: openField,
}"
:ref="v.id" :ref="v.id"
@databack="doConditionFilter($event, 'color', v.id)" @databack="doConditionFilter($event, 'color', v.id)"
/> />
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"> <p
<a class="has-text-primary mr-5" @click="addCondition(colorFilter)" v-if="colorFilter.length <= 30"> Thêm </a> class="fs-14 mt-1"
<a class="has-text-danger" @click="removeCondition(colorFilter, i)" v-if="colorFilter.length > 1"> Bớt </a> :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
>
<a
class="has-text-primary mr-5"
@click="addCondition(colorFilter)"
v-if="colorFilter.length <= 30"
>
Thêm
</a>
<a
class="has-text-danger"
@click="removeCondition(colorFilter, i)"
v-if="colorFilter.length > 1"
>
Bớt
</a>
</p> </p>
</div> </div>
</template> </template>
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'textsize' : false"> <template v-else-if="radio ? radio.code === 'condition' && sideBar === 'textsize' : false">
<div v-for="(v, i) in sizeFilter" :key="v.id" class="px-4"> <div
v-for="(v, i) in sizeFilter"
:key="v.id"
class="px-4"
>
<FilterOption <FilterOption
v-bind="{ filterObj: v, filterType: 'size', pagename: pagename, field: openField }" v-bind="{
filterObj: v,
filterType: 'size',
pagename: pagename,
field: openField,
}"
:ref="v.id" :ref="v.id"
@databack="doConditionFilter($event, 'textsize', v.id)" @databack="doConditionFilter($event, 'textsize', v.id)"
/> />
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"> <p
<a class="has-text-primary mr-5" @click="addCondition(sizeFilter)" v-if="sizeFilter.length <= 30"> Thêm </a> class="fs-14 mt-1"
<a class="has-text-danger" @click="removeCondition(sizeFilter, i)" v-if="sizeFilter.length > 1"> Bớt </a> :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
>
<a
class="has-text-primary mr-5"
@click="addCondition(sizeFilter)"
v-if="sizeFilter.length <= 30"
>
Thêm
</a>
<a
class="has-text-danger"
@click="removeCondition(sizeFilter, i)"
v-if="sizeFilter.length > 1"
>
Bớt
</a>
</p> </p>
</div> </div>
</template> </template>
@@ -62,21 +135,39 @@
<template v-else-if="tab.code === 'script' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)"> <template v-else-if="tab.code === 'script' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
<p class="my-4 mx-4"> <p class="my-4 mx-4">
<a @click="copyContent(script ? script : '')" class="mr-6"> <a
@click="copyContent(script ? script : '')"
class="mr-6"
>
<span class="icon-text"> <span class="icon-text">
<SvgIcon class="mr-2" v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"></SvgIcon> <SvgIcon
class="mr-2"
v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"
></SvgIcon>
<span class="fs-16">Copy</span> <span class="fs-16">Copy</span>
</span> </span>
</a> </a>
<a @click="paste()" class="mr-6"> <a
@click="paste()"
class="mr-6"
>
<span class="icon-text"> <span class="icon-text">
<SvgIcon class="mr-2" v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"></SvgIcon> <SvgIcon
class="mr-2"
v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"
></SvgIcon>
<span class="fs-16">Paste</span> <span class="fs-16">Paste</span>
</span> </span>
</a> </a>
</p> </p>
<div class="mx-4"> <div class="mx-4">
<textarea class="textarea fs-14" rows="8" v-model="script" @change="checkScript()" @dblclick="doCheck"></textarea> <textarea
class="textarea fs-14"
rows="8"
v-model="script"
@change="checkScript()"
@dblclick="doCheck"
></textarea>
</div> </div>
<p class="mt-5 mx-4"> <p class="mt-5 mx-4">
<span class="icon-text fsb-18"> <span class="icon-text fsb-18">
@@ -87,14 +178,29 @@
<div class="field is-grouped mx-4 mt-4"> <div class="field is-grouped mx-4 mt-4">
<div class="control"> <div class="control">
<p class="fsb-14 mb-1">Đoạn text</p> <p class="fsb-14 mb-1">Đoạn text</p>
<input class="input" type="text" placeholder="" v-model="source" /> <input
class="input"
type="text"
placeholder=""
v-model="source"
/>
</div> </div>
<div class="control"> <div class="control">
<p class="fsb-14 mb-1">Thay bằng</p> <p class="fsb-14 mb-1">Thay bằng</p>
<input class="input" type="text" placeholder="" v-model="target" /> <input
class="input"
type="text"
placeholder=""
v-model="target"
/>
</div> </div>
<div class="control pl-5"> <div class="control pl-5">
<button class="button is-primary is-rounded is-outlined mt-5" @click="replace()">Replace</button> <button
class="button is-primary is-rounded is-outlined mt-5"
@click="replace()"
>
Replace
</button>
</div> </div>
</div> </div>
<p class="mt-5 pt-2 mx-4"> <p class="mt-5 pt-2 mx-4">
@@ -103,10 +209,24 @@
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
</span> </span>
</p> </p>
<p class="mx-4 mt-4"><button class="button is-primary is-rounded" @click="changeScript()">Cập nhật</button></p> <p class="mx-4 mt-4">
<button
class="button is-primary is-rounded"
@click="changeScript()"
>
Cập nhật
</button>
</p>
</template> </template>
<TableOption v-bind="{ pagename: pagename }" v-else-if="sideBar === 'option'"> </TableOption> <TableOption
<CreateTemplate v-else-if="sideBar === 'template'" v-bind="{ pagename: pagename, field: openField }"> v-bind="{ pagename: pagename }"
v-else-if="sideBar === 'option'"
>
</TableOption>
<CreateTemplate
v-else-if="sideBar === 'template'"
v-bind="{ pagename: pagename, field: openField }"
>
</CreateTemplate> </CreateTemplate>
</template> </template>
<script setup> <script setup>

View File

@@ -1,5 +1,8 @@
<template> <template>
<div class="mb-4" v-if="currentsetting ? currentsetting.user === login.id : false"> <div
class="mb-4"
v-if="currentsetting ? currentsetting.user === login.id : false"
>
<p class="fs-16 has-text-findata"> <p class="fs-16 has-text-findata">
Đang mở: <b>{{ $stripHtml(currentsetting.name, 40) }}</b> Đang mở: <b>{{ $stripHtml(currentsetting.name, 40) }}</b>
</p> </p>
@@ -7,7 +10,11 @@
<div class="field"> <div class="field">
<label class="label fs-14">Chọn chế độ lưu <span class="has-text-danger"> * </span></label> <label class="label fs-14">Chọn chế độ lưu <span class="has-text-danger"> * </span></label>
<div class="control is-expanded fs-14"> <div class="control is-expanded fs-14">
<a class="mr-5" v-if="isOverwrite()" @click="changeType('overwrite')"> <a
class="mr-5"
v-if="isOverwrite()"
@click="changeType('overwrite')"
>
<span class="icon-text"> <span class="icon-text">
<SvgIcon <SvgIcon
v-bind="{ v-bind="{
@@ -22,7 +29,11 @@
<a @click="changeType('new')"> <a @click="changeType('new')">
<span class="icon-text"> <span class="icon-text">
<SvgIcon <SvgIcon
v-bind="{ name: radioSave === 'new' ? 'radio-checked.svg' : 'radio-unchecked.svg', type: 'gray', size: 22 }" v-bind="{
name: radioSave === 'new' ? 'radio-checked.svg' : 'radio-unchecked.svg',
type: 'gray',
size: 22,
}"
></SvgIcon> ></SvgIcon>
Tạo mới Tạo mới
</span> </span>
@@ -33,16 +44,30 @@
<div class="field mt-4 px-0 mx-0"> <div class="field mt-4 px-0 mx-0">
<label class="label fs-14">Tên thiết lập <span class="has-text-danger"> * </span></label> <label class="label fs-14">Tên thiết lập <span class="has-text-danger"> * </span></label>
<div class="control"> <div class="control">
<input class="input" type="text" placeholder="" v-model="name" ref="name" v-on:keyup.enter="saveSetting" /> <input
class="input"
type="text"
placeholder=""
v-model="name"
ref="name"
v-on:keyup.enter="saveSetting"
/>
</div> </div>
<div class="help has-text-danger" v-if="errors.find((v) => v.name === 'name')"> <div
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'name')"
>
{{ errors.find((v) => v.name === "name").msg }} {{ errors.find((v) => v.name === "name").msg }}
</div> </div>
</div> </div>
<div class="field mt-4 px-0 mx-0"> <div class="field mt-4 px-0 mx-0">
<label class="label fs-14"> Mô tả </label> <label class="label fs-14"> Mô tả </label>
<p class="control is-expanded"> <p class="control is-expanded">
<textarea class="textarea" rows="4" v-model="note"></textarea> <textarea
class="textarea"
rows="4"
v-model="note"
></textarea>
</p> </p>
</div> </div>
<!-- <!--
@@ -60,11 +85,19 @@
</div>--> </div>-->
</template> </template>
<div class="field mt-5 px-0 mx-0"> <div class="field mt-5 px-0 mx-0">
<label class="label fs-14" v-if="status !== undefined" :class="status ? 'has-text-primary' : 'has-text-danger'"> <label
class="label fs-14"
v-if="status !== undefined"
:class="status ? 'has-text-primary' : 'has-text-danger'"
>
{{ status ? "Lưu thiết lập thành công." : "Lỗi. Lưu thiết lập thất bại." }} {{ status ? "Lưu thiết lập thành công." : "Lỗi. Lưu thiết lập thất bại." }}
</label> </label>
<p class="control is-expanded"> <p class="control is-expanded">
<a class="button is-primary has-text-white" @click="saveSetting()">Lưu lại</a> <a
class="button is-primary has-text-white"
@click="saveSetting()"
>Lưu lại</a
>
</p> </p>
</div> </div>
</template> </template>
@@ -118,7 +151,10 @@ async function saveSetting() {
let result; let result;
if (radioSave.value === "new") { if (radioSave.value === "new") {
if ($empty(name)) { if ($empty(name)) {
return errors.push({ name: "name", msg: "Tên thiết lập không được bỏ trống" }); return errors.push({
name: "name",
msg: "Tên thiết lập không được bỏ trống",
});
} }
result = await $insertapi("usersetting", data); result = await $insertapi("usersetting", data);
} else { } else {

View File

@@ -1,52 +1,114 @@
<template> <template>
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li :class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs"> <li
:class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`"
v-for="v in tabs"
>
<a @click="changeTab(v)">{{ v.name }}</a> <a @click="changeTab(v)">{{ v.name }}</a>
</li> </li>
</ul> </ul>
</div> </div>
<template v-if="tab === 'datatype'"> <template v-if="tab === 'datatype'">
<Caption class="mb-3" v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"></Caption> <Caption
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields"> class="mb-3"
v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"
></Caption>
<div
class="py-1 border-bottom is-clickable"
v-for="x in current.fields"
>
{{ x.name }} {{ x.name }}
<span class="ml-6 has-text-grey">{{ x.type }}</span> <span class="ml-6 has-text-grey">{{ x.type }}</span>
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a> <a
class="ml-6 has-text-primary"
v-if="x.model"
@click="openModel(x)"
>{{ x.model }}</a
>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="columns mx-0 mb-0 pb-0"> <div class="columns mx-0 mb-0 pb-0">
<div class="column is-7"> <div class="column is-7">
<Caption class="mb-2" v-bind="{ title: 'Values', type: 'has-text-warning' }"></Caption> <Caption
<input class="input" rows="1" v-model="values" /> class="mb-2"
v-bind="{ title: 'Values', type: 'has-text-warning' }"
></Caption>
<input
class="input"
rows="1"
v-model="values"
/>
</div> </div>
<div class="column is-4s"> <div class="column is-4s">
<Caption class="mb-2" v-bind="{ title: 'Filter', type: 'has-text-warning' }"></Caption> <Caption
<input class="input" rows="1" v-model="filter" /> class="mb-2"
v-bind="{ title: 'Filter', type: 'has-text-warning' }"
></Caption>
<input
class="input"
rows="1"
v-model="filter"
/>
</div> </div>
<div class="column is-1"> <div class="column is-1">
<Caption class="mb-2" v-bind="{ title: 'Load', type: 'has-text-warning' }"></Caption> <Caption
class="mb-2"
v-bind="{ title: 'Load', type: 'has-text-warning' }"
></Caption>
<div> <div>
<button class="button is-primary has-text-white" @click="loadData()">Load</button> <button
class="button is-primary has-text-white"
@click="loadData()"
>
Load
</button>
</div> </div>
</div> </div>
</div> </div>
<Caption class="mb-2" v-bind="{ title: 'Query', type: 'has-text-warning' }"></Caption> <Caption
class="mb-2"
v-bind="{ title: 'Query', type: 'has-text-warning' }"
></Caption>
<div class="mb-4"> <div class="mb-4">
{{ query }} {{ query }}
<a class="has-text-primary ml-5" @click="copy()">copy</a> <a
class="has-text-primary ml-5"
@click="copy()"
>copy</a
>
<p> <p>
{{ apiUrl }} {{ apiUrl }}
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a> <a
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a> class="has-text-primary ml-5"
@click="$copyToClipboard(apiUrl)"
>copy</a
>
<a
class="has-text-primary ml-5"
target="_blank"
:href="apiUrl"
>open</a
>
</p> </p>
</div> </div>
<div> <div>
<Caption class="mb-2" v-bind="{ title: 'Data', type: 'has-text-warning' }"></Caption> <Caption
<DataTable v-bind="{ pagename: pagename }" v-if="pagedata" /> class="mb-2"
v-bind="{ title: 'Data', type: 'has-text-warning' }"
></Caption>
<DataTable
v-bind="{ pagename: pagename }"
v-if="pagedata"
/>
</div> </div>
</template> </template>
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template> </template>
<script setup> <script setup>
import { useStore } from "@/stores/index"; import { useStore } from "@/stores/index";

View File

@@ -1,26 +1,45 @@
<template> <template>
<div class="tabs is-boxed"> <div class="tabs is-boxed">
<ul> <ul>
<li :class="selectType.code===v.code? 'is-active fs-16' : 'fs-16'" v-for="v in fieldType"> <li
<a @click="selectType = v"><span>{{ v.name }}</span></a> :class="selectType.code === v.code ? 'is-active fs-16' : 'fs-16'"
v-for="v in fieldType"
>
<a @click="selectType = v"
><span>{{ v.name }}</span></a
>
</li> </li>
</ul> </ul>
</div> </div>
<template v-if="selectType.code==='formula'"> <template v-if="selectType.code === 'formula'">
<b-radio :class="i===1? 'ml-5' : null" v-model="choice" v-for="(v,i) in choices" :key="i" <b-radio
:native-value="v.code"> :class="i === 1 ? 'ml-5' : null"
<span :class="v.code===choice? 'fsb-16' : 'fs-16'">{{v.name}}</span> v-model="choice"
</b-radio> v-for="(v, i) in choices"
<div class="has-background-light mt-3 px-3 py-3"> :key="i"
<div class="tags are-medium mb-0" v-if="choice==='function'"> :native-value="v.code"
<span :class="`tag ${func===v.code? 'is-primary' : 'is-dark'} is-rounded is-clickable`" >
v-for="(v,i) in funcs" :key="i" @click="changeFunc(v)" @dblclick="addFunc(v)">{{v.name}}</span> <span :class="v.code === choice ? 'fsb-16' : 'fs-16'">{{ v.name }}</span>
</b-radio>
<div class="has-background-light mt-3 px-3 py-3">
<div
class="tags are-medium mb-0"
v-if="choice === 'function'"
>
<span
:class="`tag ${func === v.code ? 'is-primary' : 'is-dark'} is-rounded is-clickable`"
v-for="(v, i) in funcs"
:key="i"
@click="changeFunc(v)"
@dblclick="addFunc(v)"
>{{ v.name }}</span
>
</div> </div>
<template v-else> <template v-else>
<div class="field px-0 mx-0"> <div class="field px-0 mx-0">
<label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label> <label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label>
<div class="control"> <div class="control">
<!--<b-taginput <!--<b-taginput
size="is-small" size="is-small"
v-model="tags" v-model="tags"
:data="fields.filter(v=>v.format==='number')" :data="fields.filter(v=>v.format==='number')"
@@ -39,296 +58,418 @@
Không trường thỏa mãn Không trường thỏa mãn
</template> </template>
</b-taginput>--> </b-taginput>-->
</div> </div>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tags')"> {{errors.find(v=>v.name==='tags').message}} </p> <p
</div> class="help has-text-danger"
<div class="field mt-3" v-if="tags.length>0"> v-if="errors.find((v) => v.name === 'tags')"
>
{{ errors.find((v) => v.name === "tags").message }}
</p>
</div>
<div
class="field mt-3"
v-if="tags.length > 0"
>
<p class="help is-primary mb-1">Click đúp vào để thêm vào công thức tính.</p> <p class="help is-primary mb-1">Click đúp vào để thêm vào công thức tính.</p>
<div class="tags mb-2"> <div class="tags mb-2">
<span @dblclick="formula = formula? (formula + ' ' + v.name) : v.name" class="tag is-dark is-rounded is-clickable" <span
v-for="v in tags"> @dblclick="formula = formula ? formula + ' ' + v.name : v.name"
{{v.name}} class="tag is-dark is-rounded is-clickable"
</span> v-for="v in tags"
>
{{ v.name }}
</span>
</div> </div>
<div class="tags"> <div class="tags">
<span v-for="(v,i) in operator" :key="i"> <span
<span @dblclick="addOperator(v)" class="tag is-primary is-rounded is-clickable mr-4"> v-for="(v, i) in operator"
<span class="fs-16">{{v.code}}</span> :key="i"
>
<span
@dblclick="addOperator(v)"
class="tag is-primary is-rounded is-clickable mr-4"
>
<span class="fs-16">{{ v.code }}</span>
</span>
</span> </span>
</span>
</div> </div>
</div> </div>
</template> </template>
<div class="field mt-3 px-0 mx-0"> <div class="field mt-3 px-0 mx-0">
<label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label> <label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label>
<p class="control"> <p class="control">
<textarea class="textarea" rows="3" type="text" :placeholder="placeholder" v-model="formula"> </textarea> <textarea
</p> class="textarea"
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='formula')"> {{errors.find(v=>v.name==='formula').message}} </p> rows="3"
</div>
</div>
<div class="field is-horizontal mt-3 px-0 mx-0">
<div class="field-body">
<div class="field">
<label class="label fs-14">Hiển thị theo <span class="has-text-danger"> * </span> </label>
<div class="control">
<b-autocomplete
size="is-small"
icon-right="magnify"
:value="selectUnit? selectUnit.name : ''"
placeholder=""
:keep-first=true
:open-on-focus=true
:data="moneyunit"
field="name"
@select="option => selectUnit = option">
</b-autocomplete>
</div>
</div>
<div class="field">
<label class="label fs-14">Phần thập phân</label>
<div class="control">
<input class="input is-small" type="text" placeholder="" v-model="decimal">
</div>
</div>
</div>
</div>
</template>
<div class="field px-0 mx-0">
<label class="label">Tên trường <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="Tên trường phải là duy nhất" v-model="name"
:readonly="selectType? selectType.code==='formula': false">
</p>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='name')"> {{errors.find(v=>v.name==='name').message}} </p>
<p class="help has-text-primary" v-else> Tên trường do hệ thống tự sinh.</p>
</div>
<div class="mt-5">
<label class="label"> tả<span class="has-text-danger"> *</span></label>
<div class="field has-addons">
<div class="control is-expanded" >
<input
class="input"
type="text" type="text"
v-model="label" :placeholder="placeholder"
/> v-model="formula"
</div> >
<div class="control"> </textarea>
<button class="button" @click="editLabel()"> </p>
<span><SvgIcon v-bind="{name: 'pen.svg', type: 'dark', size: 17}"></SvgIcon></span> <p
</button> class="help has-text-danger"
</div> v-if="errors.find((v) => v.name === 'formula')"
>
{{ errors.find((v) => v.name === "formula").message }}
</p>
</div> </div>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='label')"> {{errors.find(v=>v.name==='label').message}} </p> </div>
</div> <div class="field is-horizontal mt-3 px-0 mx-0">
<div class="field mt-5" v-if="selectType.code==='empty'"> <div class="field-body">
<label class="label" <div class="field">
>Kiểu dữ liệu <label class="label fs-14">Hiển thị theo <span class="has-text-danger"> * </span> </label>
<span class="has-text-danger"> * </span> <div class="control">
</label> <b-autocomplete
<div class="control fs-14"> size="is-small"
<span class="mr-4" v-for="(v,i) in datatype"> icon-right="magnify"
<a class="icon-text" @click="changeType(v)"> :value="selectUnit ? selectUnit.name : ''"
<SvgIcon v-bind="{name: `radio-${radioType.code===v.code? '' : 'un'}checked.svg`, type: 'gray', size: 22}"></SvgIcon> placeholder=""
</a> :keep-first="true"
{{v.name}} :open-on-focus="true"
</span> :data="moneyunit"
field="name"
@select="(option) => (selectUnit = option)"
>
</b-autocomplete>
</div> </div>
</div> </div>
<div class="field mt-5"> <div class="field">
<p class="control"> <label class="label fs-14">Phần thập phân</label>
<a class="button is-primary has-text-white" <div class="control">
@click="selectType.code==='formula'? createField() : createEmptyField()">Tạo cột</a> <input
</p> class="input is-small"
type="text"
placeholder=""
v-model="decimal"
/>
</div> </div>
<Modal v-bind="showmodal" v-if="showmodal" @label="changeLabel" @close="close"></Modal> </div>
</div>
</div>
</template>
<div class="field px-0 mx-0">
<label class="label">Tên trường <span class="has-text-danger"> * </span> </label>
<p class="control">
<input
class="input"
type="text"
placeholder="Tên trường phải là duy nhất"
v-model="name"
:readonly="selectType ? selectType.code === 'formula' : false"
/>
</p>
<p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'name')"
>
{{ errors.find((v) => v.name === "name").message }}
</p>
<p
class="help has-text-primary"
v-else
>
Tên trường do hệ thống tự sinh.
</p>
</div>
<div class="mt-5">
<label class="label"> tả<span class="has-text-danger"> *</span></label>
<div class="field has-addons">
<div class="control is-expanded">
<input
class="input"
type="text"
v-model="label"
/>
</div>
<div class="control">
<button
class="button"
@click="editLabel()"
>
<span><SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 17 }"></SvgIcon></span>
</button>
</div>
</div>
<p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'label')"
>
{{ errors.find((v) => v.name === "label").message }}
</p>
</div>
<div
class="field mt-5"
v-if="selectType.code === 'empty'"
>
<label class="label"
>Kiểu dữ liệu
<span class="has-text-danger"> * </span>
</label>
<div class="control fs-14">
<span
class="mr-4"
v-for="(v, i) in datatype"
>
<a
class="icon-text"
@click="changeType(v)"
>
<SvgIcon
v-bind="{
name: `radio-${radioType.code === v.code ? '' : 'un'}checked.svg`,
type: 'gray',
size: 22,
}"
></SvgIcon>
</a>
{{ v.name }}
</span>
</div>
</div>
<div class="field mt-5">
<p class="control">
<a
class="button is-primary has-text-white"
@click="selectType.code === 'formula' ? createField() : createEmptyField()"
>Tạo cột</a
>
</p>
</div>
<Modal
v-bind="showmodal"
v-if="showmodal"
@label="changeLabel"
@close="close"
></Modal>
</template> </template>
<script setup> <script setup>
import { useStore } from '@/stores/index' import { useStore } from "@/stores/index";
import ScrollBox from '~/components/datatable/ScrollBox' import ScrollBox from "~/components/datatable/ScrollBox";
const emit = defineEmits(['modalevent']) const emit = defineEmits(["modalevent"]);
const store = useStore() const store = useStore();
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp() const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp();
var props = defineProps({ var props = defineProps({
pagename: String, pagename: String,
field: Object, field: Object,
filters: Object, filters: Object,
filterData: Object, filterData: Object,
width: String width: String,
}) });
const moneyunit = store.moneyunit const moneyunit = store.moneyunit;
const datatype = store.datatype const datatype = store.datatype;
var showmodal = ref() var showmodal = ref();
var pagedata = store[props.pagename] var pagedata = store[props.pagename];
var selectUnit = moneyunit.find(v=>v.code==='one') var selectUnit = moneyunit.find((v) => v.code === "one");
var data = [] var data = [];
var current = 1 var current = 1;
var filterData = [] var filterData = [];
var loading = false var loading = false;
var fieldType = [{code: 'formula', name: 'Tạo công thức'}, {code: 'empty', name: 'Tạo cột rỗng'}] var fieldType = [
var errors = [] { code: "formula", name: "Tạo công thức" },
var tags = [] { code: "empty", name: "Tạo cột rỗng" },
var formula = undefined ];
var name = `f${$id().toLocaleLowerCase()}` var errors = [];
var label = undefined var tags = [];
var errors = [] var formula = undefined;
var selectType = fieldType.find(v=>v.code==='empty') var name = `f${$id().toLocaleLowerCase()}`;
var radioType = ref(datatype.find(v=>v.code==='string')) var label = undefined;
var fields = [] var errors = [];
var options = undefined var selectType = fieldType.find((v) => v.code === "empty");
var columns = $copy(pagedata.fields.filter(v=>v.format==='number')) var radioType = ref(datatype.find((v) => v.code === "string"));
var decimal = undefined var fields = [];
var choices = [{code: 'column', name: 'Dùng cột dữ liệu'}, {code: 'function', name: 'Dùng hàm số'}] var options = undefined;
var choice = 'column' var columns = $copy(pagedata.fields.filter((v) => v.format === "number"));
var funcs = [{code: 'sum', name: 'Sum'}, {code: 'max', name: 'Max'}, {code: 'min', name: 'Min'}, {code: 'avg', name: 'Avg'}] var decimal = undefined;
var func = 'sum' var choices = [
var placeholder = 'Minh hoạ công thức: f10001 + f10002' { code: "column", name: "Dùng cột dữ liệu" },
var args = undefined { code: "function", name: "Dùng hàm số" },
var operator = [{code: '+', name: 'Cộng'}, {code: '-', name: 'Trừ'}, {code: '*', name: 'Nhân'}, {code: '/', name: 'Chia'}, {code: '>', name: 'Lớn hơn'}, ];
{code: '>=', name: 'Lớn hơn hoặc bằng'}, {code: '<', name: 'Nhỏ hơn'}, {code: '<=', name: 'Nhỏ hơn hoặc bằng'}, {code: '==', name: 'Bằng'}, var choice = "column";
{code: '&&', name: 'Và'}, {code: '||', name: 'Hoặc'}, {code: 'iif', name: 'Điều kiện rẽ nhánh'}] var funcs = [
function editLabel() { { code: "sum", name: "Sum" },
if($empty(label)) return { code: "max", name: "Max" },
showmodal.value = {component: 'datatable/EditLabel', width: '500px', height: '300px', vbind: {label: label}} { code: "min", name: "Min" },
} { code: "avg", name: "Avg" },
function close() { ];
showmodal.value = null var func = "sum";
} var placeholder = "Minh hoạ công thức: f10001 + f10002";
function changeLabel(evt) { var args = undefined;
label = evt var operator = [
showmodal.value = null { code: "+", name: "Cộng" },
} { code: "-", name: "Trừ" },
function changeType(v) { { code: "*", name: "Nhân" },
radioType.value = v { code: "/", name: "Chia" },
} { code: ">", name: "Lớn hơn" },
function addFunc(v) { { code: ">=", name: "Lớn hơn hoặc bằng" },
formula = (formula? formula + ' ' : '') + v.name + '(C0: C2)' { code: "<", name: "Nhỏ hơn" },
} { code: "<=", name: "Nhỏ hơn hoặc bằng" },
function addOperator(v) { { code: "==", name: "Bằng" },
let text = v.code==='iif'? 'a>b? c : d' : v.code { code: "&&", name: "Và" },
formula = `${formula || ''} ${text}` { code: "||", name: "Hoặc" },
} { code: "iif", name: "Điều kiện rẽ nhánh" },
function changeFunc(v) { ];
placeholder = `${v.name}(C0:C2) hoặc ${v.name}(C0,C1,C2). C là viết tắt của cột dữ liệu, số thứ tự của cột bắt đầu từ 0` function editLabel() {
func = v.code if ($empty(label)) return;
} showmodal.value = {
function getFields() { component: "datatable/EditLabel",
fields = pagedata? $copy(pagedata.fields) : [] width: "500px",
fields.map(v=>v.caption = (v.label? v.label.indexOf('<')>=0 : false)? v.name : v.label) height: "300px",
} vbind: { label: label },
function checkFunc() { };
let error = false }
let val = formula.trim().replaceAll(' ', '') function close() {
if(val.toLowerCase().indexOf(func)<0) error = true showmodal.value = null;
let start = val.toLowerCase().indexOf('(') }
let end = val.toLowerCase().indexOf(')') function changeLabel(evt) {
if( start<0 || end<0) error = true label = evt;
let content = val.substring(start+1, end) showmodal.value = null;
if($empty(content)) error = true }
let content1 = content.replaceAll(':', ',') function changeType(v) {
let arr = content1.split(',') radioType.value = v;
arr.map(v=>{ }
let arr1 = v.toLowerCase().split('c') function addFunc(v) {
if(arr1.length!==2) error = true formula = (formula ? formula + " " : "") + v.name + "(C0: C2)";
else if(!$isNumber(arr1[1])) error = true }
}) function addOperator(v) {
return error? 'error' : content let text = v.code === "iif" ? "a>b? c : d" : v.code;
} formula = `${formula || ""} ${text}`;
function checkValid() { }
errors = [] function changeFunc(v) {
if(tags.length===0 && choice==='column') { placeholder = `${v.name}(C0:C2) hoặc ${v.name}(C0,C1,C2). C là viết tắt của cột dữ liệu, số thứ tự của cột bắt đầu từ 0`;
errors.push({name: 'tags', message: 'Chưa chọn trường xây dựng công thức.'}) func = v.code;
}
function getFields() {
fields = pagedata ? $copy(pagedata.fields) : [];
fields.map((v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label));
}
function checkFunc() {
let error = false;
let val = formula.trim().replaceAll(" ", "");
if (val.toLowerCase().indexOf(func) < 0) error = true;
let start = val.toLowerCase().indexOf("(");
let end = val.toLowerCase().indexOf(")");
if (start < 0 || end < 0) error = true;
let content = val.substring(start + 1, end);
if ($empty(content)) error = true;
let content1 = content.replaceAll(":", ",");
let arr = content1.split(",");
arr.map((v) => {
let arr1 = v.toLowerCase().split("c");
if (arr1.length !== 2) error = true;
else if (!$isNumber(arr1[1])) error = true;
});
return error ? "error" : content;
}
function checkValid() {
errors = [];
if (tags.length === 0 && choice === "column") {
errors.push({
name: "tags",
message: "Chưa chọn trường xây dựng công thức.",
});
}
if (!$empty(formula) ? $empty(formula.trim()) : true) {
errors.push({ name: "formula", message: "Công thức không được bỏ trống." });
}
if (!$empty(label) ? $empty(label.trim()) : true)
errors.push({ name: "label", message: "Mô tả không được bỏ trống." });
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
errors.push({
name: "label",
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
});
}
if (errors.length > 0) return false;
//check formula in case use column
if (choice === "column") {
let val = $copy(formula);
tags.forEach((v) => {
let myRegExp = new RegExp(v.name, "g");
val = val.replace(myRegExp, Math.random());
});
try {
let value = $calc(val);
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
errors.push({ name: "formula", message: "Công thức không hợp lệ" });
} }
if(!$empty(formula)? $empty(formula.trim()) : true) { } catch (err) {
errors.push({name: 'formula', message: 'Công thức không được bỏ trống.'}) console.log(err);
} errors.push({ name: "formula", message: "Công thức không hợp lệ" });
if(!$empty(label)? $empty(label.trim()) : true )
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) {
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
}
if(errors.length>0) return false
//check formula in case use column
if(choice==='column') {
let val = $copy(formula)
tags.forEach(v => {
let myRegExp = new RegExp(v.name, 'g')
val = val.replace(myRegExp, Math.random())
})
try {
let value = $calc(val)
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
}
}
catch(err) {
console.log(err)
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
}
} else {
if(checkFunc()==='error') errors.push({name: 'formula', message: `Hàm ${func.toUpperCase()} không hợp lệ`})
}
return errors.length>0? false : true
}
function createField() {
if(!checkValid()) return
let field = $createField(name.trim(), label.trim(), 'number', true)
field.formula = formula.trim().replaceAll(' ', '')
if(choice==='function') {
field.func = func
field.vals = checkFunc()
} else field.tags = tags.map(v=>v.name)
field.level = Math.max(...pagedata.fields.map(v=>v.level? v.level : 0)) + 1
field.unit = selectUnit.detail
field.decimal = decimal
field.disable = 'search,value'
let copy = $copy(pagedata)
copy.fields.push(field)
store.commit(props.pagename, copy)
emit('newfield', field)
tags = []
formula = undefined
label = undefined
name = `f${$id()}`
emit('close')
} }
function createEmptyField() { } else {
errors = [] if (checkFunc() === "error")
if(!$empty(name)? $empty(name.trim()) : true ) errors.push({
errors.push({name: 'name', message: 'Tên không được bỏ trống.'}) name: "formula",
else if(pagedata.fields.find(v=>v.name.toLowerCase()===name.toLowerCase())) { message: `Hàm ${func.toUpperCase()} không hợp lệ`,
errors.push({name: 'name', message: 'Tên trường bị trùng. Hãy đặt tên khác.'}) });
} }
if(!$empty(label)? $empty(label.trim()) : true ) return errors.length > 0 ? false : true;
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'}) }
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) { function createField() {
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'}) if (!checkValid()) return;
} let field = $createField(name.trim(), label.trim(), "number", true);
if(errors.length>0) return field.formula = formula.trim().replaceAll(" ", "");
let field = $createField(name.trim(), label.trim(), radioType.value.code, true) if (choice === "function") {
if(selectType.code==='chart') field = createChartField() field.func = func;
let copy = $clone(pagedata) field.vals = checkFunc();
copy.fields.push(field) } else field.tags = tags.map((v) => v.name);
copy.update = {fields: copy.fields} field.level = Math.max(...pagedata.fields.map((v) => (v.level ? v.level : 0))) + 1;
store.commit(props.pagename, copy) field.unit = selectUnit.detail;
//pagedata = copy field.decimal = decimal;
emit('newfield', field) field.disable = "search,value";
label = undefined let copy = $copy(pagedata);
name = `f${$id()}` copy.fields.push(field);
emit('close') store.commit(props.pagename, copy);
} emit("newfield", field);
function createChartField() { tags = [];
let array = pagedata.fields.filter(v=>v.format==='number' && v.show) formula = undefined;
if(args) array = $copy(args) label = undefined;
let text = '' name = `f${$id()}`;
array.map((v,i)=>text += `'${v.name}${i<array.length-1? "'," : "'"}`) emit("close");
let label = '' }
array.map((v,i)=>label += `'${$stripHtml(v.label)}${i<array.length-1? "'," : "'"}`) function createEmptyField() {
let field = $createField(name.trim(), label.trim(), radioType.value.code, true) errors = [];
field.chart = 'yes' if (!$empty(name) ? $empty(name.trim()) : true) errors.push({ name: "name", message: "Tên không được bỏ trống." });
field.template = `<TrendingChart class="is-clickable" v-bind="{row: row, fields: [${text}], labels: [${label}], width: '80', height: '26', 'header': ['stock_code', 'name']}"/>` else if (pagedata.fields.find((v) => v.name.toLowerCase() === name.toLowerCase())) {
return field errors.push({
} name: "name",
//============ message: "Tên trường bị trùng. Hãy đặt tên khác.",
getFields() });
</script> }
if (!$empty(label) ? $empty(label.trim()) : true)
errors.push({ name: "label", message: "Mô tả không được bỏ trống." });
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
errors.push({
name: "label",
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
});
}
if (errors.length > 0) return;
let field = $createField(name.trim(), label.trim(), radioType.value.code, true);
if (selectType.code === "chart") field = createChartField();
let copy = $clone(pagedata);
copy.fields.push(field);
copy.update = { fields: copy.fields };
store.commit(props.pagename, copy);
//pagedata = copy
emit("newfield", field);
label = undefined;
name = `f${$id()}`;
emit("close");
}
function createChartField() {
let array = pagedata.fields.filter((v) => v.format === "number" && v.show);
if (args) array = $copy(args);
let text = "";
array.map((v, i) => (text += `'${v.name}${i < array.length - 1 ? "'," : "'"}`));
let label = "";
array.map((v, i) => (label += `'${$stripHtml(v.label)}${i < array.length - 1 ? "'," : "'"}`));
let field = $createField(name.trim(), label.trim(), radioType.value.code, true);
field.chart = "yes";
field.template = `<TrendingChart class="is-clickable" v-bind="{row: row, fields: [${text}], labels: [${label}], width: '80', height: '26', 'header': ['stock_code', 'name']}"/>`;
return field;
}
//============
getFields();
</script>

View File

@@ -1,64 +1,106 @@
<template> <template>
<nav class="pagination mx-0" role="navigation" aria-label="pagination"> <nav
<ul class="pagination-list" v-if="pageInfo"> class="pagination mx-0"
<li v-for="v in pageInfo"> role="navigation"
<a v-if="currentPage===v" class="pagination-link is-current has-background-primary has-text-white" :aria-label="`Page ${v}`" aria-current="page">{{ v }}</a> aria-label="pagination"
<a v-else href="#" class="pagination-link" :aria-label="`Goto page ${v}`" @click="changePage(v)">{{ v }}</a> >
</li> <ul
<a @click="previous()" class="pagination-previous ml-5"> class="pagination-list"
<SvgIcon v-bind="{name: 'left1.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon> v-if="pageInfo"
</a> >
<a @click="next()" class="pagination-next"> <li v-for="v in pageInfo">
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon> <a
</a> v-if="currentPage === v"
</ul> class="pagination-link is-current has-background-primary has-text-white"
</nav> :aria-label="`Page ${v}`"
aria-current="page"
>{{ v }}</a
>
<a
v-else
href="#"
class="pagination-link"
:aria-label="`Goto page ${v}`"
@click="changePage(v)"
>{{ v }}</a
>
</li>
<a
@click="previous()"
class="pagination-previous ml-5"
>
<SvgIcon
v-bind="{
name: 'left1.svg',
type: 'dark',
size: 20,
alt: 'Tìm kiếm',
}"
></SvgIcon>
</a>
<a
@click="next()"
class="pagination-next"
>
<SvgIcon
v-bind="{
name: 'right.svg',
type: 'dark',
size: 20,
alt: 'Tìm kiếm',
}"
></SvgIcon>
</a>
</ul>
</nav>
</template> </template>
<script setup> <script setup>
const emit = defineEmits(['changepage']) const emit = defineEmits(["changepage"]);
var props = defineProps({ var props = defineProps({
data: Array, data: Array,
perPage: Number perPage: Number,
}) });
var currentPage = 1 var currentPage = 1;
var totalRows = props.data.length var totalRows = props.data.length;
var lastPage = parseInt(totalRows / props.perPage) var lastPage = parseInt(totalRows / props.perPage);
if(lastPage*props.perPage<totalRows) lastPage += 1 if (lastPage * props.perPage < totalRows) lastPage += 1;
var pageInfo = ref() var pageInfo = ref();
function pages(current_page, last_page, onSides = 2) { function pages(current_page, last_page, onSides = 2) {
// pages // pages
let pages = []; let pages = [];
// Loop through // Loop through
for (let i = 1; i <= last_page; i++) { for (let i = 1; i <= last_page; i++) {
// Define offset // Define offset
let offset = (i == 1 || last_page) ? onSides + 1 : onSides; let offset = i == 1 || last_page ? onSides + 1 : onSides;
// If added // If added
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) || if (i == 1 || (current_page - offset <= i && current_page + offset >= i) || i == current_page || i == last_page) {
i == current_page || i == last_page) { pages.push(i);
pages.push(i); } else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) { pages.push("...");
pages.push('...');
}
}
return pages;
} }
function changePage(page) { }
if(page==='...') return return pages;
currentPage = page }
pageInfo.value = pages(page, lastPage, 2) function changePage(page) {
emit('changepage', page) if (page === "...") return;
} currentPage = page;
pageInfo.value = pages(1, lastPage, 2) pageInfo.value = pages(page, lastPage, 2);
watch(() => props.data, (newVal, oldVal) => { emit("changepage", page);
totalRows = props.data.length }
lastPage = parseInt(totalRows / props.perPage) pageInfo.value = pages(1, lastPage, 2);
if(lastPage*props.perPage<totalRows) lastPage += 1 watch(
pageInfo.value = pages(1, lastPage, 2) () => props.data,
}) (newVal, oldVal) => {
function previous() { totalRows = props.data.length;
if(currentPage>1) changePage(currentPage-1) lastPage = parseInt(totalRows / props.perPage);
} if (lastPage * props.perPage < totalRows) lastPage += 1;
function next() { pageInfo.value = pages(1, lastPage, 2);
if(currentPage<lastPage) changePage(currentPage+1) },
} );
</script> function previous() {
if (currentPage > 1) changePage(currentPage - 1);
}
function next() {
if (currentPage < lastPage) changePage(currentPage + 1);
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,79 @@
<template> <template>
<div class="px-2" :style="`max-height: ${maxheight}; overflow-y: auto;`"> <div
<div class="px-2"
v-for="(v, i) in rows" :key="i" :style="`max-height: ${maxheight}; overflow-y: auto;`"
:class="[ >
'field is-grouped py-1 my-0', <div
i !== rows.length - 1 && 'border-bottom' v-for="(v, i) in rows"
]" :key="i"
:class="['field is-grouped py-1 my-0', i !== rows.length - 1 && 'border-bottom']"
> >
<p class="control is-expanded py-0 fs-14 hyperlink" @click="doClick(v,i)"> <p
{{ $stripHtml(v[name] || v.fullname || v.code || 'n/a', 75) }} class="control is-expanded py-0 fs-14 hyperlink"
<span class="icon has-text-primary" v-if="checked[i] && notick!==true"> @click="doClick(v, i)"
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 15}"></SvgIcon> >
{{ $stripHtml(v[name] || v.fullname || v.code || "n/a", 75) }}
<span
class="icon has-text-primary"
v-if="checked[i] && notick !== true"
>
<SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 15 }"></SvgIcon>
</span> </span>
</p> </p>
<p class="control py-0" v-if="show"> <p
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.author"> class="control py-0"
<SvgIcon v-bind="{name: 'user.svg', type: 'gray', size: 15}"></SvgIcon> v-if="show"
>
<span
class="icon-text has-text-grey mr-2 fs-13"
v-if="show.author"
>
<SvgIcon v-bind="{ name: 'user.svg', type: 'gray', size: 15 }"></SvgIcon>
<span>{{ v[show.author] }}</span> <span>{{ v[show.author] }}</span>
</span> </span>
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.view"> <span
<SvgIcon v-bind="{name: 'view.svg', type: 'gray', size: 15}"></SvgIcon> class="icon-text has-text-grey mr-2 fs-13"
v-if="show.view"
>
<SvgIcon v-bind="{ name: 'view.svg', type: 'gray', size: 15 }"></SvgIcon>
<span>{{ v[show.view] }}</span> <span>{{ v[show.view] }}</span>
</span> </span>
<span class="fs-13 has-text-grey" v-if="show.time">{{$dayjs(v['create_time']).fromNow(true)}}</span> <span
<span class="tooltip"> class="fs-13 has-text-grey"
<a class="icon ml-1" v-if="show.link" @click="doClick(v,i, 'newtab')"> v-if="show.time"
<SvgIcon v-bind="{name: 'opennew.svg', type: 'gray', size: 15}"></SvgIcon> >{{ $dayjs(v["create_time"]).fromNow(true) }}</span
>
<span class="tooltip">
<a
class="icon ml-1"
v-if="show.link"
@click="doClick(v, i, 'newtab')"
>
<SvgIcon v-bind="{ name: 'opennew.svg', type: 'gray', size: 15 }"></SvgIcon>
</a> </a>
<span class="tooltiptext">Mở trong tab mớ</span> <span class="tooltiptext">Mở trong tab mớ</span>
</span> </span>
<span class="tooltip" v-if="show.rename"> <span
<a class="icon ml-1" @click="$emit('rename', v, i)"> class="tooltip"
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 15}"></SvgIcon> v-if="show.rename"
>
<a
class="icon ml-1"
@click="$emit('rename', v, i)"
>
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'gray', size: 15 }"></SvgIcon>
</a> </a>
<span class="tooltiptext">Đổi tên</span> <span class="tooltiptext">Đổi tên</span>
</span> </span>
<span class="tooltip" v-if="show.rename"> <span
<a class="icon has-text-danger ml-1" @click="$emit('remove', v, i)"> class="tooltip"
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 15}"></SvgIcon> v-if="show.rename"
>
<a
class="icon has-text-danger ml-1"
@click="$emit('remove', v, i)"
>
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'gray', size: 15 }"></SvgIcon>
</a> </a>
<span class="tooltiptext">Xóa</span> <span class="tooltiptext">Xóa</span>
</span> </span>
@@ -47,7 +83,7 @@
</template> </template>
<script> <script>
export default { export default {
props: ['data', 'name', 'maxheight', 'perpage', 'sort', 'selects', 'keyval', 'show', 'notick'], props: ["data", "name", "maxheight", "perpage", "sort", "selects", "keyval", "show", "notick"],
data() { data() {
return { return {
currentPage: 1, currentPage: 1,
@@ -56,62 +92,64 @@ export default {
selected: [], selected: [],
checked: {}, checked: {},
time: undefined, time: undefined,
array: [] array: [],
} };
}, },
created() { created() {
this.getdata() this.getdata();
}, },
watch: { watch: {
data: function(newVal) { data: function (newVal) {
this.getdata() this.getdata();
},
selects: function (newVal) {
this.getSelect();
}, },
selects: function(newVal) {
this.getSelect()
}
}, },
methods: { methods: {
getdata() { getdata() {
this.currentPage = 1 this.currentPage = 1;
this.array = this.$copy(this.data) this.array = this.$copy(this.data);
if(this.sort!==false) { if (this.sort !== false) {
let f = {} let f = {};
let showtime = this.show? this.show.time : false let showtime = this.show ? this.show.time : false;
showtime? f['create_time'] = 'desc' : f[this.name] = 'asc' showtime ? (f["create_time"] = "desc") : (f[this.name] = "asc");
this.$multiSort(this.array, f) this.$multiSort(this.array, f);
} }
this.rows = this.array.slice(0, this.perpage) this.rows = this.array.slice(0, this.perpage);
this.getSelect() this.getSelect();
}, },
getSelect() { getSelect() {
if(!this.selects) return if (!this.selects) return;
this.selected = [] this.selected = [];
this.checked = {} this.checked = {};
this.selects.map(v=>{ this.selects.map((v) => {
let idx = this.rows.findIndex(x=>x[this.keyval? this.keyval : this.name]===v) let idx = this.rows.findIndex((x) => x[this.keyval ? this.keyval : this.name] === v);
if(idx>=0) { if (idx >= 0) {
this.selected.push(this.rows[idx]) this.selected.push(this.rows[idx]);
this.checked[idx] = true this.checked[idx] = true;
} }
}) });
}, },
doClick(v, i, type) { doClick(v, i, type) {
this.checked[i] = this.checked[i]? false : true this.checked[i] = this.checked[i] ? false : true;
this.checked = this.$copy(this.checked) this.checked = this.$copy(this.checked);
let idx = this.selected.findIndex(x=>x.id===v.id) let idx = this.selected.findIndex((x) => x.id === v.id);
idx>=0? this.$remove(this.selected) : this.selected.push(v) idx >= 0 ? this.$remove(this.selected) : this.selected.push(v);
this.$emit('selected', v, type) this.$emit("selected", v, type);
}, },
handleScroll(e) { handleScroll(e) {
const bottom = e.target.scrollHeight - e.target.scrollTop -5 < e.target.clientHeight const bottom = e.target.scrollHeight - e.target.scrollTop - 5 < e.target.clientHeight;
if (bottom) { if (bottom) {
if(this.total? this.total>this.rows.length : true) { if (this.total ? this.total > this.rows.length : true) {
this.currentPage +=1 this.currentPage += 1;
let arr = this.array.filter((ele,index) => (index>=(this.currentPage-1)*this.perpage && index<this.currentPage*this.perpage)) let arr = this.array.filter(
this.rows = this.rows.concat(arr) (ele, index) => index >= (this.currentPage - 1) * this.perpage && index < this.currentPage * this.perpage,
);
this.rows = this.rows.concat(arr);
} }
} }
} },
} },
} };
</script> </script>

View File

@@ -9,17 +9,30 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr class="fs-14" v-for="(v, i) in fields"> <tr
class="fs-14"
v-for="(v, i) in fields"
>
<td>{{ i }}</td> <td>{{ i }}</td>
<td> <td>
<a class="has-text-primary" @click="openField(v, i)">{{ v.name }}</a> <a
class="has-text-primary"
@click="openField(v, i)"
>{{ v.name }}</a
>
</td> </td>
<td>{{ $stripHtml(v.label, 50) }}</td> <td>{{ $stripHtml(v.label, 50) }}</td>
<td> <td>
<a class="mr-4" @click="moveDown(v, i)"> <a
class="mr-4"
@click="moveDown(v, i)"
>
<SvgIcon v-bind="{ name: 'down1.png', type: 'dark', size: 18 }"></SvgIcon> <SvgIcon v-bind="{ name: 'down1.png', type: 'dark', size: 18 }"></SvgIcon>
</a> </a>
<a class="mr-4" @click="moveUp(v, i)"> <a
class="mr-4"
@click="moveUp(v, i)"
>
<SvgIcon v-bind="{ name: 'up.png', type: 'dark', size: 18 }"></SvgIcon> <SvgIcon v-bind="{ name: 'up.png', type: 'dark', size: 18 }"></SvgIcon>
</a> </a>
<a @click="askConfirm(v, i)"> <a @click="askConfirm(v, i)">
@@ -29,7 +42,13 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<Modal @close="showmodal = undefined" @update="update" @confirm="remove" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@close="showmodal = undefined"
@update="update"
@confirm="remove"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template> </template>
<script setup> <script setup>
import { useStore } from "@/stores/index"; import { useStore } from "@/stores/index";

View File

@@ -1,112 +1,144 @@
<template> <template>
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-body"> <div class="field-body">
<div class="field">
<label class="label fs-14"> Cỡ chữ của bảng <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='table-font-size').detail"
@change="changeSetting($event.target.value, 'table-font-size')">
</p>
</div>
<div class="field" >
<label class="label fs-14"> Cỡ chữ tiêu đề <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<p class="control fs-14">
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='header-font-size').detail"
@change="changeSetting($event.target.value, 'header-font-size')">
</p>
</p>
</div>
<div class="field">
<label class="label fs-14"> Số dòng trên 1 trang <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='per-page').detail"
@change="changeSetting($event.target.value, 'per-page')">
</p>
</div>
</div>
</div>
<div class="field is-horizontal mt-5">
<div class="field-body">
<div class="field">
<label class="label fs-14"> Màu nền bảng <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input type="color" :value="tablesetting.find(v=>v.code==='table-background').detail"
@change="changeSetting($event.target.value, 'table-background')">
</p>
</div>
<div class="field"> <div class="field">
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label> <label class="label fs-14"> Cỡ chữ của bảng <span class="has-text-danger"> * </span></label>
<p class="control fs-14"> <p class="control fs-14">
<input type="color" :value="tablesetting.find(v=>v.code==='table-font-color').detail" <input
@change="changeSetting($event.target.value, 'table-font-color')"> class="input is-small"
</p> type="text"
:value="tablesetting.find((v) => v.code === 'table-font-size').detail"
@change="changeSetting($event.target.value, 'table-font-size')"
/>
</p>
</div>
<div class="field">
<label class="label fs-14"> Cỡ chữ tiêu đề <span class="has-text-danger"> * </span></label>
<p class="control fs-14">
<input
class="input is-small"
type="text"
:value="tablesetting.find((v) => v.code === 'header-font-size').detail"
@change="changeSetting($event.target.value, 'header-font-size')"
/>
</p>
</div>
<div class="field">
<label class="label fs-14"> Số dòng trên 1 trang <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
class="input is-small"
type="text"
:value="tablesetting.find((v) => v.code === 'per-page').detail"
@change="changeSetting($event.target.value, 'per-page')"
/>
</p>
</div>
</div> </div>
</div> </div>
</div>
<div class="field is-horizontal mt-5"> <div class="field is-horizontal mt-5">
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label> <label class="label fs-14"> Màu nền bảng <span class="has-text-danger"> * </span> </label>
<p class="control fs-14"> <p class="control fs-14">
<input type="color" :value="tablesetting.find(v=>v.code==='header-font-color').detail" <input
@change="changeSetting($event.target.value, 'header-font-color')"> type="color"
</p> :value="tablesetting.find((v) => v.code === 'table-background').detail"
</div> @change="changeSetting($event.target.value, 'table-background')"
<div class="field"> />
<label class="label fs-14"> Màu nền tiêu đề <span class="has-text-danger"> * </span> </label> </p>
<p class="control fs-14"> </div>
<input type="color" :value="tablesetting.find(v=>v.code==='header-background').detail" <div class="field">
@change="changeSetting($event.target.value, 'header-background')"> <label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
</p> <p class="control fs-14">
</div> <input
<div class="field"> type="color"
<label class="label fs-14"> Màu chữ khi filter<span class="has-text-danger"> * </span> </label> :value="tablesetting.find((v) => v.code === 'table-font-color').detail"
<p class="control fs-14"> @change="changeSetting($event.target.value, 'table-font-color')"
<input type="color" :value="tablesetting.find(v=>v.code==='header-filter-color').detail" />
@change="changeSetting($event.target.value, 'header-filter-color')"> </p>
</p> </div>
</div> </div>
</div> </div>
</div>
<div class="field is-horizontal mt-5"> <div class="field is-horizontal mt-5">
<div class="field-body"> <div class="field-body">
<div class="field" > <div class="field">
<label class="label fs-14"> Đường viền <span class="has-text-danger"> * </span> </label> <label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
<p class="control fs-14"> <p class="control fs-14">
<input class="input is-small" type="text" <input
:value="tablesetting.find(v=>v.code==='td-border')? tablesetting.find(v=>v.code==='td-border').detail : undefined" type="color"
@change="changeSetting($event.target.value, 'td-border')"> :value="tablesetting.find((v) => v.code === 'header-font-color').detail"
</p> @change="changeSetting($event.target.value, 'header-font-color')"
/>
</p>
</div>
<div class="field">
<label class="label fs-14"> Màu nền tiêu đề <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
type="color"
:value="tablesetting.find((v) => v.code === 'header-background').detail"
@change="changeSetting($event.target.value, 'header-background')"
/>
</p>
</div>
<div class="field">
<label class="label fs-14"> Màu chữ khi filter<span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
type="color"
:value="tablesetting.find((v) => v.code === 'header-filter-color').detail"
@change="changeSetting($event.target.value, 'header-filter-color')"
/>
</p>
</div>
</div>
</div>
<div class="field is-horizontal mt-5">
<div class="field-body">
<div class="field">
<label class="label fs-14"> Đường viền <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
class="input is-small"
type="text"
:value="
tablesetting.find((v) => v.code === 'td-border')
? tablesetting.find((v) => v.code === 'td-border').detail
: undefined
"
@change="changeSetting($event.target.value, 'td-border')"
/>
</p>
</div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
import { useStore } from '@/stores/index' import { useStore } from "@/stores/index";
const store = useStore() const store = useStore();
var props = defineProps({ var props = defineProps({
pagename: String pagename: String,
}) });
const { $copy, $clone, $empty } = useNuxtApp() const { $copy, $clone, $empty } = useNuxtApp();
var pagedata = $clone(store[props.pagename]) var pagedata = $clone(store[props.pagename]);
var errors = [] var errors = [];
var radioNote = 'no' var radioNote = "no";
var tablesetting = pagedata.tablesetting var tablesetting = pagedata.tablesetting;
let found = tablesetting.find(v=>v.code==='note') let found = tablesetting.find((v) => v.code === "note");
if(found? found.detail!=='@' : false) radioNote = 'yes' if (found ? found.detail !== "@" : false) radioNote = "yes";
function changeSetting(value, code) { function changeSetting(value, code) {
if(code==='note' && $empty(value)) return if (code === "note" && $empty(value)) return;
let copy = $copy(tablesetting) let copy = $copy(tablesetting);
let found = copy.find(v=>v.code===code) let found = copy.find((v) => v.code === code);
if(found) found.detail = value if (found) found.detail = value;
else { else {
found = $copy(tablesetting.find(v=>v.code===code)) found = $copy(tablesetting.find((v) => v.code === code));
found.detail = value found.detail = value;
copy.push(found) copy.push(found);
}
tablesetting = copy
pagedata.tablesetting = tablesetting
store.commit(props.pagename, pagedata)
} }
</script> tablesetting = copy;
pagedata.tablesetting = tablesetting;
store.commit(props.pagename, pagedata);
}
</script>

View File

@@ -1,92 +1,157 @@
<template> <template>
<div class="pb-1" style="border-bottom: 2px solid #3c5b63" v-if="array || !enableTime"> <div
class="pb-1"
style="border-bottom: 2px solid #3c5b63"
v-if="array || !enableTime"
>
<div class="columns mx-0 mb-0"> <div class="columns mx-0 mb-0">
<div class="column is-8 px-0 pb-0" v-if="enableTime"> <div
class="column is-8 px-0 pb-0"
v-if="enableTime"
>
<div class="field is-grouped is-grouped-multiline mb-0"> <div class="field is-grouped is-grouped-multiline mb-0">
<div class="control mb-0"> <div class="control mb-0">
<Caption v-bind="{ title: lang === 'vi' ? 'Thời gian' : 'Time', type: 'has-text-warning' }" /> <Caption
v-bind="{
title: lang === 'vi' ? 'Thời gian' : 'Time',
type: 'has-text-warning',
}"
/>
</div> </div>
<div class="control mb-0" v-for="v in array" :key="v.code"> <div
<span class="icon-text fsb-16 has-text-warning px-1" v-if="v.code === current"> class="control mb-0"
v-for="v in array"
:key="v.code"
>
<span
class="icon-text fsb-16 has-text-warning px-1"
v-if="v.code === current"
>
<SvgIcon v-bind="{ name: 'tick.png', size: 20 }"></SvgIcon> <SvgIcon v-bind="{ name: 'tick.png', size: 20 }"></SvgIcon>
<span>{{ v.name }}</span> <span>{{ v.name }}</span>
</span> </span>
<span class="icon-text has-text-grey hyperlink px-1 fsb-16" @click="changeOption(v)" v-else>{{ <span
v.name class="icon-text has-text-grey hyperlink px-1 fsb-16"
}}</span> @click="changeOption(v)"
v-else
>{{ v.name }}</span
>
</div> </div>
<span v-if="newDataAvailable" class="has-text-danger is-italic is-size-6 ml-2"> dữ liệu mới, vui lòng làm <span
mới.</span> v-if="newDataAvailable"
class="has-text-danger is-italic is-size-6 ml-2"
>Có dữ liệu mới, vui lòng làm mới.</span
>
</div> </div>
</div> </div>
<div class="column is-4 px-0"> <div class="column is-4 px-0">
<div class="field is-grouped is-grouped-multiline mb-0"> <div class="field is-grouped is-grouped-multiline mb-0">
<div class="control mb-0"> <div class="control mb-0">
<Caption v-bind="{ <Caption
type: 'has-text-warning', v-bind="{
title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search', type: 'has-text-warning',
}" /> title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search',
}"
/>
</div> </div>
<div class="control mb-0"> <div class="control mb-0">
<input class="input is-small" type="text" v-model="text" <input
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`" @keyup="startSearch" class="input is-small"
id="input" :placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'" /> type="text"
v-model="text"
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`"
@keyup="startSearch"
id="input"
:placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'"
/>
</div> </div>
<div class="control mb-0"> <div class="control mb-0">
<span class="tooltip" v-if="importdata && $getEditRights()"> <span
<a class="mr-2" @click="openImport()"> class="tooltip"
<SvgIcon v-bind="{ v-if="importdata && $getEditRights()"
name: 'upload.svg', >
type: 'findata', <a
size: 22 class="mr-2"
}"></SvgIcon> @click="openImport()"
>
<SvgIcon
v-bind="{
name: 'upload.svg',
type: 'findata',
size: 22,
}"
></SvgIcon>
</a> </a>
<span class="tooltiptext" style="min-width: max-content"> <span
class="tooltiptext"
style="min-width: max-content"
>
{{ lang === "vi" ? "Nhập dữ liệu" : "Import data" }} {{ lang === "vi" ? "Nhập dữ liệu" : "Import data" }}
</span> </span>
</span> </span>
<span class="tooltip" v-if="enableAdd && $getEditRights()"> <span
<a class="mr-2" @click="$emit('add')"> class="tooltip"
v-if="enableAdd && $getEditRights()"
>
<a
class="mr-2"
@click="$emit('add')"
>
<SvgIcon v-bind="{ name: 'add1.png', type: 'findata', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'add1.png', type: 'findata', size: 22 }"></SvgIcon>
</a> </a>
<span class="tooltiptext" style="min-width: max-content">{{ <span
lang === "vi" ? "Thêm mới" : "Add new" class="tooltiptext"
}}</span> style="min-width: max-content"
>{{ lang === "vi" ? "Thêm mới" : "Add new" }}</span
>
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a class="mr-2" @click="$emit('excel')"> <a
class="mr-2"
@click="$emit('excel')"
>
<SvgIcon v-bind="{ name: 'excel.png', type: 'findata', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'excel.png', type: 'findata', size: 22 }"></SvgIcon>
</a> </a>
<span class="tooltiptext" style="min-width: max-content">{{ <span
lang === "vi" ? "Xuất excel" : "Export excel" class="tooltiptext"
}}</span> style="min-width: max-content"
>{{ lang === "vi" ? "Xuất excel" : "Export excel" }}</span
>
</span> </span>
<span class="tooltip"> <span class="tooltip">
<a @click="$emit('refresh-data')"> <a @click="$emit('refresh-data')">
<SvgIcon v-bind="{ name: 'refresh.svg', type: 'findata', size: 22 }"></SvgIcon> <SvgIcon v-bind="{ name: 'refresh.svg', type: 'findata', size: 22 }"></SvgIcon>
</a> </a>
<span class="tooltiptext" style="min-width: max-content">{{ <span
lang === "vi" ? "Làm mới" : "Refresh" class="tooltiptext"
}}</span> style="min-width: max-content"
>{{ lang === "vi" ? "Làm mới" : "Refresh" }}</span
>
</span> </span>
<a class="button is-primary is-loading is-small ml-3" v-if="loading"></a> <a
class="button is-primary is-loading is-small ml-3"
v-if="loading"
></a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal> <Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div> </div>
</template> </template>
<script> <script>
import { useStore } from "@/stores/index" import { useStore } from "@/stores/index";
export default { export default {
setup() { setup() {
const store = useStore() const store = useStore();
return { store } return { store };
}, },
props: ["pagename", "api", "timeopt", "filter", "realtime", "newDataAvailable", "params", "importdata"], props: ["pagename", "api", "timeopt", "filter", "realtime", "newDataAvailable", "params", "importdata"],
@@ -117,19 +182,19 @@ export default {
pagedata: undefined, pagedata: undefined,
loading: false, loading: false,
pollingInterval: null, pollingInterval: null,
searchableFields: [] // Lưu thông tin các field có thể tìm kiếm searchableFields: [], // Lưu thông tin các field có thể tìm kiếm
} };
}, },
watch: { watch: {
pagename(newVal) { pagename(newVal) {
this.updateSearchableFields() this.updateSearchableFields();
} },
}, },
async created() { async created() {
// Cập nhật searchable fields ngay từ đầu // Cập nhật searchable fields ngay từ đầu
this.updateSearchableFields() this.updateSearchableFields();
if (this.viewport < 5) { if (this.viewport < 5) {
this.options = [ this.options = [
@@ -139,85 +204,83 @@ export default {
{ code: 30, name: "1M" }, { code: 30, name: "1M" },
{ code: 90, name: "3M" }, { code: 90, name: "3M" },
{ code: 36000, name: "Tất cả" }, { code: 36000, name: "Tất cả" },
] ];
} }
this.checkTimeopt() this.checkTimeopt();
if (!this.enableTime) return this.$emit("option") if (!this.enableTime) return this.$emit("option");
let found = this.$findapi(this.api) let found = this.$findapi(this.api);
found.commit = undefined found.commit = undefined;
let filter = this.$copy(this.filter) let filter = this.$copy(this.filter);
if (filter) { if (filter) {
//dynamic parameter //dynamic parameter
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf("$") >= 0) { if (value.toString().indexOf("$") >= 0) {
filter[key] = this.store[value.replace("$", "")].id filter[key] = this.store[value.replace("$", "")].id;
} }
} }
} }
if (found.params.filter) { if (found.params.filter) {
if (!filter) filter = {} if (!filter) filter = {};
for (const [key, value] of Object.entries(found.params.filter)) { for (const [key, value] of Object.entries(found.params.filter)) {
filter[key] = value filter[key] = value;
} }
} }
this.options.map((v) => { this.options.map((v) => {
let f = filter ? this.$copy(filter) : {} let f = filter ? this.$copy(filter) : {};
f["create_time__date__gte"] = this.$dayjs() f["create_time__date__gte"] = this.$dayjs().subtract(v.code, "day").format("YYYY-MM-DD");
.subtract(v.code, "day") v.filter = f;
.format("YYYY-MM-DD") });
v.filter = f
})
this.$emit("option", this.$find(this.options, { code: this.current })) this.$emit("option", this.$find(this.options, { code: this.current }));
let f = {} let f = {};
this.options.map((v) => { this.options.map((v) => {
f[`${v.code}`] = { f[`${v.code}`] = {
type: "Count", type: "Count",
field: "create_time__date", field: "create_time__date",
filter: v.filter filter: v.filter,
} };
}) });
let params = { summary: "aggregate", distinct_values: f } let params = { summary: "aggregate", distinct_values: f };
found.params = params found.params = params;
try { try {
let rs = await this.$getapi([found]) let rs = await this.$getapi([found]);
for (const [key, value] of Object.entries(rs[0].data.rows)) { for (const [key, value] of Object.entries(rs[0].data.rows)) {
let found = this.$find(this.options, { code: Number(key) }) let found = this.$find(this.options, { code: Number(key) });
if (found) { if (found) {
found.name = `${found.name} (${value})` found.name = `${found.name} (${value})`;
} }
} }
} catch (error) { } catch (error) {
console.error("Error fetching data:", error) console.error("Error fetching data:", error);
} }
this.array = this.$copy(this.options) this.array = this.$copy(this.options);
}, },
mounted() { mounted() {
if (this.realtime) { if (this.realtime) {
const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000 const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000;
this.pollingInterval = setInterval(this.refresh, interval) this.pollingInterval = setInterval(this.refresh, interval);
} }
}, },
beforeUnmount() { beforeUnmount() {
if (this.pollingInterval) { if (this.pollingInterval) {
clearInterval(this.pollingInterval) clearInterval(this.pollingInterval);
} }
}, },
computed: { computed: {
lang: function () { lang: function () {
return this.store.lang return this.store.lang;
}, },
}, },
@@ -226,140 +289,141 @@ export default {
updateSearchableFields() { updateSearchableFields() {
try { try {
// Lấy API config // Lấy API config
const found = this.$findapi(this.api) const found = this.$findapi(this.api);
if (!found) { if (!found) {
console.warn('Không tìm thấy API config') console.warn("Không tìm thấy API config");
this.choices = [] this.choices = [];
this.searchableFields = [] this.searchableFields = [];
return return;
} }
// Ưu tiên lấy values từ props.params, nếu không có thì lấy từ API config // Ưu tiên lấy values từ props.params, nếu không có thì lấy từ API config
let valuesString = '' let valuesString = "";
if (this.params && this.params.values) { if (this.params && this.params.values) {
// Lấy từ props.params (ưu tiên cao nhất) // Lấy từ props.params (ưu tiên cao nhất)
valuesString = this.params.values valuesString = this.params.values;
} else if (found.params && found.params.values) { } else if (found.params && found.params.values) {
// Lấy từ API config mặc định // Lấy từ API config mặc định
valuesString = found.params.values valuesString = found.params.values;
} else { } else {
console.warn('Không tìm thấy API values trong props hoặc config') console.warn("Không tìm thấy API values trong props hoặc config");
this.choices = [] this.choices = [];
this.searchableFields = [] this.searchableFields = [];
return return;
} }
// Parse values string từ API // Parse values string từ API
let fieldNames = valuesString.split(',').map(v => v.trim()) let fieldNames = valuesString.split(",").map((v) => v.trim());
// Lấy pagedata để lấy label // Lấy pagedata để lấy label
this.pagedata = this.store[this.pagename] this.pagedata = this.store[this.pagename];
// Lấy tất cả các field để search (không lọc format) // Lấy tất cả các field để search (không lọc format)
const searchable = fieldNames.filter(fieldName => { const searchable = fieldNames.filter((fieldName) => {
// Loại bỏ các field kỹ thuật // Loại bỏ các field kỹ thuật
if (fieldName === 'id' || if (
fieldName === 'create_time' || fieldName === "id" ||
fieldName === 'update_time' || fieldName === "create_time" ||
fieldName === 'created_at' || fieldName === "update_time" ||
fieldName === 'updated_at') { fieldName === "created_at" ||
return false fieldName === "updated_at"
) {
return false;
} }
return true return true;
}) });
// Lấy tên và label các field // Lấy tên và label các field
this.choices = searchable this.choices = searchable;
this.searchableFields = searchable.map(fieldName => { this.searchableFields = searchable.map((fieldName) => {
// Lấy field base name (trước dấu __) // Lấy field base name (trước dấu __)
const baseFieldName = fieldName.split('__')[0] const baseFieldName = fieldName.split("__")[0];
const fieldInfo = this.pagedata && this.pagedata.fields const fieldInfo =
? this.pagedata.fields.find(f => f.name === baseFieldName) this.pagedata && this.pagedata.fields ? this.pagedata.fields.find((f) => f.name === baseFieldName) : null;
: null
return { return {
name: fieldName, name: fieldName,
label: fieldInfo ? fieldInfo.label : fieldName label: fieldInfo ? fieldInfo.label : fieldName,
} };
}) });
} catch (error) { } catch (error) {
console.error('Error updating searchable fields:', error) console.error("Error updating searchable fields:", error);
this.choices = [] this.choices = [];
this.searchableFields = [] this.searchableFields = [];
} }
}, },
refresh() { refresh() {
let found = this.$find(this.options, { code: this.current }) let found = this.$find(this.options, { code: this.current });
this.changeOption(found) this.changeOption(found);
}, },
changeOption(v) { changeOption(v) {
this.current = v.code this.current = v.code;
if (this.search) { if (this.search) {
this.text = undefined this.text = undefined;
this.search = undefined this.search = undefined;
} }
this.$emit("option", this.$find(this.array, { code: this.current })) this.$emit("option", this.$find(this.array, { code: this.current }));
}, },
doSearch() { doSearch() {
// Cập nhật choices trước khi search // Cập nhật choices trước khi search
this.updateSearchableFields() this.updateSearchableFields();
this.pagedata = this.store[this.pagename] this.pagedata = this.store[this.pagename];
if (!this.pagedata || !this.pagedata.fields) { if (!this.pagedata || !this.pagedata.fields) {
console.warn('Không có pagedata hoặc fields') console.warn("Không có pagedata hoặc fields");
return return;
} }
let fields = this.pagedata.fields.filter( let fields = this.pagedata.fields.filter((v) => this.choices.findIndex((x) => x === v.name) >= 0);
(v) => this.choices.findIndex((x) => x === v.name) >= 0
)
if (fields.length === 0) { if (fields.length === 0) {
console.warn('Không tìm thấy field để tìm kiếm') console.warn("Không tìm thấy field để tìm kiếm");
return return;
} }
let f = {} let f = {};
fields.map((v) => { fields.map((v) => {
f[`${v.name}__icontains`] = this.search f[`${v.name}__icontains`] = this.search;
}) });
this.$emit("option", { filter_or: f }) this.$emit("option", { filter_or: f });
}, },
openImport() { openImport() {
if (!this.importdata) return if (!this.importdata) return;
// Emit event lên parent (DataView) // Emit event lên parent (DataView)
this.$emit('import', this.importdata) this.$emit("import", this.importdata);
}, },
startSearch(val) { startSearch(val) {
this.search = this.$empty(val.target.value) this.search = this.$empty(val.target.value) ? "" : val.target.value.trim();
? ""
: val.target.value.trim()
if (this.timer) clearTimeout(this.timer) if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.doSearch(), 300) this.timer = setTimeout(() => this.doSearch(), 300);
}, },
checkTimeopt() { checkTimeopt() {
if (this.timeopt > 0) { if (this.timeopt > 0) {
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt) }) let obj = this.$find(this.options, {
if (obj) this.current = obj.code code: this.$formatNumber(this.timeopt),
});
if (obj) this.current = obj.code;
} }
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return;
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false;
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false;
if (this.timeopt.time) { if (this.timeopt.time) {
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt.time) }) let obj = this.$find(this.options, {
if (obj) this.current = obj.code code: this.$formatNumber(this.timeopt.time),
});
if (obj) this.current = obj.code;
} }
}, },
}, },
} };
</script> </script>

View File

@@ -5,6 +5,6 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
text: { type: String, required: true }, text: { type: String, required: true },
color: { type: String, default: "#000" } color: { type: String, default: "#000" },
}) });
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<span :style="color? `color:${color}` : ''">{{ $dayjs(date).format('DD/MM/YYYY') }}</span> <span :style="color ? `color:${color}` : ''">{{ $dayjs(date).format("DD/MM/YYYY") }}</span>
</template> </template>
<script setup> <script setup>
const { $dayjs } = useNuxtApp() const { $dayjs } = useNuxtApp();
const props = defineProps({ const props = defineProps({
date: String, date: String,
color: String color: String,
}) });
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<span :style="color? `color:${color}` : ''">{{ value === 0 || value === null ? '-' : $numtoString(value) }}</span> <span :style="color ? `color:${color}` : ''">{{ value === 0 || value === null ? "-" : $numtoString(value) }}</span>
</template> </template>
<script setup> <script setup>
const { $numtoString } = useNuxtApp() const { $numtoString } = useNuxtApp();
const props = defineProps({ const props = defineProps({
value: Number, value: Number,
color: String color: String,
}) });
</script> </script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<span :style="color? `color:${color}` : ''">{{ $numtoString(value) }}</span> <span :style="color ? `color:${color}` : ''">{{ $numtoString(value) }}</span>
</template> </template>
<script setup> <script setup>
const { $numtoString } = useNuxtApp() const { $numtoString } = useNuxtApp();
const props = defineProps({ const props = defineProps({
value: Number, value: Number,
color: String color: String,
}) });
</script> </script>

View File

@@ -1,28 +1,54 @@
<template> <template>
<div class="control has-icons-left" :id="docid"> <div
<div :class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`" style="width: 100%"> class="control has-icons-left"
<div class="dropdown-trigger" style="width: 100%;"> :id="docid"
<input :disabled="disabled" :class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-dark' : ''}`" type="text" placeholder="DD/MM/YYYY" >
maxlength="10" @focus="setFocus" @blur="lostFocus" @keyup.enter="pressEnter" @keyup="checkDate" v-model="show" /> <div
</div> :class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`"
<div class="dropdown-menu" role="menu" @click="doClick()"> style="width: 100%"
<div class="dropdown-content"> >
<PickDay v-bind="{ date, maxdate }" @date="selectDate"></PickDay> <div
class="dropdown-trigger"
style="width: 100%"
>
<input
:disabled="disabled"
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-dark' : ''}`"
type="text"
placeholder="DD/MM/YYYY"
maxlength="10"
@focus="setFocus"
@blur="lostFocus"
@keyup.enter="pressEnter"
@keyup="checkDate"
v-model="show"
/>
</div>
<div
class="dropdown-menu"
role="menu"
@click="doClick()"
>
<div class="dropdown-content">
<PickDay
v-bind="{ date, maxdate }"
@date="selectDate"
></PickDay>
</div>
</div>
</div> </div>
</div> <span class="icon is-left">
</div> <Icon
<span class="icon is-left"> name="material-symbols:calendar-today-outline-rounded"
<Icon :size="21"
name="material-symbols:calendar-today-outline-rounded" class="has-text-grey"
:size="21" />
class="has-text-grey" </span>
/>
</span>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['record', 'attr', 'position', 'mindate', 'maxdate', 'disabled'], props: ["record", "attr", "position", "mindate", "maxdate", "disabled"],
data() { data() {
return { return {
date: undefined, date: undefined,
@@ -32,103 +58,102 @@ export default {
docid: this.$id(), docid: this.$id(),
count1: 0, count1: 0,
count2: 0, count2: 0,
pos: undefined pos: undefined,
} };
}, },
created() { created() {
this.getPos() this.getPos();
if(this.record) { if (this.record) {
this.date = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined this.date = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
if(this.date) this.show = this.$dayjs(this.date).format('DD/MM/YYYY') if (this.date) this.show = this.$dayjs(this.date).format("DD/MM/YYYY");
} }
}, },
watch: { watch: {
record: function(newVal) { record: function (newVal) {
if(this.record) { if (this.record) {
this.date = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined this.date = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
if(this.date) this.show = this.$dayjs(this.date).format('DD/MM/YYYY') if (this.date) this.show = this.$dayjs(this.date).format("DD/MM/YYYY");
} }
} },
}, },
methods: { methods: {
pressEnter() { pressEnter() {
this.checkDate() this.checkDate();
if(!this.error) this.focused = false if (!this.error) this.focused = false;
}, },
setFocus() { setFocus() {
this.focused = true this.focused = true;
this.count1 = 0 this.count1 = 0;
this.count2 = 0 this.count2 = 0;
}, },
lostFocus() { lostFocus() {
let self = this let self = this;
setTimeout(()=>{ setTimeout(() => {
if(self.focused && self.count1===0) self.focused = false if (self.focused && self.count1 === 0) self.focused = false;
}, 200) }, 200);
}, },
processEvent(event) { processEvent(event) {
var doc = document.getElementById(this.docid) var doc = document.getElementById(this.docid);
if(!doc) return if (!doc) return;
this.count2 += 1 this.count2 += 1;
var isClickInside = false var isClickInside = false;
isClickInside = doc.contains(event.target); isClickInside = doc.contains(event.target);
if(!isClickInside && this.focused) { if (!isClickInside && this.focused) {
if(this.count2-1!==this.count1) { if (this.count2 - 1 !== this.count1) {
this.focused = false this.focused = false;
this.count1 = 0 this.count1 = 0;
this.count2 = 0 this.count2 = 0;
} }
} }
}, },
doClick() { doClick() {
this.count1 += 1 this.count1 += 1;
}, },
selectDate(v) { selectDate(v) {
this.date = v this.date = v;
this.show = this.$dayjs(v).format('DD/MM/YYYY') this.show = this.$dayjs(v).format("DD/MM/YYYY");
this.$emit('date', this.date) this.$emit("date", this.date);
if(this.focused) this.focused = false if (this.focused) this.focused = false;
this.count1 = 0 this.count1 = 0;
this.count2 = 0 this.count2 = 0;
}, },
getDate(value) { getDate(value) {
let v = value.replace(/\D/g,'').slice(0, 10); let v = value.replace(/\D/g, "").slice(0, 10);
if (v.length >= 5) { if (v.length >= 5) {
return `${v.slice(0,2)}/${v.slice(2,4)}/${v.slice(4)}`; return `${v.slice(0, 2)}/${v.slice(2, 4)}/${v.slice(4)}`;
} else if (v.length >= 3) {
return `${v.slice(0, 2)}/${v.slice(2)}`;
} }
else if (v.length >= 3) { return v;
return `${v.slice(0,2)}/${v.slice(2)}`;
}
return v
}, },
checkDate() { checkDate() {
if(!this.focused) this.setFocus() if (!this.focused) this.setFocus();
this.error = false this.error = false;
this.date = undefined this.date = undefined;
if(this.$empty(this.show)) return this.$emit('date', null) if (this.$empty(this.show)) return this.$emit("date", null);
this.show = this.getDate(this.show) this.show = this.getDate(this.show);
let val = `${this.show.substring(6,10)}-${this.show.substring(3,5)}-${this.show.substring(0,2)}` let val = `${this.show.substring(6, 10)}-${this.show.substring(3, 5)}-${this.show.substring(0, 2)}`;
if(this.$dayjs(val, "YYYY-MM-DD", true).isValid()) { if (this.$dayjs(val, "YYYY-MM-DD", true).isValid()) {
this.date = val this.date = val;
this.$emit('date', this.date) this.$emit("date", this.date);
} else this.error = true } else this.error = true;
}, },
getPos() { getPos() {
switch(this.position) { switch (this.position) {
case 'is-top-left': case "is-top-left":
this.pos = 'is-up is-left' this.pos = "is-up is-left";
break; break;
case 'is-top-right': case "is-top-right":
this.pos = 'is-up is-right' this.pos = "is-up is-right";
break; break;
case 'is-bottom-left': case "is-bottom-left":
this.pos = 'is-left' this.pos = "is-left";
break; break;
case 'is-bottom-right': case "is-bottom-right":
this.pos = 'is-right' this.pos = "is-right";
break; break;
} }
} },
} },
} };
</script> </script>

View File

@@ -1,185 +1,270 @@
<template> <template>
<div> <div>
<div class="field is-grouped mb-4 border-bottom" v-if="1<0"> <div
<div class="control pl-2" v-if="mode!=='simple'"> class="field is-grouped mb-4 border-bottom"
<a class="mr-1" @click="previousYear()"> v-if="1 < 0"
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon> >
</a> <div
<a @click="previousMonth()" v-if="type==='days'"> class="control pl-2"
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon> v-if="mode !== 'simple'"
</a> >
<a
class="mr-1"
@click="previousYear()"
>
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
<a
@click="previousMonth()"
v-if="type === 'days'"
>
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
</div> </div>
<div class="control is-expanded has-text-centered"> <div class="control is-expanded has-text-centered">
<span class="fsb-16 hyperlink mr-3" @click="type='months'" v-if="type==='days'">{{`Tháng ${month}`}}</span> <span
<span class="fsb-16 hyperlink" @click="type='years'">{{ caption || year }}</span> class="fsb-16 hyperlink mr-3"
@click="type = 'months'"
v-if="type === 'days'"
>{{ `Tháng ${month}` }}</span
>
<span
class="fsb-16 hyperlink"
@click="type = 'years'"
>{{ caption || year }}</span
>
</div> </div>
<div class="control pr-2" v-if="mode!=='simple'"> <div
<a class="mr-1" @click="nextMonth()" v-if="type==='days'"> class="control pr-2"
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon> v-if="mode !== 'simple'"
</a> >
<a @click="nextYear()"> <a
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon> class="mr-1"
</a> @click="nextMonth()"
v-if="type === 'days'"
>
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
<a @click="nextYear()">
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
</div> </div>
</div> </div>
<div v-if="type==='days'"> <div v-if="type === 'days'">
<div :class="`columns mx-0 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i"> <div
<div class="column px-3 py-1 has-text-centered" v-for="(m,h) in v.dates" :key="h" style="min-height: 100px;" :class="`columns mx-0 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
:style="`border-right: 1px solid #DCDCDC; border-bottom: 1px solid #DCDCDC; ${(viewport>1 && h===0 || viewport===1)? 'border-left: 1px solid #DCDCDC;' : ''} v-for="(v, i) in weeks"
${i===0? 'border-top: 1px solid #DCDCDC;' : ''}`"> :key="i"
<p class="mb-1" v-if="i===0"><b>{{ $find(dateOfWeek, {id: h}).text}}</b></p> >
<span class="has-background-primary has-text-white px-1 py-1" v-if="m.date===today">{{ m.dayPrint }}</span> <div
<span v-else>{{m.dayPrint}}</span> class="column px-3 py-1 has-text-centered"
<div class="has-text-left fs-15 mt-1" v-if="m.event && m.currentMonth===m.mothCondition"> v-for="(m, h) in v.dates"
<p :class="`pt-1 ${j===m.event.length-1? '' : 'border-bottom'}`" v-for="(h,j) in m.event"> :key="h"
<SvgIcon v-bind="{name: h.icon, type: h.color, size: 16}"></SvgIcon> style="min-height: 100px"
<a class="ml-3" @click="openEvent(h)">{{ h.text }}</a> :style="`border-right: 1px solid #DCDCDC; border-bottom: 1px solid #DCDCDC; ${(viewport > 1 && h === 0) || viewport === 1 ? 'border-left: 1px solid #DCDCDC;' : ''}
${i === 0 ? 'border-top: 1px solid #DCDCDC;' : ''}`"
>
<p
class="mb-1"
v-if="i === 0"
>
<b>{{ $find(dateOfWeek, { id: h }).text }}</b>
</p> </p>
<span
class="has-background-primary has-text-white px-1 py-1"
v-if="m.date === today"
>{{ m.dayPrint }}</span
>
<span v-else>{{ m.dayPrint }}</span>
<div
class="has-text-left fs-15 mt-1"
v-if="m.event && m.currentMonth === m.mothCondition"
>
<p
:class="`pt-1 ${j === m.event.length - 1 ? '' : 'border-bottom'}`"
v-for="(h, j) in m.event"
>
<SvgIcon v-bind="{ name: h.icon, type: h.color, size: 16 }"></SvgIcon>
<a
class="ml-3"
@click="openEvent(h)"
>{{ h.text }}</a
>
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div> </div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</div>
</template> </template>
<script> <script>
export default { export default {
props: ['date', 'events', 'mode', 'vyear', 'vmonth'], props: ["date", "events", "mode", "vyear", "vmonth"],
data() { data() {
return { return {
dates: [], dates: [],
dateOfWeek: [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"}, dateOfWeek: [
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}], { id: 0, text: "CN" },
{ id: 1, text: "T2" },
{ id: 2, text: "T3" },
{ id: 3, text: "T4" },
{ id: 4, text: "T5" },
{ id: 5, text: "T6" },
{ id: 6, text: "T7" },
],
weeks: [], weeks: [],
today: this.$dayjs().format('YYYY/MM/DD'), today: this.$dayjs().format("YYYY/MM/DD"),
year: undefined, year: undefined,
month: undefined, month: undefined,
type: 'days', type: "days",
caption: undefined, caption: undefined,
action: undefined, action: undefined,
curdate: undefined, curdate: undefined,
showmodal: undefined, showmodal: undefined,
viewport: 5 viewport: 5,
} };
}, },
created() { created() {
this.showDate() this.showDate();
}, },
watch: { watch: {
date: function(newVal) { date: function (newVal) {
if(newVal) this.showDate() if (newVal) this.showDate();
}, },
vmonth: function(newVal) { vmonth: function (newVal) {
this.showDate() this.showDate();
},
events: function (newVal) {
this.showDate();
}, },
events: function(newVal) {
this.showDate()
}
}, },
methods: { methods: {
async openEvent(event) { async openEvent(event) {
let row = await this.$getdata('sale', {id: event.sale}, undefined, true) let row = await this.$getdata("sale", { id: event.sale }, undefined, true);
this.showmodal = {title: 'Bán hàng', height: '500px', width: '90%', component: 'sale/Sale', vbind: {row: row, highlight: event.id}} this.showmodal = {
title: "Bán hàng",
height: "500px",
width: "90%",
component: "sale/Sale",
vbind: { row: row, highlight: event.id },
};
}, },
compiledComponent(value) { compiledComponent(value) {
return { return {
template: `${value}` template: `${value}`,
} };
}, },
showDate() { showDate() {
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
this.year = this.$dayjs(this.curdate || this.today).year() this.year = this.$dayjs(this.curdate || this.today).year();
this.month = this.$dayjs(this.curdate || this.today).month() + 1 this.month = this.$dayjs(this.curdate || this.today).month() + 1;
if(this.vyear) this.year = this.$copy(this.vyear) if (this.vyear) this.year = this.$copy(this.vyear);
if(this.vmonth) this.month = this.$copy(this.vmonth) if (this.vmonth) this.month = this.$copy(this.vmonth);
this.getDates() this.getDates();
}, },
chooseToday() { chooseToday() {
this.$emit('date', this.today.replaceAll('/', '-')) this.$emit("date", this.today.replaceAll("/", "-"));
this.year = this.$dayjs(this.today).year() this.year = this.$dayjs(this.today).year();
this.month = this.$dayjs(this.today).month() + 1 this.month = this.$dayjs(this.today).month() + 1;
this.getDates() this.getDates();
}, },
changeCaption(v) { changeCaption(v) {
this.caption = v this.caption = v;
}, },
selectMonth(v) { selectMonth(v) {
this.month = v this.month = v;
this.getDates() this.getDates();
this.type = 'days' this.type = "days";
}, },
selectYear(v) { selectYear(v) {
this.year = v this.year = v;
this.getDates() this.getDates();
this.type = 'days' this.type = "days";
}, },
getDates() { getDates() {
this.caption = undefined this.caption = undefined;
this.dates = this.allDaysInMonth(this.year, this.month) this.dates = this.allDaysInMonth(this.year, this.month);
this.dates.map(v=>{ this.dates.map((v) => {
let event = this.events? this.$filter(this.events, {isodate: v.date}) : undefined let event = this.events ? this.$filter(this.events, { isodate: v.date }) : undefined;
if(event.length>0) v.event = event if (event.length > 0) v.event = event;
}) });
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}}) this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
this.weeks.map(v=>{ return { week: v.week };
v.dates = this.dates.filter(x=>x.week===v.week) });
}) this.weeks.map((v) => {
v.dates = this.dates.filter((x) => x.week === v.week);
});
}, },
nextMonth() { nextMonth() {
let month = this.month + 1 let month = this.month + 1;
if(month>12) { if (month > 12) {
month = 1 month = 1;
this.year += 1 this.year += 1;
} }
this.month = month this.month = month;
this.getDates() this.getDates();
}, },
previousMonth() { previousMonth() {
let month = this.month - 1 let month = this.month - 1;
if(month===0) { if (month === 0) {
month = 12 month = 12;
this.year -= 1 this.year -= 1;
} }
this.month = month this.month = month;
this.getDates() this.getDates();
}, },
nextYear() { nextYear() {
if(this.type==='years') return this.action = {name: 'next', id: this.$id()} if (this.type === "years") return (this.action = { name: "next", id: this.$id() });
this.year += 1 this.year += 1;
this.getDates() this.getDates();
}, },
previousYear() { previousYear() {
if(this.type==='years') return this.action = {name: 'previous', id: this.$id()} if (this.type === "years") return (this.action = { name: "previous", id: this.$id() });
this.year -= 1 this.year -= 1;
this.getDates() this.getDates();
}, },
choose(m) { choose(m) {
this.$emit('date', m.date.replaceAll('/', '-')) this.$emit("date", m.date.replaceAll("/", "-"));
}, },
createDate(v, x, y) { createDate(v, x, y) {
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString()) return (
v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString())
);
}, },
allDaysInMonth(year, month) { allDaysInMonth(year, month) {
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1) let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
let arr = [] let arr = [];
days.map(v=>{ days.map((v) => {
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i) let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i);
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD") let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD") let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
let mothCondition = this.$dayjs(date).month() +1 let mothCondition = this.$dayjs(date).month() + 1;
let currentMonth = month let currentMonth = month;
let found = arr.find(x=>x.date===date) let found = arr.find((x) => x.date === date);
if(!found) { if (!found) {
let dayOfWeek = this.$dayjs(date).day() let dayOfWeek = this.$dayjs(date).day();
let week = this.$dayjs(date).week() let week = this.$dayjs(date).week();
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, mothCondition: mothCondition, currentMonth: currentMonth} let ele = {
arr.push(ele) date: date,
week: week,
day: v,
dayOfWeek: dayOfWeek,
dayPrint: dayPrint,
mothCondition: mothCondition,
currentMonth: currentMonth,
};
arr.push(ele);
} }
} }
}) });
return arr return arr;
} },
} },
} };
</script> </script>

View File

@@ -1,19 +1,29 @@
<template> <template>
<div> <div>
<div class="columns is-multiline mx-0"> <div class="columns is-multiline mx-0">
<div class="column is-4" v-for="v in months"> <div
<EventSummary v-bind="{events: events, mode: 'simple', vyear: v.year, vmonth: v.month}"></EventSummary> class="column is-4"
v-for="v in months"
>
<EventSummary
v-bind="{
events: events,
mode: 'simple',
vyear: v.year,
vmonth: v.month,
}"
></EventSummary>
</div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import EventSummary from '@/components/datepicker/EventSummary' import EventSummary from "@/components/datepicker/EventSummary";
export default { export default {
components: { components: {
EventSummary EventSummary,
//EventSummary: () => import('@/components/datepicker/EventSummary') //EventSummary: () => import('@/components/datepicker/EventSummary')
}, },
props: ['events', 'months'] props: ["events", "months"],
} };
</script> </script>

View File

@@ -1,52 +1,63 @@
<template> <template>
<div class="columns mx-0"> <div class="columns mx-0">
<div class="column is-narrow" v-if="1<0"> <div
class="column is-narrow"
v-if="1 < 0"
>
<EventSummary></EventSummary> <EventSummary></EventSummary>
</div> </div>
<div class="column"> <div class="column">
<div class="mb-4"> <div class="mb-4">
<span class="fsb-17 mr-4">{{ `T${vmonth}/${vyear}` }}</span> <span class="fsb-17 mr-4">{{ `T${vmonth}/${vyear}` }}</span>
<a class="mr-2" @click="previous()"><SvgIcon v-bind="{name: 'left1.svg', type: 'dark', size: 18}"></SvgIcon></a> <a
<a class="mr-3" @click="next()"><SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 18}"></SvgIcon></a> class="mr-2"
<a @click="refresh()"><SvgIcon v-bind="{name: 'refresh.svg', type: 'dark', size: 18}"></SvgIcon></a> @click="previous()"
><SvgIcon v-bind="{ name: 'left1.svg', type: 'dark', size: 18 }"></SvgIcon
></a>
<a
class="mr-3"
@click="next()"
><SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 18 }"></SvgIcon
></a>
<a @click="refresh()"><SvgIcon v-bind="{ name: 'refresh.svg', type: 'dark', size: 18 }"></SvgIcon></a>
</div> </div>
<EventDetail v-bind="{events: events, vyear: vyear, vmonth: vmonth}"></EventDetail> <EventDetail v-bind="{ events: events, vyear: vyear, vmonth: vmonth }"></EventDetail>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import EventSummary from '@/components/datepicker/EventSummary' import EventSummary from "@/components/datepicker/EventSummary";
import EventDetail from '@/components/datepicker/EventDetail' import EventDetail from "@/components/datepicker/EventDetail";
</script> </script>
<script> <script>
export default { export default {
props: ['events', 'year', 'month'], props: ["events", "year", "month"],
data() { data() {
return { return {
vyear: this.year? this.$copy(this.year) : undefined, vyear: this.year ? this.$copy(this.year) : undefined,
vmonth: this.month? this.$copy(this.month) : undefined vmonth: this.month ? this.$copy(this.month) : undefined,
} };
}, },
methods: { methods: {
next() { next() {
let month = this.vmonth + 1 let month = this.vmonth + 1;
if(month>12) { if (month > 12) {
month = 1 month = 1;
this.vyear += 1 this.vyear += 1;
} }
this.vmonth = month this.vmonth = month;
}, },
previous() { previous() {
let month = this.vmonth - 1 let month = this.vmonth - 1;
if(month===0) { if (month === 0) {
month = 12 month = 12;
this.vyear -= 1 this.vyear -= 1;
} }
this.vmonth = month this.vmonth = month;
}, },
refresh() { refresh() {
this.$emit('refresh') this.$emit("refresh");
} },
} },
} };
</script> </script>

View File

@@ -1,189 +1,261 @@
<template> <template>
<div> <div>
<div class="field is-grouped mb-4 border-bottom"> <div class="field is-grouped mb-4 border-bottom">
<div class="control pl-2" v-if="mode!=='simple'"> <div
<a class="mr-1" @click="previousYear()"> class="control pl-2"
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon> v-if="mode !== 'simple'"
</a> >
<a @click="previousMonth()" v-if="type==='days'"> <a
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon> class="mr-1"
</a> @click="previousYear()"
>
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
<a
@click="previousMonth()"
v-if="type === 'days'"
>
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
</div> </div>
<div class="control is-expanded has-text-centered"> <div class="control is-expanded has-text-centered">
<span class="fsb-16 hyperlink mr-3" @click="mode==='simple'? false : type='PickMonth'" v-if="type==='days'">{{`Tháng ${month}`}}</span> <span
<span class="fsb-16 hyperlink" @click="mode==='simple'? false : type='PickYear'">{{ caption || year }}</span> class="fsb-16 hyperlink mr-3"
@click="mode === 'simple' ? false : (type = 'PickMonth')"
v-if="type === 'days'"
>{{ `Tháng ${month}` }}</span
>
<span
class="fsb-16 hyperlink"
@click="mode === 'simple' ? false : (type = 'PickYear')"
>{{ caption || year }}</span
>
</div> </div>
<div class="control pr-2" v-if="mode!=='simple'"> <div
<a class="mr-1" @click="nextMonth()" v-if="type==='days'"> class="control pr-2"
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon> v-if="mode !== 'simple'"
</a> >
<a @click="nextYear()"> <a
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon> class="mr-1"
</a> @click="nextMonth()"
</div> v-if="type === 'days'"
</div> >
<div v-if="type==='days'"> <SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
<div class="columns is-mobile mx-0"> </a>
<div class="column px-2 py-1 has-text-grey-dark" v-for="(m,h) in dateOfWeek" :key="h"> <a @click="nextYear()">
{{ m.text }} <SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
</div>
</div>
<div :class="`columns is-mobile mx-0 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
<div class="column px-3 fs-14" v-for="(m,h) in v.dates" :key="h" style="padding-top: 1px; padding-bottom: 1px;">
<a v-if="m.event && m.currentMonth===m.mothCondition">
<Tooltip v-bind="{html: m.event.html, tooltip: m.event.tooltip}"></Tooltip>
</a> </a>
<span class="has-background-findata has-text-white px-1 py-1" v-else-if="m.date===today">{{ m.dayPrint }}</span>
<span v-else>{{m.dayPrint}}</span>
</div> </div>
</div> </div>
<template v-if="mode!=='simple'"> <div v-if="type === 'days'">
<div class="border-bottom"></div> <div class="columns is-mobile mx-0">
<div class="mt-2"> <div
<span class="ml-2 mr-2">Hôm nay: </span> class="column px-2 py-1 has-text-grey-dark"
<span class="has-text-primary hyperlink" @click="chooseToday()">{{ $dayjs(today).format('DD/MM/YYYY') }}</span> v-for="(m, h) in dateOfWeek"
:key="h"
>
{{ m.text }}
</div>
</div>
<div
:class="`columns is-mobile mx-0 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
v-for="(v, i) in weeks"
:key="i"
>
<div
class="column px-3 fs-14"
v-for="(m, h) in v.dates"
:key="h"
style="padding-top: 1px; padding-bottom: 1px"
>
<a v-if="m.event && m.currentMonth === m.mothCondition">
<Tooltip v-bind="{ html: m.event.html, tooltip: m.event.tooltip }"></Tooltip>
</a>
<span
class="has-background-findata has-text-white px-1 py-1"
v-else-if="m.date === today"
>{{ m.dayPrint }}</span
>
<span v-else>{{ m.dayPrint }}</span>
</div>
</div>
<template v-if="mode !== 'simple'">
<div class="border-bottom"></div>
<div class="mt-2">
<span class="ml-2 mr-2">Hôm nay: </span>
<span
class="has-text-primary hyperlink"
@click="chooseToday()"
>{{ $dayjs(today).format("DD/MM/YYYY") }}</span
>
</div>
</template>
</div> </div>
</template> <div v-else-if="type === 'PickMonth'">
<PickMonth @month="selectMonth"></PickMonth>
</div>
<div v-else-if="type === 'PickYear'">
<PickYear
v-bind="{ year: year, month: month, action: action }"
@year="selectYear"
@caption="changeCaption"
></PickYear>
</div>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div> </div>
<div v-else-if="type==='PickMonth'">
<PickMonth @month="selectMonth"></PickMonth>
</div>
<div v-else-if="type==='PickYear'">
<PickYear v-bind="{year: year, month: month, action: action}" @year="selectYear" @caption="changeCaption"></PickYear>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</div>
</template> </template>
<script> <script>
export default { export default {
components: { components: {
PickMonth: () => import('@/components/datepicker/PickMonth'), PickMonth: () => import("@/components/datepicker/PickMonth"),
PickYear: () => import('@/components/datepicker/PickYear') PickYear: () => import("@/components/datepicker/PickYear"),
}, },
props: ['date', 'events', 'mode', 'vyear', 'vmonth'], props: ["date", "events", "mode", "vyear", "vmonth"],
data() { data() {
return { return {
dates: [], dates: [],
dateOfWeek: [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"}, dateOfWeek: [
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}], { id: 0, text: "CN" },
{ id: 1, text: "T2" },
{ id: 2, text: "T3" },
{ id: 3, text: "T4" },
{ id: 4, text: "T5" },
{ id: 5, text: "T6" },
{ id: 6, text: "T7" },
],
weeks: [], weeks: [],
today: this.$dayjs().format('YYYY/MM/DD'), today: this.$dayjs().format("YYYY/MM/DD"),
year: undefined, year: undefined,
month: undefined, month: undefined,
type: 'days', type: "days",
caption: undefined, caption: undefined,
action: undefined, action: undefined,
curdate: undefined, curdate: undefined,
showmodal: undefined showmodal: undefined,
} };
}, },
created() { created() {
this.showDate() this.showDate();
}, },
watch: { watch: {
date: function(newVal) { date: function (newVal) {
if(newVal) this.showDate() if (newVal) this.showDate();
},
events: function (newVal) {
this.showDate();
}, },
events: function(newVal) {
this.showDate()
}
}, },
methods: { methods: {
showDate() { showDate() {
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
this.year = this.$dayjs(this.curdate || this.today).year() this.year = this.$dayjs(this.curdate || this.today).year();
this.month = this.$dayjs(this.curdate || this.today).month() + 1 this.month = this.$dayjs(this.curdate || this.today).month() + 1;
if(this.vyear) this.year = this.$copy(this.vyear) if (this.vyear) this.year = this.$copy(this.vyear);
if(this.vmonth) this.month = this.$copy(this.vmonth) if (this.vmonth) this.month = this.$copy(this.vmonth);
this.getDates() this.getDates();
}, },
chooseToday() { chooseToday() {
this.$emit('date', this.today.replaceAll('/', '-')) this.$emit("date", this.today.replaceAll("/", "-"));
this.year = this.$dayjs(this.today).year() this.year = this.$dayjs(this.today).year();
this.month = this.$dayjs(this.today).month() + 1 this.month = this.$dayjs(this.today).month() + 1;
this.getDates() this.getDates();
}, },
changeCaption(v) { changeCaption(v) {
this.caption = v this.caption = v;
}, },
selectMonth(v) { selectMonth(v) {
this.month = v this.month = v;
this.getDates() this.getDates();
this.type = 'days' this.type = "days";
}, },
selectYear(v) { selectYear(v) {
this.year = v this.year = v;
this.getDates() this.getDates();
this.type = 'days' this.type = "days";
}, },
getDates() { getDates() {
this.caption = undefined this.caption = undefined;
this.dates = this.allDaysInMonth(this.year, this.month) this.dates = this.allDaysInMonth(this.year, this.month);
this.dates.map(v=>{ this.dates.map((v) => {
let event = this.events? this.$find(this.events, {isodate: v.date}) : undefined let event = this.events ? this.$find(this.events, { isodate: v.date }) : undefined;
if(event) v.event = event if (event) v.event = event;
}) });
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}}) this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
this.weeks.map(v=>{ return { week: v.week };
v.dates = this.dates.filter(x=>x.week===v.week) });
}) this.weeks.map((v) => {
v.dates = this.dates.filter((x) => x.week === v.week);
});
}, },
nextMonth() { nextMonth() {
let month = this.month + 1 let month = this.month + 1;
if(month>12) { if (month > 12) {
month = 1 month = 1;
this.year += 1 this.year += 1;
} }
this.month = month this.month = month;
this.getDates() this.getDates();
}, },
previousMonth() { previousMonth() {
let month = this.month - 1 let month = this.month - 1;
if(month===0) { if (month === 0) {
month = 12 month = 12;
this.year -= 1 this.year -= 1;
} }
this.month = month this.month = month;
this.getDates() this.getDates();
}, },
nextYear() { nextYear() {
if(this.type==='PickYear') return this.action = {name: 'next', id: this.$id()} if (this.type === "PickYear") return (this.action = { name: "next", id: this.$id() });
this.year += 1 this.year += 1;
this.getDates() this.getDates();
}, },
previousYear() { previousYear() {
if(this.type==='PickYear') return this.action = {name: 'previous', id: this.$id()} if (this.type === "PickYear") return (this.action = { name: "previous", id: this.$id() });
this.year -= 1 this.year -= 1;
this.getDates() this.getDates();
}, },
choose(m) { choose(m) {
this.$emit('date', m.date.replaceAll('/', '-')) this.$emit("date", m.date.replaceAll("/", "-"));
}, },
createDate(v, x, y) { createDate(v, x, y) {
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString()) return (
v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString())
);
}, },
allDaysInMonth(year, month) { allDaysInMonth(year, month) {
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1) let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
let arr = [] let arr = [];
days.map(v=>{ days.map((v) => {
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i) let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i);
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD") let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD") let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
let mothCondition = this.$dayjs(date).month() +1 let mothCondition = this.$dayjs(date).month() + 1;
let currentMonth = month let currentMonth = month;
let found = arr.find(x=>x.date===date) let found = arr.find((x) => x.date === date);
if(!found) { if (!found) {
let dayOfWeek = this.$dayjs(date).day() let dayOfWeek = this.$dayjs(date).day();
let week = this.$dayjs(date).week() let week = this.$dayjs(date).week();
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, let ele = {
mothCondition: mothCondition, currentMonth: currentMonth} date: date,
arr.push(ele) week: week,
day: v,
dayOfWeek: dayOfWeek,
dayPrint: dayPrint,
mothCondition: mothCondition,
currentMonth: currentMonth,
};
arr.push(ele);
} }
} }
}) });
return arr return arr;
} },
} },
} };
</script> </script>

View File

@@ -1,53 +1,91 @@
<template> <template>
<div class="p-2" style="width: 300px"> <div
class="p-2"
style="width: 300px"
>
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control pl-2"> <div class="control pl-2">
<a class="mr-1" @click="previousYear()"> <a
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon> class="mr-1"
</a> @click="previousYear()"
<a @click="previousMonth()" v-if="type==='days'"> >
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon> <SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
</a> </a>
<a
@click="previousMonth()"
v-if="type === 'days'"
>
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
</div> </div>
<div class="control is-expanded has-text-centered"> <div class="control is-expanded has-text-centered">
<a class="font-bold mr-2" @click="type='months'" v-if="type==='days'">{{`T${month}`}}</a> <a
<a class="font-bold" @click="type='years'">{{ caption || year }}</a> class="font-bold mr-2"
@click="type = 'months'"
v-if="type === 'days'"
>{{ `T${month}` }}</a
>
<a
class="font-bold"
@click="type = 'years'"
>{{ caption || year }}</a
>
</div> </div>
<div class="control pr-2"> <div class="control pr-2">
<a class="mr-1" @click="nextMonth()" v-if="type==='days'"> <a
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon> class="mr-1"
@click="nextMonth()"
v-if="type === 'days'"
>
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
</a> </a>
<a @click="nextYear()"> <a @click="nextYear()">
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon> <SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
</a> </a>
</div> </div>
</div> </div>
<hr class="mt-0 mb-5" /> <hr class="mt-0 mb-5" />
<div v-if="type==='days'"> <div v-if="type === 'days'">
<div class="columns is-mobile mx-0 mb-3"> <div class="columns is-mobile mx-0 mb-3">
<div <div
v-for="(m, h) in dateOfWeek" v-for="(m, h) in dateOfWeek"
:key="h" :key="h"
class="fs-14 column p-0 has-text-grey is-flex is-justify-content-center is-align-items-center" class="fs-14 column p-0 has-text-grey is-flex is-justify-content-center is-align-items-center"
> >
{{ m.text }} {{ m.text }}
</div> </div>
</div> </div>
<div :class="`columns is-mobile mx-0 mb-1 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i"> <div
<div class="column p-0 is-flex is-justify-content-center is-align-items-center" style="width: 40px; height: 32px" v-for="(m,h) in v.dates" :key="h"> :class="`columns is-mobile mx-0 mb-1 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
<span class="fs-13 has-text-grey-80" v-if="m.disable"> v-for="(v, i) in weeks"
{{m.dayPrint}} :key="i"
>
<div
class="column p-0 is-flex is-justify-content-center is-align-items-center"
style="width: 40px; height: 32px"
v-for="(m, h) in v.dates"
:key="h"
>
<span
class="fs-13 has-text-grey-80"
v-if="m.disable"
>
{{ m.dayPrint }}
</span> </span>
<span class="fs-13 is-clickable" @click="choose(m)" v-else> <span
<span class="fs-13 is-clickable"
@click="choose(m)"
v-else
>
<span
style="width: 25px; height: 25px" style="width: 25px; height: 25px"
:class="[ :class="[
'p-1 rounded-md is-flex is-justify-content-center is-align-items-center', 'p-1 rounded-md is-flex is-justify-content-center is-align-items-center',
{ {
'has-background-primary-50 has-text-white': m.date === curdate, 'has-background-primary-50 has-text-white': m.date === curdate,
'has-background-success-50 has-text-white': m.date === today, 'has-background-success-50 has-text-white': m.date === today,
'has-text-grey-70': m.currentMonth !== m.monthCondition 'has-text-grey-70': m.currentMonth !== m.monthCondition,
} },
]" ]"
> >
{{ m.dayPrint }} {{ m.dayPrint }}
@@ -58,133 +96,165 @@
<hr class="my-1" /> <hr class="my-1" />
<div class="mt-2 fs-14"> <div class="mt-2 fs-14">
<span class="ml-2">Hôm nay: </span> <span class="ml-2">Hôm nay: </span>
<a class="has-text-primary" @click="chooseToday()">{{ $dayjs(today).format('DD/MM/YYYY') }}</a> <a
class="has-text-primary"
@click="chooseToday()"
>{{ $dayjs(today).format("DD/MM/YYYY") }}</a
>
</div> </div>
</div> </div>
<PickMonth v-else-if="type==='months'" @month="selectMonth"></PickMonth> <PickMonth
<PickYear v-else-if="type==='years'" v-bind="{ year, month, action }" @year="selectYear" @caption="changeCaption"></PickYear> v-else-if="type === 'months'"
@month="selectMonth"
></PickMonth>
<PickYear
v-else-if="type === 'years'"
v-bind="{ year, month, action }"
@year="selectYear"
@caption="changeCaption"
></PickYear>
</div> </div>
</template> </template>
<script setup> <script setup>
import PickMonth from '@/components/datepicker/PickMonth' import PickMonth from "@/components/datepicker/PickMonth";
import PickYear from '@/components/datepicker/PickYear' import PickYear from "@/components/datepicker/PickYear";
const { $id, $dayjs, $unique} = useNuxtApp() const { $id, $dayjs, $unique } = useNuxtApp();
const emit = defineEmits(['date']) const emit = defineEmits(["date"]);
var props = defineProps({ var props = defineProps({
date: String, date: String,
maxdate: String maxdate: String,
}) });
var dates = [] var dates = [];
var dateOfWeek = [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"}, var dateOfWeek = [
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}] { id: 0, text: "CN" },
var weeks = ref([]) { id: 1, text: "T2" },
var year= ref(undefined) { id: 2, text: "T3" },
var month = undefined { id: 3, text: "T4" },
var type = ref('days') { id: 4, text: "T5" },
var caption = ref(undefined) { id: 5, text: "T6" },
var action = ref(undefined) { id: 6, text: "T7" },
var curdate = undefined ];
var today = new Date() var weeks = ref([]);
function showDate() { var year = ref(undefined);
curdate = props.date? props.date.replaceAll('-', '/') : undefined var month = undefined;
year.value = $dayjs(curdate || today).year() var type = ref("days");
month = $dayjs(curdate || today).month() + 1 var caption = ref(undefined);
getDates() var action = ref(undefined);
} var curdate = undefined;
function chooseToday() { var today = new Date();
emit('date', $dayjs(today).format('YYYY-MM-DD') )//today.replaceAll('/', '-')) function showDate() {
year.value = $dayjs(today).year() curdate = props.date ? props.date.replaceAll("-", "/") : undefined;
month = $dayjs(today).month() + 1 year.value = $dayjs(curdate || today).year();
getDates() month = $dayjs(curdate || today).month() + 1;
} getDates();
function changeCaption(v) { }
caption.value = v function chooseToday() {
} emit("date", $dayjs(today).format("YYYY-MM-DD")); //today.replaceAll('/', '-'))
function selectMonth(v) { year.value = $dayjs(today).year();
month = v month = $dayjs(today).month() + 1;
getDates() getDates();
type.value = 'days' }
} function changeCaption(v) {
function selectYear(v) { caption.value = v;
year.value = v }
getDates() function selectMonth(v) {
type.value = 'days' month = v;
} getDates();
function getDates() { type.value = "days";
caption.value = undefined }
dates = allDaysInMonth(year.value, month) function selectYear(v) {
weeks.value = $unique(dates, ['week']).map(v=>{return {week: v.week}}) year.value = v;
weeks.value.map(v=>{ getDates();
v.dates = dates.filter(x=>x.week===v.week) type.value = "days";
}) }
} function getDates() {
function nextMonth() { caption.value = undefined;
month = month + 1 dates = allDaysInMonth(year.value, month);
if(month>12) { weeks.value = $unique(dates, ["week"]).map((v) => {
month = 1 return { week: v.week };
year.value += 1 });
} weeks.value.map((v) => {
getDates() v.dates = dates.filter((x) => x.week === v.week);
} });
function previousMonth() { }
month = month - 1 function nextMonth() {
if(month===0) { month = month + 1;
month = 12 if (month > 12) {
year.value -= 1 month = 1;
} year.value += 1;
getDates() }
} getDates();
function nextYear() { }
if(type.value==='years') return action.value = {name: 'next', id: $id()} function previousMonth() {
year.value += 1 month = month - 1;
getDates() if (month === 0) {
} month = 12;
function previousYear() { year.value -= 1;
if(type.value==='years') return action.value = {name: 'previous', id: $id()} }
year.value -= 1 getDates();
getDates() }
} function nextYear() {
function choose(m) { if (type.value === "years") return (action.value = { name: "next", id: $id() });
emit('date', m.date.replaceAll('/', '-')) year.value += 1;
} getDates();
function createDate(v, x, y) { }
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString()) function previousYear() {
} if (type.value === "years") return (action.value = { name: "previous", id: $id() });
function allDaysInMonth(year, month) { year.value -= 1;
let days = Array.from({length: $dayjs(createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1) getDates();
let arr = [] }
days.map(v=>{ function choose(m) {
for (let i = 0; i < 7; i++) { emit("date", m.date.replaceAll("/", "-"));
let thedate = $dayjs(createDate(year, month, v)).weekday(i) }
let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD") function createDate(v, x, y) {
let dayPrint = $dayjs(new Date(thedate.$d)).format("DD") return v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString());
let monthCondition = $dayjs(date).month() +1 }
let currentMonth = month function allDaysInMonth(year, month) {
let found = arr.find(x=>x.date===date) let days = Array.from({ length: $dayjs(createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
if(!found) { let arr = [];
let dayOfWeek = $dayjs(date).day() days.map((v) => {
let week = $dayjs(date).week() for (let i = 0; i < 7; i++) {
let disable = false let thedate = $dayjs(createDate(year, month, v)).weekday(i);
if(props.maxdate? $dayjs(props.maxdate).diff(props.date, 'day')>=0 : false) { let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
if($dayjs(props.maxdate).startOf('day').diff(date, 'day')<0) disable = true let dayPrint = $dayjs(new Date(thedate.$d)).format("DD");
} let monthCondition = $dayjs(date).month() + 1;
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, monthCondition: monthCondition, let currentMonth = month;
currentMonth: currentMonth, disable: disable} let found = arr.find((x) => x.date === date);
arr.push(ele) if (!found) {
} let dayOfWeek = $dayjs(date).day();
let week = $dayjs(date).week();
let disable = false;
if (props.maxdate ? $dayjs(props.maxdate).diff(props.date, "day") >= 0 : false) {
if ($dayjs(props.maxdate).startOf("day").diff(date, "day") < 0) disable = true;
} }
}) let ele = {
return arr date: date,
week: week,
day: v,
dayOfWeek: dayOfWeek,
dayPrint: dayPrint,
monthCondition: monthCondition,
currentMonth: currentMonth,
disable: disable,
};
arr.push(ele);
}
} }
// display });
showDate() return arr;
// change date }
watch(() => props.date, (newVal, oldVal) => { // display
showDate() showDate();
}) // change date
watch(
() => props.date,
(newVal, oldVal) => {
showDate();
},
);
</script> </script>
<style scoped> <style scoped>
a { a {
color: var(--bulma-link-60); color: var(--bulma-link-60);
} }
</style> </style>

View File

@@ -1,14 +1,19 @@
<template> <template>
<div class="columns is-mobile is-multiline mx-0"> <div class="columns is-mobile is-multiline mx-0">
<span class="column has-text-centered is-4 is-clickable fs-14" v-for="v in months" @click="$emit('month', v)">Tháng {{ v }}</span> <span
class="column has-text-centered is-4 is-clickable fs-14"
v-for="v in months"
@click="$emit('month', v)"
>Tháng {{ v }}</span
>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
months: [1,2,3,4,5,6,7,8,9,10,11,12] months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
} };
} },
} };
</script> </script>

View File

@@ -1,57 +1,65 @@
<template> <template>
<div class="columns is-mobile is-multiline mx-0"> <div class="columns is-mobile is-multiline mx-0">
<span <span
v-for="(v,i) in years" v-for="(v, i) in years"
class="column is-4 has-text-centered is-clickable fs-14" class="column is-4 has-text-centered is-clickable fs-14"
:class="i===0 || i===11 ? 'has-text-grey-light' : ''" :class="i === 0 || i === 11 ? 'has-text-grey-light' : ''"
@click="$emit('year', v)">{{ v }}</span> @click="$emit('year', v)"
>{{ v }}</span
>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['year', 'month', 'action'], props: ["year", "month", "action"],
data() { data() {
return { return {
years: [] years: [],
} };
}, },
created() { created() {
this.years = [this.year] this.years = [this.year];
for(let i = 1; i < 7; i++) { for (let i = 1; i < 7; i++) {
this.years.push(this.year+i) this.years.push(this.year + i);
this.years.push(this.year-i) this.years.push(this.year - i);
} }
this.years.sort(function(a, b) {return a - b;}) this.years.sort(function (a, b) {
this.years = this.years.slice(0,12) return a - b;
this.$emit('caption', `${this.years[1]}-${this.years[10]}`) });
this.years = this.years.slice(0, 12);
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
}, },
watch: { watch: {
action: function(newVal) { action: function (newVal) {
if(newVal.name==='next') this.next() if (newVal.name === "next") this.next();
if(newVal.name==='previous') this.previous() if (newVal.name === "previous") this.previous();
} },
}, },
methods: { methods: {
next() { next() {
let year = this.years[this.years.length-1] let year = this.years[this.years.length - 1];
this.years = [] this.years = [];
for(let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
this.years.push(year+i) this.years.push(year + i);
} }
this.years.sort(function(a, b) {return a - b;}) this.years.sort(function (a, b) {
this.years = this.years.slice(0,12) return a - b;
this.$emit('caption', `${this.years[1]}-${this.years[10]}`) });
this.years = this.years.slice(0, 12);
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
}, },
previous() { previous() {
let year = this.years[0] let year = this.years[0];
this.years = [] this.years = [];
for(let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
this.years.push(year-i) this.years.push(year - i);
} }
this.years.sort(function(a, b) {return a - b;}) this.years.sort(function (a, b) {
this.years = this.years.slice(0,12) return a - b;
this.$emit('caption', `${this.years[1]}-${this.years[10]}`) });
} this.years = this.years.slice(0, 12);
} this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
} },
},
};
</script> </script>

View File

@@ -1,9 +1,9 @@
<script setup> <script setup>
import Template1 from '@/lib/email/templates/Template1.vue'; import Template1 from "@/lib/email/templates/Template1.vue";
import { render } from '@vue-email/render'; import { render } from "@vue-email/render";
import { forEachAsync, isEqual } from 'es-toolkit'; import { forEachAsync, isEqual } from "es-toolkit";
const { const {
$dayjs, $dayjs,
$getdata, $getdata,
$insertapi, $insertapi,
@@ -19,9 +19,9 @@ const {
const payables = ref(null); const payables = ref(null);
const defaultFilter = { const defaultFilter = {
status: 1, status: 1,
to_date__gte: $dayjs().format('YYYY-MM-DD'), to_date__gte: $dayjs().format("YYYY-MM-DD"),
to_date__lte: undefined, to_date__lte: undefined,
} };
const filter = ref(defaultFilter); const filter = ref(defaultFilter);
const activeDateFilter = ref(null); const activeDateFilter = ref(null);
const key = ref(0); const key = ref(0);
@@ -35,35 +35,42 @@ function resetDateFilter() {
} }
onMounted(async () => { onMounted(async () => {
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'duepayables' }, sort: 'index' }); const payablesData = await $getdata("bizsetting", undefined, {
filter: { classify: "duepayables" },
sort: "index",
});
payables.value = payablesData; payables.value = payablesData;
}); });
watch(activeDateFilter, (val) => { watch(
if (!val) { activeDateFilter,
filter.value = defaultFilter; (val) => {
contents.value = null; if (!val) {
} else { filter.value = defaultFilter;
const cutoffDate = $dayjs().add(val.time, 'day').format('YYYY-MM-DD'); contents.value = null;
const filterField = `to_date__${val.lookup}`; } else {
filter.value = { const cutoffDate = $dayjs().add(val.time, "day").format("YYYY-MM-DD");
...defaultFilter, const filterField = `to_date__${val.lookup}`;
[filterField]: cutoffDate, filter.value = {
...defaultFilter,
[filterField]: cutoffDate,
};
} }
} },
}, { deep: true }) { deep: true },
);
const contents = ref(null); const contents = ref(null);
const isSending = ref(false); const isSending = ref(false);
function sanitizeContentPayment(text, maxLength = 80) { function sanitizeContentPayment(text, maxLength = 80) {
if (!text) return ''; if (!text) return "";
return text return text
.normalize('NFD') // bỏ dấu tiếng Việt .normalize("NFD") // bỏ dấu tiếng Việt
.replace(/[\u0300-\u036f]/g, '') .replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ .replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
.replace(/\s+/g, ' ') .replace(/\s+/g, " ")
.trim() .trim()
.slice(0, maxLength); .slice(0, maxLength);
} }
@@ -88,19 +95,19 @@ const buildContentPayment = (data) => {
cycle, cycle,
} = data; } = data;
if (customerType.toLowerCase() === 'cn') { if (customerType.toLowerCase() === "cn") {
if (customerName.length < 14) { if (customerName.length < 14) {
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
} else { } else {
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
} }
} else { } else {
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
} }
}; };
function replaceTemplateVars(html, paymentScheduleItem) { function replaceTemplateVars(html, paymentScheduleItem) {
const { const {
txn_detail__transaction__product__trade_code, txn_detail__transaction__product__trade_code,
txn_detail__transaction__customer__code, txn_detail__transaction__customer__code,
txn_detail__transaction__customer__fullname, txn_detail__transaction__customer__fullname,
@@ -112,118 +119,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
from_date, from_date,
to_date, to_date,
remain_amount, remain_amount,
cycle cycle,
} = paymentScheduleItem; } = paymentScheduleItem;
return html return html
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '') .replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '') .replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
.replace(/\[year]/g, new Date().getFullYear() || '') .replace(/\[year]/g, new Date().getFullYear() || "")
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code) .replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code)) .replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname) .replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
.replace( .replace(
/\[customer\.name\]/g, /\[customer\.name\]/g,
`${txn_detail__transaction__customer__type__code.toLowerCase() == 'cn' ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ''}` || `${txn_detail__transaction__customer__type__code.toLowerCase() == "cn" ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ""}` ||
'', "",
)
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
.replace(
/\[customer\.legal_code\]/g,
txn_detail__transaction__customer__legal_code || '',
) )
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || "")
.replace(/\[customer\.legal_code\]/g, txn_detail__transaction__customer__legal_code || "")
.replace( .replace(
/\[customer\.contact_address\]/g, /\[customer\.contact_address\]/g,
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
txn_detail__transaction__customer__address ||
'',
) )
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '') .replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '') .replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '') .replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '') .replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
.replace( .replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
/\[payment_schedule\.amount_in_word\]/g, .replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
$numberToVietnameseCurrency(remain_amount) || '',
)
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
.replace( .replace(
/\[payment_schedule\.cycle-in-words\]/g, /\[payment_schedule\.cycle-in-words\]/g,
`${cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || `${cycle == 0 ? "đặt cọc" : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || "",
'',
) )
.replace( .replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
/\[payment_schedule\.note\]/g,
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
);
} }
function quillToEmailHtml(html) { function quillToEmailHtml(html) {
return html return (
// ALIGN html
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"') // ALIGN
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"') .replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"') .replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"') .replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
// FONT SIZE // FONT SIZE
.replace(/ql-size-small/g, '') .replace(/ql-size-small/g, "")
.replace(/ql-size-large/g, '') .replace(/ql-size-large/g, "")
.replace(/ql-size-huge/g, '') .replace(/ql-size-huge/g, "")
// REMOVE EMPTY CLASS // REMOVE EMPTY CLASS
.replace(/class=""/g, '') .replace(/class=""/g, "")
; );
} }
const showmodal = ref(null); const showmodal = ref(null);
function openConfirmModal() { function openConfirmModal() {
showmodal.value = { showmodal.value = {
component: 'dialog/Confirm', component: "dialog/Confirm",
title: 'Xác nhận', title: "Xác nhận",
width: '500px', width: "500px",
height: '100px', height: "100px",
vbind: { vbind: {
content: 'Bạn có đồng ý gửi thông báo hàng loạt không?', content: "Bạn có đồng ý gửi thông báo hàng loạt không?",
} },
} };
} }
async function sendEmails() { async function sendEmails() {
isSending.value = true; isSending.value = true;
$snackbar('Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...'); $snackbar("Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...");
const paymentScheduleData = await $getdata( const paymentScheduleData = await $getdata("payment_schedule", undefined, {
'payment_schedule', filter: filter.value,
undefined, sort: "to_date",
{ values:
filter: filter.value, "penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry",
sort: 'to_date', });
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
}
);
const emailTemplate = await $getdata( const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
'emailtemplate',
{ id: activeDateFilter.value.emailTemplate },
undefined,
true
);
let message = emailTemplate.content.content; let message = emailTemplate.content.content;
contents.value = paymentScheduleData.map(paymentSchedule => { contents.value = paymentScheduleData.map((paymentSchedule) => {
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, ''); message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
const transfer = { const transfer = {
bank: { bank: {
code: 'MB', code: "MB",
name: 'MB Bank', name: "MB Bank",
}, },
account: { account: {
number: '146768686868', number: "146768686868",
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA', name: "CONG TY CO PHAN BAT DONG SAN UTOPIA",
}, },
content: 'Thanh toán đơn #xyz', content: "Thanh toán đơn #xyz",
}; };
transfer.content = buildContentPayment(paymentSchedule); transfer.content = buildContentPayment(paymentSchedule);
@@ -242,7 +230,7 @@ async function sendEmails() {
...emailTemplate.content, ...emailTemplate.content,
content: undefined, content: undefined,
message: replaceTemplateVars(message, paymentSchedule), message: replaceTemplateVars(message, paymentSchedule),
} };
}); });
await forEachAsync(contents.value, async (bigContent, i) => { await forEachAsync(contents.value, async (bigContent, i) => {
@@ -251,16 +239,16 @@ async function sendEmails() {
content: toRaw(bigContent), content: toRaw(bigContent),
previewMode: true, previewMode: true,
}; };
// ===== QUILL → HTML EMAIL (INLINE STYLE) ===== // ===== QUILL → HTML EMAIL (INLINE STYLE) =====
tempEm.content.message = quillToEmailHtml(message); tempEm.content.message = quillToEmailHtml(message);
let emailHtml = await render(Template1, tempEm); let emailHtml = await render(Template1, tempEm);
// If no image URL provided, remove image section from HTML // If no image URL provided, remove image section from HTML
if ((imageUrl ?? '').trim() === '') { if ((imageUrl ?? "").trim() === "") {
emailHtml = emailHtml emailHtml = emailHtml
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '') .replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
.replace(/\n\s*\n\s*\n/g, '\n\n'); .replace(/\n\s*\n\s*\n/g, "\n\n");
} }
// Replace keywords in HTML // Replace keywords in HTML
@@ -268,55 +256,65 @@ async function sendEmails() {
if (keyword && keyword.length > 0) { if (keyword && keyword.length > 0) {
keyword.forEach(({ keyword, value }) => { keyword.forEach(({ keyword, value }) => {
if (keyword && value) { if (keyword && value) {
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g'); const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
finalEmailHtml = finalEmailHtml.replace(regex, value); finalEmailHtml = finalEmailHtml.replace(regex, value);
} }
}); });
} }
const response = await $insertapi( const response = await $insertapi(
'sendemail', "sendemail",
{ {
to: paymentScheduleData[i].txn_detail__transaction__customer__email, to: paymentScheduleData[i].txn_detail__transaction__customer__email,
content: finalEmailHtml, content: finalEmailHtml,
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || 'Thông báo từ Utopia Villas & Resort', subject: replaceTemplateVars(subject, paymentScheduleData[i]) || "Thông báo từ Utopia Villas & Resort",
}, },
undefined, undefined,
false, false,
); );
if (response !== null) { if (response !== null) {
await $insertapi('productnote', { await $insertapi(
ref: paymentScheduleData[i].txn_detail__transaction__product, "productnote",
user: $store.login.id, {
detail: `Đã gửi email thông báo nhắc nợ cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format('HH:mm ngày DD/MM/YYYY')}.` ref: paymentScheduleData[i].txn_detail__transaction__product,
}, undefined, false); user: $store.login.id,
detail: `Đã gửi email thông báo nhắc nợ cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format("HH:mm ngày DD/MM/YYYY")}.`,
},
undefined,
false,
);
} }
}) });
setTimeout(() => { setTimeout(() => {
$snackbar('Thông báo đã được gửi thành công đến các khách hàng.'); $snackbar("Thông báo đã được gửi thành công đến các khách hàng.");
isSending.value = false; isSending.value = false;
}, 1000); }, 1000);
} }
watch(filter, () => { watch(
key.value += 1; filter,
}, { deep: true }) () => {
key.value += 1;
},
{ deep: true },
);
</script> </script>
<template> <template>
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4"> <div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
<div class="buttons m-0"> <div class="buttons m-0">
<p>Đến hạn:</p> <p>Đến hạn:</p>
<button <button
v-for="payable in payables" v-for="payable in payables"
:key="payable.id" :key="payable.id"
@click="setDateFilter(payable.detail)" @click="setDateFilter(payable.detail)"
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]" :class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
> >
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày {{ payable.detail.lookup === "lte" ? "≤" : ">" }}
{{ payable.detail.time }} ngày
</button> </button>
<button <button
v-if="activeDateFilter" v-if="activeDateFilter"
@click="resetDateFilter()" @click="resetDateFilter()"
class="button is-white" class="button is-white"
@@ -324,7 +322,7 @@ watch(filter, () => {
Xoá lọc Xoá lọc
</button> </button>
</div> </div>
<button <button
v-if="activeDateFilter" v-if="activeDateFilter"
@click="openConfirmModal()" @click="openConfirmModal()"
:class="['button', 'is-light', { 'is-loading': isSending }]" :class="['button', 'is-light', { 'is-loading': isSending }]"
@@ -342,16 +340,18 @@ watch(filter, () => {
params: { params: {
filter, filter,
sort: 'to_date', sort: 'to_date',
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry', values:
} 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
}" /> },
<Modal }"
v-if="showmodal"
v-bind="showmodal"
@confirm="sendEmails()"
@close="showmodal = undefined"
/> />
<!-- <div class="is-flex is-gap-1"> <Modal
v-if="showmodal"
v-bind="showmodal"
@confirm="sendEmails()"
@close="showmodal = undefined"
/>
<!-- <div class="is-flex is-gap-1">
// debug // debug
<Template1 <Template1
v-if="contents" v-if="contents"
@@ -360,4 +360,4 @@ watch(filter, () => {
previewMode previewMode
/> />
</div> --> </div> -->
</template> </template>

View File

@@ -1,9 +1,9 @@
<script setup> <script setup>
import Template1 from '@/lib/email/templates/Template1.vue'; import Template1 from "@/lib/email/templates/Template1.vue";
import { render } from '@vue-email/render'; import { render } from "@vue-email/render";
import { forEachAsync, isEqual } from 'es-toolkit'; import { forEachAsync, isEqual } from "es-toolkit";
const { const {
$dayjs, $dayjs,
$getdata, $getdata,
$insertapi, $insertapi,
@@ -19,8 +19,8 @@ const {
const payables = ref(null); const payables = ref(null);
const defaultFilter = { const defaultFilter = {
status: 1, status: 1,
to_date__lt: $dayjs().format('YYYY-MM-DD'), to_date__lt: $dayjs().format("YYYY-MM-DD"),
} };
const filter = ref(defaultFilter); const filter = ref(defaultFilter);
const activeDateFilter = ref(null); const activeDateFilter = ref(null);
const key = ref(0); const key = ref(0);
@@ -34,36 +34,42 @@ function resetDateFilter() {
} }
onMounted(async () => { onMounted(async () => {
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'overduepayables' }, sort: 'index' }); const payablesData = await $getdata("bizsetting", undefined, {
filter: { classify: "overduepayables" },
sort: "index",
});
payables.value = payablesData; payables.value = payablesData;
}); });
watch(activeDateFilter, (val) => { watch(
if (!val) { activeDateFilter,
filter.value = defaultFilter; (val) => {
contents.value = null; if (!val) {
} else { filter.value = defaultFilter;
const cutoffDate = $dayjs().subtract(val.time, 'day').format('YYYY-MM-DD'); contents.value = null;
const filterField = `to_date__${val.lookup === 'lte' ? 'gt' : } else {
'lte'}`; const cutoffDate = $dayjs().subtract(val.time, "day").format("YYYY-MM-DD");
filter.value = { const filterField = `to_date__${val.lookup === "lte" ? "gt" : "lte"}`;
...defaultFilter, filter.value = {
[filterField]: cutoffDate, ...defaultFilter,
[filterField]: cutoffDate,
};
} }
} },
}, { deep: true }) { deep: true },
);
const contents = ref(null); const contents = ref(null);
const isSending = ref(false); const isSending = ref(false);
function sanitizeContentPayment(text, maxLength = 80) { function sanitizeContentPayment(text, maxLength = 80) {
if (!text) return ''; if (!text) return "";
return text return text
.normalize('NFD') // bỏ dấu tiếng Việt .normalize("NFD") // bỏ dấu tiếng Việt
.replace(/[\u0300-\u036f]/g, '') .replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ .replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
.replace(/\s+/g, ' ') .replace(/\s+/g, " ")
.trim() .trim()
.slice(0, maxLength); .slice(0, maxLength);
} }
@@ -88,19 +94,19 @@ const buildContentPayment = (data) => {
cycle, cycle,
} = data; } = data;
if (customerType.toLowerCase() === 'cn') { if (customerType.toLowerCase() === "cn") {
if (customerName.length < 14) { if (customerName.length < 14) {
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
} else { } else {
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
} }
} else { } else {
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
} }
}; };
function replaceTemplateVars(html, paymentScheduleItem) { function replaceTemplateVars(html, paymentScheduleItem) {
const { const {
txn_detail__transaction__product__trade_code, txn_detail__transaction__product__trade_code,
txn_detail__transaction__customer__code, txn_detail__transaction__customer__code,
txn_detail__transaction__customer__fullname, txn_detail__transaction__customer__fullname,
@@ -112,118 +118,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
from_date, from_date,
to_date, to_date,
remain_amount, remain_amount,
cycle cycle,
} = paymentScheduleItem; } = paymentScheduleItem;
return html return html
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '') .replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '') .replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
.replace(/\[year]/g, new Date().getFullYear() || '') .replace(/\[year]/g, new Date().getFullYear() || "")
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code) .replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code)) .replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname) .replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
.replace( .replace(
/\[customer\.name\]/g, /\[customer\.name\]/g,
`${txn_detail__transaction__customer__type__code.toLowerCase() == 'cn' ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ''}` || `${txn_detail__transaction__customer__type__code.toLowerCase() == "cn" ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ""}` ||
'', "",
)
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
.replace(
/\[customer\.legal_code\]/g,
txn_detail__transaction__customer__legal_code || '',
) )
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || "")
.replace(/\[customer\.legal_code\]/g, txn_detail__transaction__customer__legal_code || "")
.replace( .replace(
/\[customer\.contact_address\]/g, /\[customer\.contact_address\]/g,
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
txn_detail__transaction__customer__address ||
'',
) )
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '') .replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '') .replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '') .replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '') .replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
.replace( .replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
/\[payment_schedule\.amount_in_word\]/g, .replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
$numberToVietnameseCurrency(remain_amount) || '',
)
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
.replace( .replace(
/\[payment_schedule\.cycle-in-words\]/g, /\[payment_schedule\.cycle-in-words\]/g,
`${cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || `${cycle == 0 ? "đặt cọc" : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || "",
'',
) )
.replace( .replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
/\[payment_schedule\.note\]/g,
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
);
} }
function quillToEmailHtml(html) { function quillToEmailHtml(html) {
return html return (
// ALIGN html
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"') // ALIGN
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"') .replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"') .replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"') .replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
// FONT SIZE // FONT SIZE
.replace(/ql-size-small/g, '') .replace(/ql-size-small/g, "")
.replace(/ql-size-large/g, '') .replace(/ql-size-large/g, "")
.replace(/ql-size-huge/g, '') .replace(/ql-size-huge/g, "")
// REMOVE EMPTY CLASS // REMOVE EMPTY CLASS
.replace(/class=""/g, '') .replace(/class=""/g, "")
; );
} }
const showmodal = ref(null); const showmodal = ref(null);
function openConfirmModal() { function openConfirmModal() {
showmodal.value = { showmodal.value = {
component: 'dialog/Confirm', component: "dialog/Confirm",
title: 'Xác nhận', title: "Xác nhận",
width: '500px', width: "500px",
height: '100px', height: "100px",
vbind: { vbind: {
content: 'Bạn có đồng ý gửi thông báo hàng loạt không?', content: "Bạn có đồng ý gửi thông báo hàng loạt không?",
} },
} };
} }
async function sendEmails() { async function sendEmails() {
isSending.value = true; isSending.value = true;
$snackbar('Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...'); $snackbar("Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...");
const paymentScheduleData = await $getdata( const paymentScheduleData = await $getdata("payment_schedule", undefined, {
'payment_schedule', filter: filter.value,
undefined, sort: "to_date",
{ values:
filter: filter.value, "penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry",
sort: 'to_date', });
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
}
);
const emailTemplate = await $getdata( const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
'emailtemplate',
{ id: activeDateFilter.value.emailTemplate },
undefined,
true
);
let message = emailTemplate.content.content; let message = emailTemplate.content.content;
contents.value = paymentScheduleData.map(paymentSchedule => { contents.value = paymentScheduleData.map((paymentSchedule) => {
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, ''); message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
const transfer = { const transfer = {
bank: { bank: {
code: 'MB', code: "MB",
name: 'MB Bank', name: "MB Bank",
}, },
account: { account: {
number: '146768686868', number: "146768686868",
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA', name: "CONG TY CO PHAN BAT DONG SAN UTOPIA",
}, },
content: 'Thanh toán đơn #xyz', content: "Thanh toán đơn #xyz",
}; };
transfer.content = buildContentPayment(paymentSchedule); transfer.content = buildContentPayment(paymentSchedule);
@@ -242,7 +229,7 @@ async function sendEmails() {
...emailTemplate.content, ...emailTemplate.content,
content: undefined, content: undefined,
message: replaceTemplateVars(message, paymentSchedule), message: replaceTemplateVars(message, paymentSchedule),
} };
}); });
await forEachAsync(contents.value, async (bigContent, i) => { await forEachAsync(contents.value, async (bigContent, i) => {
@@ -251,16 +238,16 @@ async function sendEmails() {
content: toRaw(bigContent), content: toRaw(bigContent),
previewMode: true, previewMode: true,
}; };
// ===== QUILL → HTML EMAIL (INLINE STYLE) ===== // ===== QUILL → HTML EMAIL (INLINE STYLE) =====
tempEm.content.message = quillToEmailHtml(message); tempEm.content.message = quillToEmailHtml(message);
let emailHtml = await render(Template1, tempEm); let emailHtml = await render(Template1, tempEm);
// If no image URL provided, remove image section from HTML // If no image URL provided, remove image section from HTML
if ((imageUrl ?? '').trim() === '') { if ((imageUrl ?? "").trim() === "") {
emailHtml = emailHtml emailHtml = emailHtml
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '') .replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
.replace(/\n\s*\n\s*\n/g, '\n\n'); .replace(/\n\s*\n\s*\n/g, "\n\n");
} }
// Replace keywords in HTML // Replace keywords in HTML
@@ -268,55 +255,65 @@ async function sendEmails() {
if (keyword && keyword.length > 0) { if (keyword && keyword.length > 0) {
keyword.forEach(({ keyword, value }) => { keyword.forEach(({ keyword, value }) => {
if (keyword && value) { if (keyword && value) {
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g'); const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
finalEmailHtml = finalEmailHtml.replace(regex, value); finalEmailHtml = finalEmailHtml.replace(regex, value);
} }
}); });
} }
const response = await $insertapi( const response = await $insertapi(
'sendemail', "sendemail",
{ {
to: paymentScheduleData[i].txn_detail__transaction__customer__email, to: paymentScheduleData[i].txn_detail__transaction__customer__email,
content: finalEmailHtml, content: finalEmailHtml,
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || 'Thông báo từ Utopia Villas & Resort', subject: replaceTemplateVars(subject, paymentScheduleData[i]) || "Thông báo từ Utopia Villas & Resort",
}, },
undefined, undefined,
false, false,
); );
if (response !== null) { if (response !== null) {
await $insertapi('productnote', { await $insertapi(
ref: paymentScheduleData[i].txn_detail__transaction__product, "productnote",
user: $store.login.id, {
detail: `Đã gửi email thông báo quá hạn cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format('HH:mm ngày DD/MM/YYYY')}.` ref: paymentScheduleData[i].txn_detail__transaction__product,
}, undefined, false); user: $store.login.id,
detail: `Đã gửi email thông báo quá hạn cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format("HH:mm ngày DD/MM/YYYY")}.`,
},
undefined,
false,
);
} }
}) });
setTimeout(() => { setTimeout(() => {
$snackbar('Thông báo đã được gửi thành công đến các khách hàng.'); $snackbar("Thông báo đã được gửi thành công đến các khách hàng.");
isSending.value = false; isSending.value = false;
}, 1000); }, 1000);
} }
watch(filter, () => { watch(
key.value += 1; filter,
}, { deep: true }) () => {
key.value += 1;
},
{ deep: true },
);
</script> </script>
<template> <template>
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4"> <div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
<div class="buttons m-0"> <div class="buttons m-0">
<p>Quá hạn:</p> <p>Quá hạn:</p>
<button <button
v-for="payable in payables" v-for="payable in payables"
:key="payable.id" :key="payable.id"
@click="setDateFilter(payable.detail)" @click="setDateFilter(payable.detail)"
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]" :class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
> >
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày {{ payable.detail.lookup === "lte" ? "≤" : ">" }}
{{ payable.detail.time }} ngày
</button> </button>
<button <button
v-if="activeDateFilter" v-if="activeDateFilter"
@click="resetDateFilter()" @click="resetDateFilter()"
class="button is-white" class="button is-white"
@@ -324,7 +321,7 @@ watch(filter, () => {
Xoá lọc Xoá lọc
</button> </button>
</div> </div>
<button <button
v-if="activeDateFilter" v-if="activeDateFilter"
@click="openConfirmModal()" @click="openConfirmModal()"
:class="['button', 'is-light', { 'is-loading': isSending }]" :class="['button', 'is-light', { 'is-loading': isSending }]"
@@ -342,16 +339,18 @@ watch(filter, () => {
params: { params: {
filter, filter,
sort: 'to_date', sort: 'to_date',
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry', values:
} 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
}" /> },
<Modal }"
v-if="showmodal"
v-bind="showmodal"
@confirm="sendEmails()"
@close="showmodal = undefined"
/> />
<!-- <div class="is-flex is-gap-1"> <Modal
v-if="showmodal"
v-bind="showmodal"
@confirm="sendEmails()"
@close="showmodal = undefined"
/>
<!-- <div class="is-flex is-gap-1">
// debug // debug
<Template1 <Template1
v-if="contents" v-if="contents"
@@ -360,4 +359,4 @@ watch(filter, () => {
previewMode previewMode
/> />
</div> --> </div> -->
</template> </template>

Some files were not shown because too many files have changed in this diff Show More