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 {
max-width: 1900px !important;
padding: 1rem 2rem 2rem;
@include mobile { padding: 1rem; }
@include mobile {
padding: 1rem;
}
.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
// .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;
// }
// }

View File

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

View File

@@ -1,14 +1,21 @@
<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>
<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>
<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>
</template>
<script setup>
var props = defineProps({
type: String,
size: Number,
title: String
})
</script>
var props = defineProps({
type: String,
size: Number,
title: String,
});
</script>

View File

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

View File

@@ -1,49 +1,110 @@
<template>
<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="`dropdown ${ pos || ''} ${focused? 'is-active' : ''}`" style="width: 100%;">
<div class="dropdown-trigger" style="width: 100%;">
<input
:disabled="disabled"
:class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-dark' : ''}`"
type="text"
@focus="setFocus"
@blur="lostFocus"
@keyup.enter="pressEnter"
@keyup="beginSearch"
<div
:class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`"
style="width: 100%"
>
<div
class="dropdown-trigger"
style="width: 100%"
>
<input
: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"
:placeholder="placeholder"
/>
</div>
<div class="dropdown-menu" style="min-width: 100%" 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 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
class="dropdown-menu"
style="min-width: 100%"
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>
<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>
</div>
<div class="control" 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>
<div
class="control"
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>
</div>
<div class="control" 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>
<div
class="control"
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>
</div>
<div class="control" 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>
<div
class="control"
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>
</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>
</template>
@@ -51,7 +112,22 @@
import { useStore } from "@/stores/index";
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() {
const store = useStore();
return { store };
@@ -64,7 +140,7 @@ export default {
value: undefined,
selected: undefined,
showmodal: undefined,
params: this.api? this.$findapi(this.api)['params'] : undefined,
params: this.api ? this.$findapi(this.api)["params"] : undefined,
orgdata: undefined,
error: false,
focused: false,
@@ -72,142 +148,142 @@ export default {
count2: 0,
docid: this.$id(),
pos: undefined,
}
};
},
computed: {
isVietnamese() {
return this.store.lang === "vi";
}
},
},
async created() {
this.getPos()
if(this.vdata) {
this.orgdata = this.$copy(this.vdata)
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field]))
this.getPos();
if (this.vdata) {
this.orgdata = this.$copy(this.vdata);
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
}
if(this.first) {
this.data = this.orgdata? this.$copy(this.orgdata) : await this.getData()
if(this.optionid) {
let f = {}
f[this.field] = this.optionid
if(this.optionid>0) f = {id: this.optionid}
this.selected = this.$find(this.data, f)
if(this.selected && this.vdata) {
return this.value = this.selected[this.field]
if (this.first) {
this.data = this.orgdata ? this.$copy(this.orgdata) : await this.getData();
if (this.optionid) {
let f = {};
f[this.field] = this.optionid;
if (this.optionid > 0) f = { id: this.optionid };
this.selected = this.$find(this.data, f);
if (this.selected && this.vdata) {
return (this.value = this.selected[this.field]);
}
}
} else if(this.optionid) {
this.selected = await this.$getdata(this.api, {id: this.optionid}, undefined, true)
} else if (this.optionid) {
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: {
optionid: function(newVal) {
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid})
if(this.selected) this.doSelect(this.selected)
else this.value = undefined
optionid: function (newVal) {
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
if (this.selected) this.doSelect(this.selected);
else this.value = undefined;
},
filter: async function(newVal) {
this.data = await this.getData()
filter: async function (newVal) {
this.data = await this.getData();
},
vdata: function(newval) {
if(newval) {
this.orgdata = this.$copy(this.vdata)
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field]))
this.data = this.$copy(this.orgdata)
this.selected = undefined
this.value = undefined
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid})
if(this.selected) this.doSelect(this.selected)
vdata: function (newval) {
if (newval) {
this.orgdata = this.$copy(this.vdata);
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
this.data = this.$copy(this.orgdata);
this.selected = undefined;
this.value = undefined;
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
if (this.selected) this.doSelect(this.selected);
}
}
},
},
methods: {
choose(v) {
this.focused = false
this.count1 = 0
this.count2 = 0
this.doSelect(v)
this.focused = false;
this.count1 = 0;
this.count2 = 0;
this.doSelect(v);
},
setFocus() {
this.focused = true
this.count1 = 0
this.count2 = 0
this.focused = true;
this.count1 = 0;
this.count2 = 0;
},
lostFocus() {
let self = this
setTimeout(()=>{
if(self.focused && self.count1===0) self.focused = false
}, 200)
let self = this;
setTimeout(() => {
if (self.focused && self.count1 === 0) self.focused = false;
}, 200);
},
pressEnter() {
if(this.data.length===0) return
this.choose(this.data[0])
if (this.data.length === 0) return;
this.choose(this.data[0]);
},
doClick() {
this.count1 += 1
this.count1 += 1;
},
doSelect(option) {
if(this.$empty(option)) return
this.$emit('option', option)
this.$emit('modalevent', {name: 'option', data: option})
this.selected = option
this.value = this.selected[this.field]
if (this.$empty(option)) return;
this.$emit("option", option);
this.$emit("modalevent", { name: "option", data: option });
this.selected = option;
this.value = this.selected[this.field];
},
clearValue() {
this.value = undefined
this.selected = undefined
this.$emit('option', null)
this.$emit('modalevent', {name: 'option', data: null})
this.value = undefined;
this.selected = undefined;
this.$emit("option", null);
this.$emit("modalevent", { name: "option", data: null });
},
findObject(val) {
let rows = this.$copy(this.orgdata)
if(this.$empty(val)) this.data = rows
let rows = this.$copy(this.orgdata);
if (this.$empty(val)) this.data = rows;
else {
let text = this.$nonAccent(val)
this.data = rows.filter(v=>v.search.toLowerCase().indexOf(text.toLowerCase())>=0)
let text = this.$nonAccent(val);
this.data = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
}
},
async getData() {
this.params.filter = this.filter
let data = await this.$getdata(this.api, undefined, this.params)
return data
this.params.filter = this.filter;
let data = await this.$getdata(this.api, undefined, this.params);
return data;
},
async getApi(val) {
if(this.vdata) return this.findObject(val)
let text = val? val.toLowerCase() : ''
let f = {}
if (this.vdata) return this.findObject(val);
let text = val ? val.toLowerCase() : "";
let f = {};
// Sử dụng searchfield nếu có, nếu không thì dùng column
const fieldsToSearch = this.searchfield || this.column;
fieldsToSearch.map(v=>{
f[`${v}__icontains`] = text
})
this.params.filter_or = f
if(this.filter) this.params.filter = this.$copy(this.filter)
let arr = await this.$getdata(this.api, undefined, this.params)
this.data = this.$copy(arr)
fieldsToSearch.map((v) => {
f[`${v}__icontains`] = text;
});
this.params.filter_or = f;
if (this.filter) this.params.filter = this.$copy(this.filter);
let arr = await this.$getdata(this.api, undefined, this.params);
this.data = this.$copy(arr);
},
beginSearch(e) {
let val = e.target.value
this.search = val
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => this.getApi(val), 150)
let val = e.target.value;
this.search = val;
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.getApi(val), 150);
},
addNew() {
this.showmodal = this.$copy(this.addon)
this.showmodal = this.$copy(this.addon);
},
dataevent(v) {
console.log("SearchBox received dataevent:", v); // Debug log
if (!v || !v.id) {
console.error("Invalid data received in SearchBox:", v);
return;
}
// 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) {
// Nếu chưa có trong danh sách, thêm vào đầu
this.data.unshift(v);
@@ -217,10 +293,10 @@ export default {
this.data[idx] = v;
console.log("Updated existing item in data:", v);
}
// Cập nhật orgdata nếu có
if (this.orgdata) {
let orgIdx = this.$findIndex(this.orgdata, {id: v.id});
let orgIdx = this.$findIndex(this.orgdata, { id: v.id });
if (orgIdx < 0) {
this.orgdata.unshift(v);
// Thêm search field cho orgdata
@@ -234,42 +310,46 @@ export default {
}
}
}
// **Tự động select item vừa tạo/cập nhật**
this.doSelect(v);
// Đóng modal
this.showmodal = undefined;
console.log("SearchBox data after update:", this.data);
},
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')
let copy = this.$copy(this.viewaddon)
copy.vbind = {row: this.selected}
this.showmodal = copy
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",
);
let copy = this.$copy(this.viewaddon);
copy.vbind = { row: this.selected };
this.showmodal = copy;
},
getPos() {
switch(this.position) {
case 'is-top-left':
this.pos = 'is-up is-left'
switch (this.position) {
case "is-top-left":
this.pos = "is-up is-left";
break;
case 'is-top-right':
this.pos = 'is-up is-right'
case "is-top-right":
this.pos = "is-up is-right";
break;
case 'is-bottom-left':
this.pos = 'is-right'
case "is-bottom-left":
this.pos = "is-right";
break;
case 'is-bottom-right':
this.pos = 'is-right'
case "is-bottom-right":
this.pos = "is-right";
break;
}
}
}
}
},
},
};
</script>
<style scoped>
.field:not(:last-child) {
margin-bottom: 0;
}
</style>
</style>

View File

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

View File

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

View File

@@ -1,17 +1,31 @@
<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">
<span class="navbar-item is-gap-1">
<div style="width: 16px; height: 16px" class="has-background-primary rounded-full"></div>
<span class="fs-17 font-semibold has-text-primary">{{ $dayjs().format('DD/MM') }}</span>
</span>
<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>
<a
class="navbar-item p-0 has-text-primary"
@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">
<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>
<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"
>
<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
role="button"
@@ -28,30 +42,45 @@
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navMenu">
<div class="navbar-start is-gap-1 is-align-items-center" >
<template v-for="(v, i) in leftmenu" :key="i" :id="v.code">
<a class="navbar-item rounded-lg is-clipped p-0" v-if="!v.submenu" @click="changeTab(v)">
<span :class="[
'px-2 py-2 font-medium',
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
<div
class="navbar-menu"
id="navMenu"
>
<div class="navbar-start is-gap-1 is-align-items-center">
<template
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"
>
{{ v[lang] }}
</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"
>
<p class="is-flex is-align-items-center">
<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>
</a>
<div class="navbar-dropdown">
@@ -76,19 +105,23 @@
</a>
</div> -->
<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>
<p class="fs-13">Xin chào,</p>
<p class="fs-14 font-bold">Quản </p>
</div>
<Avatarbox text="Q" type="findata" size="two" />
<Avatarbox
text="Q"
type="findata"
size="two"
/>
</a>
</div>
</div>
</nav>
</template>
<script setup>
import Avatarbox from '@/components/common/Avatarbox.vue';
import Avatarbox from "@/components/common/Avatarbox.vue";
import { watch } from "vue";
const router = useRouter();
const route = useRoute();
@@ -99,229 +132,229 @@ const lang = ref($store.lang);
const menu = [
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'dashboard',
vi: 'Dashboard',
category: "topmenu",
classify: "left",
code: "dashboard",
vi: "Dashboard",
link: null,
detail: {
base: 'Dashboard',
component: 'DashboardMaster',
base: "Dashboard",
component: "DashboardMaster",
},
index: 0,
},
{
id: 2,
category: 'topmenu',
classify: 'left',
code: 'orders',
vi: 'Đơn hàng',
category: "topmenu",
classify: "left",
code: "orders",
vi: "Đơn hàng",
link: null,
detail: {
base: 'Orders',
component: 'OrdersMaster',
base: "Orders",
component: "OrdersMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'inventory',
vi: 'Tồn kho',
category: "topmenu",
classify: "left",
code: "inventory",
vi: "Tồn kho",
link: null,
detail: {
base: 'Inventory',
component: 'InventoryMaster',
base: "Inventory",
component: "InventoryMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'rights',
vi: 'Phân quyền',
category: "topmenu",
classify: "left",
code: "rights",
vi: "Phân quyền",
link: null,
detail: {
base: 'Rights',
component: 'RightsMaster',
base: "Rights",
component: "RightsMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'POS',
vi: 'POS',
category: "topmenu",
classify: "left",
code: "POS",
vi: "POS",
link: null,
detail: {
base: 'POS',
component: 'POSMaster',
base: "POS",
component: "POSMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'receipts',
vi: 'Hoá đơn',
category: "topmenu",
classify: "left",
code: "receipts",
vi: "Hoá đơn",
link: null,
detail: {
base: 'Receipts',
component: 'ReceiptsMaster',
base: "Receipts",
component: "ReceiptsMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'imports',
vi: 'Nhập hàng',
category: "topmenu",
classify: "left",
code: "imports",
vi: "Nhập hàng",
link: null,
detail: {
base: 'Imports',
component: 'ImportsMaster',
base: "Imports",
component: "ImportsMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'exports',
vi: 'Xuất hàng',
category: "topmenu",
classify: "left",
code: "exports",
vi: "Xuất hàng",
link: null,
detail: {
base: 'Exports',
component: 'ExportsMaster',
base: "Exports",
component: "ExportsMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'inventory-transfer',
vi: 'Chuyển kho',
category: "topmenu",
classify: "left",
code: "inventory-transfer",
vi: "Chuyển kho",
link: null,
detail: {
base: 'InventoryTransfer',
component: 'InventoryTransferMaster',
base: "InventoryTransfer",
component: "InventoryTransferMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'inventory-count',
vi: 'Kiểm kho',
category: "topmenu",
classify: "left",
code: "inventory-count",
vi: "Kiểm kho",
link: null,
detail: {
base: 'InventoryCount',
component: 'InventoryCountMaster',
base: "InventoryCount",
component: "InventoryCountMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'cash-book',
vi: 'Sổ quỹ',
category: "topmenu",
classify: "left",
code: "cash-book",
vi: "Sổ quỹ",
link: null,
detail: {
base: 'CashBook',
component: 'CashBookMaster',
base: "CashBook",
component: "CashBookMaster",
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'report',
vi: 'Báo cáo',
category: "topmenu",
classify: "left",
code: "report",
vi: "Báo cáo",
link: null,
submenu: [
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'ncc',
vi: 'NCC',
category: "submenu",
classify: "report",
code: "ncc",
vi: "NCC",
link: null,
detail: {
base: 'NCC',
component: 'NCCMaster',
base: "NCC",
component: "NCCMaster",
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'customers',
vi: 'Khách hàng',
category: "submenu",
classify: "report",
code: "customers",
vi: "Khách hàng",
link: null,
detail: {
base: 'Customers',
component: 'CustomersMaster',
base: "Customers",
component: "CustomersMaster",
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'goods',
vi: 'Hàng hoá',
category: "submenu",
classify: "report",
code: "goods",
vi: "Hàng hoá",
link: null,
detail: {
base: 'Goods',
component: 'GoodsMaster',
base: "Goods",
component: "GoodsMaster",
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'report-cash-book',
vi: 'Sổ quỹ',
category: "submenu",
classify: "report",
code: "report-cash-book",
vi: "Sổ quỹ",
link: null,
detail: {
base: 'ReportCashBook',
component: 'ReportCashBookMaster',
base: "ReportCashBook",
component: "ReportCashBookMaster",
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'finance',
vi: 'Tài chính',
category: "submenu",
classify: "report",
code: "finance",
vi: "Tài chính",
link: null,
detail: {
base: 'Finance',
component: 'FinanceMaster',
base: "Finance",
component: "FinanceMaster",
},
index: 0,
},
],
index: 0,
},
]
];
// if($store.rights.length>0) {
// menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
// }
if(menu.length===0) {
$snackbar($store.lang==='vi'? 'Bạn không có quyền truy cập' : 'You do not have permission to access.')
if (menu.length === 0) {
$snackbar($store.lang === "vi" ? "Bạn không có quyền truy cập" : "You do not have permission to access.");
}
// menu.map(v=>{
// let arr = $filter($store.common, {category: 'submenu', classify: v.code})
@@ -330,8 +363,8 @@ if(menu.length===0) {
// }
// v.submenu = arr.length>0? arr : null
// })
var leftmenu = $filter(menu, {category: 'topmenu', classify: 'left'})
var currentTab = ref(leftmenu.length>0? leftmenu[0] : undefined)
var leftmenu = $filter(menu, { category: "topmenu", classify: "left" });
var currentTab = ref(leftmenu.length > 0 ? leftmenu[0] : undefined);
var subTab = ref();
var tabConfig = $find(menu, { code: "configuration" });
var avatar = ref();
@@ -364,11 +397,16 @@ function changeTab(tab, subtab) {
router.push({ query: query });
}
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);
}
let found = route.query.tab? $find(menu, {code: route.query.tab}) : undefined
if(found || currentTab.value) changeTab(found || currentTab.value)
let found = route.query.tab ? $find(menu, { code: route.query.tab }) : undefined;
if (found || currentTab.value) changeTab(found || currentTab.value);
onMounted(() => {
if (!$store.login) return;
avatar.value = {
@@ -391,7 +429,7 @@ watch(
};
isAdmin.value = $store.login.type__code === "admin";
lang.value = $store.lang;
}
},
);
</script>
<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-link {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
> p {
> p {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.navbar-item > .navbar-link:after {
display: none;
}
</style>
</style>

View File

@@ -1,48 +1,51 @@
<template>
<div v-if="record">
<div class="columns is-multiline mx-0 mt-1" id="printable">
<div class="column is-5">
<div class="field">
<label class="label">{{ $lang('code') }}:</label>
<div class="control">
{{ `${record.code}` }}
<div v-if="record">
<div
class="columns is-multiline mx-0 mt-1"
id="printable"
>
<div class="column is-5">
<div class="field">
<label class="label">{{ $lang("code") }}:</label>
<div class="control">
{{ `${record.code}` }}
</div>
</div>
</div>
</div>
<div class="column is-7">
<div class="field">
<label class="label">{{ $lang('account-type') }}:</label>
<div class="control">
{{ `${record.type__code} / ${record.type__name}` }}
<div class="column is-7">
<div class="field">
<label class="label">{{ $lang("account-type") }}:</label>
<div class="control">
{{ `${record.type__code} / ${record.type__name}` }}
</div>
</div>
</div>
</div>
<div class="column is-5">
<div class="field">
<label class="label">{{ $lang('currency') }}:</label>
<div class="control">
{{ `${record.currency__code} / ${record.currency__name}` }}
<div class="column is-5">
<div class="field">
<label class="label">{{ $lang("currency") }}:</label>
<div class="control">
{{ `${record.currency__code} / ${record.currency__name}` }}
</div>
</div>
</div>
</div>
<div class="column is-7">
<div class="field">
<label class="label">{{ $lang('balance') }}:</label>
<div class="control">
{{ $numtoString(record.balance) }}
</div>
<!--<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 class="column is-7">
<div class="field">
<label class="label">{{ $lang("balance") }}:</label>
<div class="control">
{{ $numtoString(record.balance) }}
</div>
<!--<p class="help is-findata">{{$vnmoney($formatNumber(record.balance))}}</p>-->
</div>
</div>
</div>
<!--<div class="column is-7">
<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 class="column is-7">
<div class="field">
<label class="label">Chi nhánh:</label>
<div class="control">
@@ -50,30 +53,38 @@
</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 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>
<script>
export default {
props: ['row'],
data() {
return {
errors: {},
record: undefined
}
export default {
props: ["row"],
data() {
return {
errors: {},
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)
},
methods: {
selected(attr, obj) {
this.record[attr] = obj
if(attr==='_type') this.category = obj.category__code
}
}
}
</script>
},
};
</script>

View File

@@ -1,50 +1,60 @@
<!-- components/dialog/ConfirmDeleteEntry.vue -->
<template>
<div class="has-text-centered">
<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="mt-3 has-text-danger has-text-weight-semibold">
Hành động này <strong>không thể hoàn tác</strong>.<br>
<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="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.
</p>
</div>
<div class="field is-grouped is-justify-content-center">
<!-- 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">
<p class="control">
<input
class="input"
type="text"
placeholder="Nhập mã xác nhận"
<input
class="input"
type="text"
placeholder="Nhập mã xác nhận"
v-model="userInputCaptcha"
@keyup.enter="isConfirmed && confirmDelete()"
>
/>
</p>
<p class="control">
<a class="button is-static has-text-weight-bold has-background-grey-lighter"
style="font-family: 'Courier New', monospace; letter-spacing: 2px;">
<a
class="button is-static has-text-weight-bold has-background-grey-lighter"
style="font-family: &quot;Courier New&quot;, monospace; letter-spacing: 2px"
>
{{ captchaCode }}
</a>
</p>
<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">
<SvgIcon name="refresh.svg" type="primary" :size="23" />
<SvgIcon
name="refresh.svg"
type="primary"
:size="23"
></SvgIcon>
</span>
</button>
</p>
</div>
</p>
</div>
<!-- Action buttons -->
<!-- Confirm button - shown only when captcha IS confirmed -->
<p class="control" v-if="isConfirmed">
<p
class="control"
v-if="isConfirmed"
>
<button
class="button is-danger"
:class="{ 'is-loading': isDeleting }"
@@ -69,75 +79,70 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { useNuxtApp } from '#app'
import { ref, computed } from "vue";
import { useNuxtApp } from "#app";
const props = defineProps({
entryId: {
type: [String, Number],
required: true
}
})
required: true,
},
});
const emit = defineEmits(['close', 'deleted'])
const emit = defineEmits(["close", "deleted"]);
const { $snackbar ,$insertapi} = useNuxtApp()
const isDeleting = ref(false)
const captchaCode = ref('')
const userInputCaptcha = ref('')
const { $snackbar, $insertapi } = useNuxtApp();
const isDeleting = ref(false);
const captchaCode = ref("");
const userInputCaptcha = ref("");
const isConfirmed = computed(() => {
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== ''
})
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== "";
});
const generateCaptcha = () => {
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase()
userInputCaptcha.value = ''
}
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase();
userInputCaptcha.value = "";
};
// Initial generation
generateCaptcha()
generateCaptcha();
const confirmDelete = async () => {
if (isDeleting.value || !isConfirmed.value) return
isDeleting.value = true
if (isDeleting.value || !isConfirmed.value) return;
isDeleting.value = true;
try {
// 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) {
throw new Error('API xóa trả về lỗi')
if (result === "error" || !result) {
throw new Error("API xóa trả về lỗi");
}
$snackbar(
`Đã xóa bút toán ID ${props.entryId} thành công`,
'Thành công',
'Success'
)
emit('deleted', props.entryId)
emit('close')
$snackbar(`Đã xóa bút toán ID ${props.entryId} thành công`, "Thành công", "Success");
emit("deleted", props.entryId);
emit("close");
} 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ể
if (err?.response?.data?.detail) {
errorMsg = err.response.data.detail
errorMsg = err.response.data.detail;
} 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 {
isDeleting.value = false
isDeleting.value = false;
}
}
};
const cancel = () => {
emit('close')
}
emit("close");
};
</script>

View File

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

View File

@@ -9,7 +9,12 @@
@date="selected('fdate', $event)"
></Datepicker>
</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 class="column is-3">
@@ -21,11 +26,19 @@
@date="selected('tdate', $event)"
></Datepicker>
</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>
<DataView v-bind="vbind" v-if="vbind" />
<DataView
v-bind="vbind"
v-if="vbind"
/>
</template>
<script>
export default {
@@ -60,13 +73,21 @@ export default {
values:
"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: {
sumCR: { type: "Sum", filter: { type__code: "CR" }, field: "amount" },
sumDR: { type: "Sum", filter: { type__code: "DR" }, field: "amount" },
sumCR: {
type: "Sum",
filter: { type__code: "CR" },
field: "amount",
},
sumDR: {
type: "Sum",
filter: { type__code: "DR" },
field: "amount",
},
},
summary: "annotate",
filter: { date__gte: this.fdate, date__lte: this.tdate },
},
})
}),
);
},
},

View File

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

View File

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

View File

@@ -1,176 +1,288 @@
<template>
<div>
<div class="columns is-multiline mx-0">
<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>
<div class="columns is-multiline mx-0">
<div class="column is-8">
<div class="field">
<label class="label"
>Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label
<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">
<label class="label">Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<Datepicker
v-bind="{ record: record, attr: 'date', maxdate: new Date()}"
v-bind="{ record: record, attr: 'date', maxdate: new Date() }"
@date="selected('date', $event)"
></Datepicker>
</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 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>
<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">
<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">
<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 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>
<script>
import { useStore } from '~/stores/index'
import { useStore } from "~/stores/index";
export default {
setup() {
const store = useStore()
return {store}
const store = useStore();
return { store };
},
props: ['pagename', 'row', 'option'],
props: ["pagename", "row", "option"],
data() {
return {
record: {date: this.$dayjs().format('YYYY-MM-DD')},
record: { date: this.$dayjs().format("YYYY-MM-DD") },
errors: {},
isUpdating: false,
showmodal: undefined,
showContractModal: undefined,
entry: undefined
}
entry: undefined,
};
},
created() {
if(!this.option) return
this.record.account = this.option.account
this.record.type = this.option.type
if (!this.option) return;
this.record.account = this.option.account;
this.record.type = this.option.type;
},
methods: {
selected(attr, obj) {
this.record[attr] = obj
this.record = this.$copy(this.record)
this.record[attr] = obj;
this.record = this.$copy(this.record);
},
checkError() {
this.errors = {}
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.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'
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(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 = {};
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.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";
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 (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ợ";
}
return Object.keys(this.errors).length>0
return Object.keys(this.errors).length > 0;
},
confirm() {
if(this.checkError()) return
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10},
title: this.$lang('confirm'), width: '500px', height: '100px'}
if (this.checkError()) return;
this.showmodal = {
component: `dialog/Confirm`,
vbind: { content: this.$lang("confirm-action"), duration: 10 },
title: this.$lang("confirm"),
width: "500px",
height: "100px",
};
},
async update() {
this.isUpdating = true;
@@ -179,28 +291,29 @@ export default {
amount: this.record.amount,
content: this.record.content,
type: this.record._type.code,
category: this.record._category ? this.record._category.id : 1, user: this.store.login.id,
ref: this.row ? this.row.code : (!this.$empty(this.record.ref) ? this.record.ref.trim() : null),
category: this.record._category ? this.record._category.id : 1,
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,
product: this.record.product ? this.record.product.id : null,
date: this.$empty(this.record.date) ? null : this.record.date
}
let rs1 = await this.$insertapi('accountentry', obj1, undefined, false)
if(rs1==='error') return
date: this.$empty(this.record.date) ? null : this.record.date,
};
let rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
if (rs1 === "error") return;
if (this.record._category.id === 2) {
const genDoc = await this.$generateDocument({
doc_code: 'PHIEU_THU_TIEN_MAT',
doc_code: "PHIEU_THU_TIEN_MAT",
entry_id: rs1.id,
output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}`
output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}`,
});
await this.$insertapi('file', {
name: genDoc.data.pdf,
user: this.store.login.id,
type: 4,
size: 1000,
file: genDoc.data.pdf // or genDoc.data.pdf
await this.$insertapi("file", {
name: genDoc.data.pdf,
user: this.store.login.id,
type: 4,
size: 1000,
file: genDoc.data.pdf, // or genDoc.data.pdf
});
this.showContractModal = {
@@ -209,20 +322,27 @@ export default {
width: "95%",
height: "95vh",
vbind: {
directDocument: genDoc.data
directDocument: genDoc.data,
},
}
};
}
this.entry = rs1
if(this.pagename) {
let data = await this.$getdata('internalaccount', {code__in: [this.record._account.code]})
this.$updatepage(this.pagename, data)
this.entry = rs1;
if (this.pagename) {
let data = await this.$getdata("internalaccount", {
code__in: [this.record._account.code],
});
this.$updatepage(this.pagename, data);
}
this.isUpdating = false;
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)
}
}
}
</script>
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,
);
},
},
};
</script>

View File

@@ -1,247 +1,326 @@
<template>
<div v-if="record" id="printable">
<Caption v-bind="{ title: $lang('info') }" />
<div class="columns is-multiline is-2 m-0">
<div class="column is-3">
<div class="field">
<label class="label">{{ $lang('code') }}:</label>
<div class="control">
<span>{{ record.code }}</span>
</div>
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">Tài khoản:</label>
<div class="control">
<span>{{ record.account__code }}</span>
</div>
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">Ngày hạch toán:</label>
<div class="control">
{{ $dayjs(record.date).format('DD/MM/YYYY') }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">{{ $lang('amount-only') }}:</label>
<div class="control">
{{ $numtoString(record.amount) }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">Thu / chi:</label>
<div class="control">
{{ record.type__name }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label"> trước:</label>
<div class="control">
{{ $numtoString(record.balance_before) }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label"> sau:</label>
<div class="control">
{{ $numtoString(record.balance_after) }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<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
v-if="record"
id="printable"
>
<Caption v-bind="{ title: $lang('info') }" />
<div class="columns is-multiline is-2 m-0">
<div class="column is-3">
<div class="field">
<label class="label">{{ $lang("code") }}:</label>
<div class="control">
<span>{{ record.code }}</span>
</div>
<p
class="help is-danger"
v-if="errors.type"
>
{{ errors.type }}
</p>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">Tài khoản:</label>
<div class="control">
<span>{{ record.account__code }}</span>
</div>
<p
class="help is-danger"
v-if="errors.type"
>
{{ errors.type }}
</p>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">Ngày hạch toán:</label>
<div class="control">
{{ $dayjs(record.date).format("DD/MM/YYYY") }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">{{ $lang("amount-only") }}:</label>
<div class="control">
{{ $numtoString(record.amount) }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">Thu / chi:</label>
<div class="control">
{{ record.type__name }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label"> trước:</label>
<div class="control">
{{ $numtoString(record.balance_before) }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label"> sau:</label>
<div class="control">
{{ $numtoString(record.balance_after) }}
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<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="field">
<label class="label">Người hạch toán:</label>
<div class="control">
{{ `${record.inputer__fullname}` }}
</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 class="column is-3">
<div class="field">
<label class="label">Người hạch toán:</label>
<div class="control">
{{ `${record.inputer__fullname}` }}
</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 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 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>
<div class="column is-3">
<div class="field">
<label class="label">Ref:</label>
<div class="control">
{{ `${record.ref || "/"}` }}
</div>
</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 class="column is-8">
<div class="field">
<label class="label">{{ $lang("content") }}:</label>
<div class="control">
{{ `${record.content}` }}
</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>
<script>
export default {
props: ['row'],
data() {
return {
errors: {},
record: undefined
}
props: ["row"],
data() {
return {
errors: {},
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() {
this.record = await this.$getdata('internalentry', { code: this.row.code }, undefined, true)
// 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);
},
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)
},
// 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)
}
// 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);
},
methods: {
selected(attr, obj) {
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');
}
}
}
// 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: {
selected(attr, obj) {
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>
<style scoped>
.column {
padding-inline: 0;
padding-inline: 0;
}
</style>
</style>

View File

@@ -1,104 +1,191 @@
<template>
<div>
<div class="columns is-multiline mx-0">
<div class="column is-12">
<div class="field">
<label class="label">{{$lang('source-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: row.id}"
@option="selected('_source', $event)" v-if="!record.id"></SearchBox>
<span v-else>{{record.account__code}}</span>
</div>
<p class="help is-danger" v-if="errors.source">{{ errors.source }}</p>
</div>
</div>
<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 class="column is-12">
<div class="field">
<label class="label">{{ $lang("source-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: row.id,
}"
@option="selected('_source', $event)"
v-if="!record.id"
></SearchBox>
<span v-else>{{ record.account__code }}</span>
</div>
<p
class="help is-danger"
v-if="errors.source"
>
{{ errors.source }}
</p>
</div>
</div>
<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>
<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>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
@confirm="update()"
v-if="showmodal"
></Modal>
</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>
<script>
export default {
props: ['pagename', 'row'],
props: ["pagename", "row"],
data() {
return {
record: {},
errors: {},
showmodal: undefined,
vbind: undefined
}
vbind: undefined,
};
},
methods: {
selected(attr, obj) {
this.record[attr] = obj
this.record = this.$copy(this.record)
if(attr==='_source') {
let currency = obj? obj.currency : undefined
this.vbind = undefined
setTimeout(()=>this.vbind = {api:'internalaccount', field:'label', column:['label'], first: true, filter: {currency: currency}})
this.record[attr] = obj;
this.record = this.$copy(this.record);
if (attr === "_source") {
let currency = obj ? obj.currency : undefined;
this.vbind = undefined;
setTimeout(
() =>
(this.vbind = {
api: "internalaccount",
field: "label",
column: ["label"],
first: true,
filter: { currency: currency },
}),
);
}
},
checkError() {
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._target)) this.errors.target = 'Chưa chọn tài khoản đích'
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'
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._target)) this.errors.target = "Chưa chọn tài khoản đích";
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.$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.record._source.balance<this.$formatNumber(this.record.amount)) this.errors.source = 'Tài khoản nguồn không đủ số để điều chuyển'
if(this.$empty(this.record.content)) this.errors.content = 'Chưa nhập nội dung'
return Object.keys(this.errors).length>0
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.record._source.balance < this.$formatNumber(this.record.amount))
this.errors.source = "Tài khoản nguồn không đủ số dư để điều chuyển";
if (this.$empty(this.record.content)) this.errors.content = "Chưa nhập nội dung";
return Object.keys(this.errors).length > 0;
},
confirm() {
if(this.checkError()) return
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10},
title: this.$lang('confirm'), width: '500px', height: '100px'}
if (this.checkError()) return;
this.showmodal = {
component: `dialog/Confirm`,
vbind: { content: this.$lang("confirm-action"), duration: 10 },
title: this.$lang("confirm"),
width: "500px",
height: "100px",
};
},
async update() {
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 rs1 = await this.$insertapi('accountentry', obj1, undefined, false)
if(rs1==='error') return
let obj2 = {code: this.record._target.code, amount: this.record.amount, 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>
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 rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
if (rs1 === "error") return;
let obj2 = {
code: this.record._target.code,
amount: this.record.amount,
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 class="column is-1"></div>
</div>
<div class="columns" v-for="(invoice, index) in invoices">
<div
class="columns"
v-for="(invoice, index) in invoices"
>
<div class="column">
<input
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 class="column is-2">
<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 class="column is-2">
<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 class="column is-2">
<select
@@ -84,22 +102,44 @@
<option value="principal">Tiền gốc</option>
<option value="interest">Tiền lãi</option>
</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 class="column is-narrow is-1">
<label class="label" 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)">
<label
class="label"
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">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
</button>
<button class="button is-dark" @click="add()">
<button
class="button is-dark"
@click="add()"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
</button>
<a class="button is-dark" :href="invoice.link" target="_blank">
<a
class="button is-dark"
:href="invoice.link"
target="_blank"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
@@ -108,8 +148,18 @@
</div>
</div>
<div class="mt-5 buttons is-right">
<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>
<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>
@@ -157,7 +207,11 @@ if (resInvoice.length) {
errorAmount: 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;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,22 @@
<template>
<div :id="docid">
<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="column is-4 pb-1 px-0">
<div class="field">
<label class="label">{{ dataLang && findFieldName("loan_code")[lang] }}</label>
<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>
@@ -54,7 +63,7 @@
<div class="control">
<span>{{ record?.commission ? $numtoString(record.commission) : "/" }}</span>
</div>
</div>
</div>
</div>
<div class="column is-5 pb-1 px-0">
@@ -76,14 +85,18 @@
</div>
</div>
</div>
</div>
</div>
<!-- <div class="mt-2 border-bottom"></div> -->
<div class="buttons mt-5" id="ignore">
<button class="button is-primary has-text-white mt-2" @click="handleUpdate()">
<div
class="buttons mt-5"
id="ignore"
>
<button
class="button is-primary has-text-white mt-2"
@click="handleUpdate()"
>
{{ dataLang && findFieldName("update")[lang] }}
</button>
</div>

View File

@@ -1,17 +1,28 @@
<template>
<div :id="docid">
<!-- 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>
<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>
</div>
<!-- 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">
<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
? "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 -->
<template v-else>
<!-- 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">
<li 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)">
<li
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">
<span>{{ contract.document[0]?.name || contract.document[0]?.en || `Contract ${index + 1}` }}</span>
</a>
@@ -38,23 +61,42 @@
<!-- Contract content -->
<div v-if="currentContract && pdfFileUrl && hasValidDocument">
<div class="contract-content mt-2">
<iframe :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
: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>
</div>
</div>
<!-- Download buttons -->
<div class="mt-4" id="ignore">
<button v-if="hasValidDocument" class="button is-primary has-text-white mr-4" @click="downloadDocx">
<div
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" }}
</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" }}
</button>
<p v-if="contractError" class="has-text-danger mt-2">
<p
v-if="contractError"
class="has-text-danger mt-2"
>
{{ contractError }}
</p>
</div>
@@ -73,16 +115,16 @@ export default {
props: {
contractId: {
type: [Number, String],
default: null
default: null,
},
row: {
type: Object,
default: null
default: null,
},
directDocument: {
type: Object,
default: null
}
default: null,
},
},
emits: ["contractCreated", "update", "close", "dataevent"],
data() {
@@ -107,10 +149,12 @@ export default {
},
hasValidDocument() {
if (!this.currentContract) return false;
return this.currentContract.document &&
return (
this.currentContract.document &&
this.currentContract.document.length > 0 &&
this.currentContract.document[0]?.pdf;
}
this.currentContract.document[0]?.pdf
);
},
},
async created() {
try {
@@ -118,11 +162,9 @@ export default {
this.contractError = null;
if (this.directDocument) {
this.contractsList = [
{ document: [this.directDocument] }
];
this.contractsList = [{ document: [this.directDocument] }];
this.updatePdfUrl(0);
return;
return;
}
let contracts = [];
@@ -130,31 +172,22 @@ export default {
if (this.contractId) {
fetchParams = { id: this.contractId };
}
else if (this.row?.id) {
} else if (this.row?.id) {
fetchParams = { transaction: this.row.id };
}
if (!fetchParams) {
throw new Error(
this.isVietnamese
? 'Không có ID hợp đồng hoặc transaction để tải.'
: 'No contract ID or transaction provided to load.'
? "Không có ID hợp đồng hoặc transaction để tải."
: "No contract ID or transaction provided to load.",
);
}
contracts = await this.$getdata(
'contract',
fetchParams,
undefined
);
contracts = await this.$getdata("contract", fetchParams, undefined);
if (!contracts || contracts.length === 0) {
throw new Error(
this.isVietnamese
? 'Không tìm thấy hợp đồng.'
: 'Contract not found.'
);
throw new Error(this.isVietnamese ? "Không tìm thấy hợp đồng." : "Contract not found.");
}
this.contractsList = contracts;
@@ -164,12 +197,9 @@ export default {
this.updatePdfUrl(this.activeContractIndex);
}
} catch (error) {
console.error('Error loading contracts:', error);
this.contractError = error.message || (
this.isVietnamese
? 'Lỗi khi tải danh sách hợp đồng.'
: 'Error loading contracts list.'
);
console.error("Error loading contracts:", error);
this.contractError =
error.message || (this.isVietnamese ? "Lỗi khi tải danh sách hợp đồng." : "Error loading contracts list.");
this.contractsList = [];
} finally {
this.isLoading = false;
@@ -192,10 +222,7 @@ export default {
downloadDocx() {
if (!this.hasValidDocument) {
this.$snackbar(
this.isVietnamese ? "Không có file để tải" : "No file to download",
{ type: 'is-warning' }
);
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
return;
}
@@ -206,10 +233,7 @@ export default {
downloadPdf() {
if (!this.hasValidDocument) {
this.$snackbar(
this.isVietnamese ? "Không có file để tải" : "No file to download",
{ type: 'is-warning' }
);
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
return;
}
@@ -259,4 +283,4 @@ export default {
max-width: 100%;
}
}
</style>
</style>

View File

@@ -1,17 +1,29 @@
<template>
<div>
<!-- Loading Overlay -->
<div v-if="isLoading" class="loading-overlay">
<div
v-if="isLoading"
class="loading-overlay"
>
<div class="loader"></div>
</div>
<!-- View Mode Toggle -->
<div class="mb-5 pb-3" style="border-bottom: 2px solid #e8e8e8;">
<div class="buttons has-addons ">
<button @click="viewMode = 'list'" :class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']">
<div
class="mb-5 pb-3"
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
</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
</button>
</div>
@@ -19,8 +31,11 @@
<!-- Phase Document Types List -->
<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 -->
<div class="level is-mobile mb-4">
<div class="level-left">
@@ -34,9 +49,9 @@
<div class="level-item">
<FileUpload
v-if="$getEditRights()"
:type="['file', 'image', 'pdf']"
@files="(files) => handleUpload(files, doctype.doctype)"
position="right"
:type="['file', 'image', 'pdf']"
@files="(files) => handleUpload(files, doctype.doctype)"
position="right"
/>
</div>
</div>
@@ -45,112 +60,195 @@
<!-- List View -->
<div v-if="viewMode === 'list'">
<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"
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'"
@mouseleave="$event.currentTarget.style.opacity = '0.95'">
<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;">
@mouseleave="$event.currentTarget.style.opacity = '0.95'"
>
<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"
>
{{ file.name || file.file__name }}
</p>
<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>
</div>
<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">
<SvgIcon v-bind="{
name: 'view.svg',
type: 'success',
size: 18,
}"></SvgIcon>
<SvgIcon
v-bind="{
name: 'view.svg',
type: 'success',
size: 18,
}"
></SvgIcon>
</span>
</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">
<SvgIcon v-bind="{
name: 'download.svg',
type: 'success',
size: 18,
}"></SvgIcon>
<SvgIcon
v-bind="{
name: 'download.svg',
type: 'success',
size: 18,
}"
></SvgIcon>
</span>
</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">
<SvgIcon v-bind="{
name: 'bin.svg',
type: 'danger',
size: 18,
}"></SvgIcon>
<SvgIcon
v-bind="{
name: 'bin.svg',
type: 'danger',
size: 18,
}"
></SvgIcon>
</span>
</button>
</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
</div>
</div>
<!-- Gallery View -->
<div v-if="viewMode === 'gallery'">
<div v-if="getFilesByDocType(doctype.doctype).length > 0" class="columns is-multiline is-variable is-2">
<div v-for="file in getFilesByDocType(doctype.doctype)" :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
v-if="getFilesByDocType(doctype.doctype).length > 0"
class="columns is-multiline is-variable is-2"
>
<div
v-for="file in getFilesByDocType(doctype.doctype)"
: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
style="flex: 1; display: flex; align-items: center; 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;">
style="
flex: 1;
display: flex;
align-items: center;
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 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
</div>
</div>
<div style="padding: 12px 16px;">
<p 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;">
<div style="padding: 12px 16px">
<p
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 }}
</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">
<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">
<SvgIcon v-bind="{
name: 'view.svg',
type: 'success',
size: 18,
}"></SvgIcon>
<SvgIcon
v-bind="{
name: 'view.svg',
type: 'success',
size: 18,
}"
></SvgIcon>
</span>
</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">
<SvgIcon v-bind="{
name: 'download.svg',
type: 'success',
size: 18,
}"></SvgIcon>
<SvgIcon
v-bind="{
name: 'download.svg',
type: 'success',
size: 18,
}"
></SvgIcon>
</span>
</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">
<SvgIcon v-bind="{
name: 'bin.svg',
type: 'danger',
size: 18,
}"></SvgIcon>
<SvgIcon
v-bind="{
name: 'bin.svg',
type: 'danger',
size: 18,
}"
></SvgIcon>
</span>
</button>
</div>
@@ -158,7 +256,10 @@
</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
</div>
</div>
@@ -166,11 +267,19 @@
</div>
<!-- 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>
</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>
</template>
@@ -184,7 +293,7 @@ export default {
props: {
row: {
type: Object,
required: true
required: true,
},
},
data() {
@@ -194,7 +303,7 @@ export default {
isLoading: false,
showmodal: undefined,
phasedoctypes: [],
viewMode: 'list',
viewMode: "list",
};
},
async created() {
@@ -221,9 +330,14 @@ export default {
if (!this.transaction?.phase) return;
try {
const phasedoctypesData = await $getdata('phasedoctype', {
phase: this.transaction.phase,
}, undefined, false);
const phasedoctypesData = await $getdata(
"phasedoctype",
{
phase: this.transaction.phase,
},
undefined,
false,
);
if (phasedoctypesData) {
this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData];
@@ -240,15 +354,27 @@ export default {
if (!this.row.id) return;
this.isLoading = true;
try {
const detail = await $getdata('reservation', {
id: this.transaction.txncurrent__detail
}, undefined, true)
const filesArray = await $getdata('transactionfile', {
txn_detail: detail.id,
}, undefined, false);
const detail = await $getdata(
"reservation",
{
id: this.transaction.txncurrent__detail,
},
undefined,
true,
);
const filesArray = await $getdata(
"transactionfile",
{
txn_detail: detail.id,
},
undefined,
false,
);
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 {
this.files = [];
}
@@ -260,18 +386,20 @@ export default {
}
},
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) {
return fileName ? fileName.split('.').pop().toLowerCase() : 'file';
return fileName ? fileName.split(".").pop().toLowerCase() : "file";
},
isImage(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) {
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) {
if (!uploadedFiles || uploadedFiles.length === 0) return;
@@ -282,15 +410,20 @@ export default {
try {
for (const fileRecord of uploadedFiles) {
if (docTypeId) {
await $patchapi('file', {
await $patchapi("file", {
id: fileRecord.id,
doc_type: docTypeId
doc_type: docTypeId,
});
}
const detail = await $getdata('reservation', {
id: this.transaction.txncurrent__detail,
}, undefined, true)
const detail = await $getdata(
"reservation",
{
id: this.transaction.txncurrent__detail,
},
undefined,
true,
);
const payload = {
txn_detail: detail.id,
@@ -305,7 +438,7 @@ export default {
}
await this.fetchFiles();
this.$emit('upload-completed');
this.$emit("upload-completed");
} catch (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.");
@@ -318,21 +451,21 @@ export default {
const filePath = file.file__file || file.file;
if (!filePath) return;
const link = document.createElement('a');
const link = document.createElement("a");
link.href = `${$getpath()}static/files/${filePath}`;
link.download = file.file__name || 'download';
link.download = file.file__name || "download";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
deleteFile(fileId) {
this.showmodal = {
component: 'dialog/Confirm',
title: 'Xác nhận xóa',
height: '10vh',
width: '40%',
component: "dialog/Confirm",
title: "Xác nhận xóa",
height: "10vh",
width: "40%",
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 () => {
this.isLoading = true;
@@ -350,17 +483,17 @@ export default {
} finally {
this.isLoading = false;
}
}
},
};
},
handleModalEvent(event) {
if (event.name === 'confirm' && typeof this.showmodal?.onConfirm === 'function') {
if (event.name === "confirm" && typeof this.showmodal?.onConfirm === "function") {
this.showmodal.onConfirm();
}
},
viewFile(file) {
const { $getpath } = useNuxtApp();
const fileName = file.file__name || '';
const fileName = file.file__name || "";
const filePath = file.file__file || file.file;
if (!filePath) return;
@@ -371,23 +504,23 @@ export default {
if (isImageFile) {
this.showmodal = {
title: fileName,
component: 'media/ChipImage',
component: "media/ChipImage",
vbind: {
extend: false,
file: file,
image: fileUrl,
show: ['download', 'delete']
}
show: ["download", "delete"],
},
};
} else if (isViewable) {
// Mở Google Viewer trực tiếp trong tab mới
const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`;
window.open(viewerUrl, '_blank');
window.open(viewerUrl, "_blank");
} else {
this.downloadFile(file);
}
},
}
},
};
</script>
@@ -420,4 +553,4 @@ export default {
transform: rotate(360deg);
}
}
</style>
</style>

View File

@@ -1,4 +1,4 @@
<!-- Viewer: display when click tem from another dealer -->
<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>
</template>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,15 @@
<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 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 -->
<div class="is-flex is-justify-content-space-between is-align-items-center">
<h3 class="title is-4 has-text-primary mb-1">
@@ -12,101 +19,186 @@
<span class="button is-white">
<span class="has-text-weight-semibold">Đơn vị: VNĐ</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>
</button>
</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 -->
<div class="fixed-grid has-4-cols-mobile has-7-cols-desktop">
<div class="grid">
<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="has-text-primary has-text-weight-medium">{{ productData.trade_code || productData.code }} <a
class="ml-4" id="ignore" @click="$copyToClipboard(productData.trade_code)">
<SvgIcon name="copy.svg" type="primary" :size="18" />
<p class="has-text-primary has-text-weight-medium">
{{ productData.trade_code || productData.code }}
<a
class="ml-4"
id="ignore"
@click="$copyToClipboard(productData.trade_code)"
>
<SvgIcon
name="copy.svg"
type="primary"
:size="18"
/>
</a>
</p>
</div>
<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="has-text-primary">{{ $numtoString(calculatorData.originPrice) }}</p>
<p class="has-text-primary">
{{ $numtoString(calculatorData.originPrice) }}
</p>
</div>
<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="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 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="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 v-if="selectedPolicy.contract_allocation_percentage < 100"
class="cell is-col-span-6-mobile is-col-span-1-desktop">
<div
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="has-text-primary">{{ $numtoString(calculatorData.allocatedPrice) }}</p>
<p class="has-text-primary">
{{ $numtoString(calculatorData.allocatedPrice) }}
</p>
</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="has-text-primary">{{ $numtoString(selectedPolicy.deposit) }}</p>
<p class="has-text-primary">
{{ $numtoString(selectedPolicy.deposit) }}
</p>
</div>
<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 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 }}
</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>
<hr class="my-4" style="background-color: var(--bulma-background)"></hr>
<hr
class="my-4"
style="background-color: var(--bulma-background)"
/>
<!-- Detailed Discounts -->
<div v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0" class="mt-4 mb-4">
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
CHI TIẾT CHIẾT KHẤU:
</p>
<div
v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0"
class="mt-4 mb-4"
>
<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">
<thead>
<tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;"
colspan="2">Diễn giải chiết khấu</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>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
colspan="2"
>
Diễn giải chiết khấu
</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>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #f5f5f5;" class="has-text-grey-light">
<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
style="border-bottom: 1px solid #f5f5f5"
class="has-text-grey-light"
>
<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 v-for="(item, idx) in calculatorData.detailedDiscounts" :key="`discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5;">
<td width="5%" class="has-text-centered">{{ idx + 1 }}</td>
<tr
v-for="(item, idx) in calculatorData.detailedDiscounts"
:key="`discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5"
>
<td
width="5%"
class="has-text-centered"
>
{{ idx + 1 }}
</td>
<td>
<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>
</td>
<td class="has-text-right">{{ item.customType === 1 ? item.customValue + '%' :
$numtoString(item.customValue) }}</td>
<td class="has-text-right">
{{ 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-primary">{{ $numtoString(item.remaining) }}</td>
<td class="has-text-right has-text-primary">
{{ $numtoString(item.remaining) }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- Early Payment Details -->
<div v-if="isEarlyPaymentActive" class="mt-4 mb-4">
<div
v-if="isEarlyPaymentActive"
class="mt-4 mb-4"
>
<!-- Original Schedule -->
<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)
@@ -115,26 +207,57 @@
<table class="table is-fullwidth is-hoverable is-size-6">
<thead>
<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 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>
</thead>
<tbody>
<tr v-for="(plan, index) in calculatorData.originalPaymentSchedule" :key="`orig-plan-${index}`"
style="border-bottom: 1px solid #f5f5f5;">
<tr
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-right">{{ plan.type === 1 ? `${plan.value}%` : '-' }}</td>
<td class="has-text-right">{{ $numtoString(plan.amount) }}</td>
<td class="has-text-right">
{{ plan.type === 1 ? `${plan.value}%` : "-" }}
</td>
<td class="has-text-right">
{{ $numtoString(plan.amount) }}
</td>
<td>{{ formatDate(plan.from_date) }}</td>
<td>{{ formatDate(plan.to_date) }}</td>
<td class="has-text-right">{{ plan.days }}</td>
@@ -151,29 +274,62 @@
<table class="table is-fullwidth is-hoverable is-size-6">
<thead>
<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 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>
</thead>
<tbody>
<tr v-for="(item, idx) in calculatorData.earlyDiscountDetails" :key="`early-discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5;">
<tr
v-for="(item, idx) in calculatorData.earlyDiscountDetails"
:key="`early-discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5"
>
<td>Đợt {{ item.cycle }}</td>
<td>{{ formatDate(item.original_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.discount_rate }}</td>
<td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td>
@@ -181,9 +337,15 @@
</tbody>
<tfoot>
<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 class="has-text-right has-text-weight-bold has-text-danger">-{{
$numtoString(totalEarlyDiscount) }}</th>
<th
colspan="6"
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>
</tfoot>
</table>
@@ -191,7 +353,10 @@
</div>
<!-- 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-left">
<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>
</p>
</div>
<div class="level-right" id="ignore-print">
<div
class="level-right"
id="ignore-print"
>
<div class="buttons are-small has-addons">
<button class="button" @click="viewMode = 'table'"
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'">
<button
class="button"
@click="viewMode = 'table'"
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'"
>
<span class="is-size-6">Bảng</span>
</button>
<button class="button" @click="viewMode = 'list'"
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'">
<button
class="button"
@click="viewMode = 'list'"
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'"
>
<span class="is-size-6">Thẻ</span>
</button>
</div>
@@ -214,64 +388,136 @@
</div>
<!-- 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">
<thead>
<tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
thanh toán</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 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>
<th
class="has-background-primary has-text-white has-font-weight-normal"
style="border: none"
>
Đợt thanh toán
</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 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>
</thead>
<tbody>
<tr v-for="(plan, index) in displaySchedule" :key="`plan-${index}`"
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' : ''">
<tr
v-for="(plan, index) in displaySchedule"
:key="`plan-${index}`"
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 }}
<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 class="has-text-right">
<div v-if="plan.is_merged" class="has-text-right">
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount) }}
<div
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 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>
<span v-else>{{ $numtoString(plan.amount) }}</span>
</td>
<td class="has-text-right has-text-success">{{ $numtoString(plan.paid_amount) }}</td>
<td class="has-text-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</td>
<td class="has-text-right has-text-success">
{{ $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.to_date) }}</td>
<td>
<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>
<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>
</tr>
</tbody>
<tfoot>
<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">{{ $numtoString(totalAmount) }}</th>
<th class="has-text-right has-text-weight-bold has-text-success">{{ $numtoString(totalPaid) }}
<th class="has-text-right has-text-weight-bold">
{{ $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 class="has-text-right has-text-weight-bold has-text-danger">{{
$numtoString(calculatorData.totalRemaining) }}</th>
<th colspan="3"></th>
</tr>
</tfoot>
@@ -279,28 +525,57 @@
</div>
<!-- List View (Card) -->
<div v-else-if="viewMode === 'list'" 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
v-else-if="viewMode === 'list'"
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="level is-mobile mb-5">
<div class="level-left">
<div class="level-item">
<span class="tag is-primary" :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>
<span
class="tag is-primary"
: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 class="level-right">
<div class="level-item has-text-weight-bold">
<div 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">
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
<div
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"
/>
<p class="has-text-weight-bold">
{{ $numtoString(plan.amount) }}
</p>
</div>
<span v-else>{{ $numtoString(plan.amount) }}</span>
</div>
@@ -308,25 +583,41 @@
</div>
<div class="level is-mobile mb-1">
<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 class="level is-mobile mb-1">
<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 class="level is-mobile mb-1">
<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 class="level is-mobile mb-1">
<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 class="level is-mobile">
<div class="level-left">Trạng thái:</div>
<div class="level-right">
<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>
<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>
@@ -335,7 +626,10 @@
</div>
<!-- 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-right">
<div class="level-item">
@@ -354,22 +648,22 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
import { ref, computed } from "vue";
import dayjs from "dayjs";
// Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN
const props = defineProps({
productData: {
type: Object,
default: null
default: null,
},
selectedPolicy: {
type: Object,
default: null
default: null,
},
selectedCustomer: {
type: Object,
default: null
default: null,
},
calculatorData: {
type: Object,
@@ -390,15 +684,15 @@ const props = defineProps({
},
isLoading: {
type: Boolean,
default: false
}
default: false,
},
});
// Emits
const emit = defineEmits(['print']);
const emit = defineEmits(["print"]);
// Local state
const viewMode = ref('table');
const viewMode = ref("table");
// Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN
const displaySchedule = computed(() => {
@@ -426,8 +720,8 @@ const totalPaid = computed(() => {
});
const formatDate = (date) => {
if (!date) return '-';
return dayjs(date).format('DD/MM/YYYY');
if (!date) return "-";
return dayjs(date).format("DD/MM/YYYY");
};
</script>
@@ -501,4 +795,4 @@ th,
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>
<template v-if="option === 'your'">
<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">
<textarea class="textarea" placeholder="" rows="3" v-model="v.text"></textarea>
<textarea
class="textarea"
placeholder=""
rows="3"
v-model="v.text"
></textarea>
</div>
<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>
</a>
<a @click="remove(v, i)">
@@ -44,22 +55,40 @@
</div>
</div>
<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>
</template>
<template v-else-if="tab === 'image'">
<div class="field is-grouped mb-0">
<div class="control is-expanded"></div>
<div class="control">
<FileUpload v-bind="{ position: 'left' }" @files="getImages"></FileUpload>
<FileUpload
v-bind="{ position: 'left' }"
@files="getImages"
></FileUpload>
</div>
</div>
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0">
<div class="control mb-2" v-for="(v, i) in image">
<div
class="field is-grouped is-grouped-multiline"
v-if="image.length > 0"
>
<div
class="control mb-2"
v-for="(v, i) in image"
>
<ChipImage
style="width: 128px"
@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>
</div>
@@ -69,24 +98,47 @@
<div class="field is-grouped mb-0">
<div class="control is-expanded"></div>
<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>
<FileShow @remove="removeFile" v-bind="{ files: file, show: { delete: 1 } }"></FileShow>
<FileShow
@remove="removeFile"
v-bind="{ files: file, show: { delete: 1 } }"
></FileShow>
</template>
<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">
<input class="input" placeholder="" v-model="v.link" />
<input
class="input"
placeholder=""
v-model="v.link"
/>
</div>
<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>
</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>
</a>
<a class="mr-3" @click="addLink()">
<a
class="mr-3"
@click="addLink()"
>
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 18 }"></SvgIcon>
</a>
<a @click="removeLink(v, i)">
@@ -95,14 +147,23 @@
</div>
</div>
<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>
</template>
</template>
<template v-else-if="option === 'system'">
<template v-if="tab === '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>
<a @click="copyContent(v.text)">
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
@@ -111,11 +172,21 @@
</div>
</template>
<template v-else-if="tab === 'image'">
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0">
<div class="control mb-2" v-for="(v, i) in image">
<div
class="field is-grouped is-grouped-multiline"
v-if="image.length > 0"
>
<div
class="control mb-2"
v-for="(v, i) in image"
>
<ChipImage
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>
</div>
@@ -125,8 +196,17 @@
<FileShow v-bind="{ files: file }"></FileShow>
</template>
<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">
<a :href="v.link" target="_blank" class="mr-3">{{ v.link }}</a>
<div
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)">
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
</a>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
autocomplete="tel"
/>
<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>
</div>
</template>

View File

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

View File

@@ -1,19 +1,25 @@
<template>
<div>
<template v-if="data">
<article class="message is-findata" v-if="data.length===0">
<div class="message-body py-2 fs-16">
Chưa <b>ghi chú</b> nào được lưu
</div>
<article
class="message is-findata"
v-if="data.length === 0"
>
<div class="message-body py-2 fs-16">Chưa <b>ghi chú</b> nào được lưu</div>
</article>
<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">
<Avatarbox v-bind="{
text: v.user__fullname[0].toUpperCase(),
size: 'two',
type: 'primary'
}" />
<Avatarbox
v-bind="{
text: v.user__fullname[0].toUpperCase(),
size: 'two',
type: 'primary',
}"
/>
</figure>
<div class="media-content">
<div>
@@ -22,14 +28,20 @@
</p>
<p class="mt-1 fs-14 has-text-grey">
<span class="icon-text">
<span>{{v.user__fullname}}</span>
<span class="ml-3">{{ $dayjs(v['create_time']).fromNow(true) }}</span>
<template v-if="login.id===v.user">
<a class="ml-3" @click="edit(v)">
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 20}"></SvgIcon>
<span>{{ v.user__fullname }}</span>
<span class="ml-3">{{ $dayjs(v["create_time"]).fromNow(true) }}</span>
<template v-if="login.id === v.user">
<a
class="ml-3"
@click="edit(v)"
>
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'gray', size: 20 }"></SvgIcon>
</a>
<a class="ml-3" @click="askConfirm(v, i)">
<SvgIcon v-bind="{name: 'bin.svg', type: 'gray', size: 20}"></SvgIcon>
<a
class="ml-3"
@click="askConfirm(v, i)"
>
<SvgIcon v-bind="{ name: 'bin.svg', type: 'gray', size: 20 }"></SvgIcon>
</a>
</template>
</span>
@@ -39,87 +51,126 @@
</article>
</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">
<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 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>
<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>
</template>
<script>
export default {
props: ['row', 'api', 'pagename'],
props: ["row", "api", "pagename"],
data() {
return {
data: undefined,
detail: undefined,
vbind2: {image: undefined, text: 'ABC', size: 'two', type: 'findata'},
vbind2: { image: undefined, text: "ABC", size: "two", type: "findata" },
current: undefined,
showmodal: undefined,
obj: undefined
}
obj: undefined,
};
},
async created() {
if(!this.row) return
this.data = await this.$getdata(this.api, {ref: this.row.id})
if (!this.row) return;
this.data = await this.$getdata(this.api, { ref: this.row.id });
},
computed: {
login: {
get: function() {return this.$store.login},
set: function(val) {this.$store.commit("updateLogin", {login: val})}
}
get: function () {
return this.$store.login;
},
set: function (val) {
this.$store.commit("updateLogin", { login: val });
},
},
},
methods: {
async save() {
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}
if(this.current) {
data = this.$copy(this.current)
data.detail = this.detail
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,
};
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)
if(!rs) return
this.detail = undefined
if(this.current) {
this.current = undefined
let idx = this.$findIndex(this.data, {id: rs.id})
this.$set(this.data, idx, rs)
let rs = data.id ? await this.$updateapi(this.api, data) : await this.$insertapi(this.api, data);
if (!rs) return;
this.detail = undefined;
if (this.current) {
this.current = undefined;
let idx = this.$findIndex(this.data, { id: rs.id });
this.$set(this.data, idx, rs);
} else {
this.data.push(rs)
let rows = this.$copy(this.$store[this.pagename].data)
let idx = this.$findIndex(rows, {id: this.row.id})
let copy = this.$copy(this.row)
copy.count_note += 1
rows[idx] = copy
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}})
this.data.push(rs);
let rows = this.$copy(this.$store[this.pagename].data);
let idx = this.$findIndex(rows, { id: this.row.id });
let copy = this.$copy(this.row);
copy.count_note += 1;
rows[idx] = copy;
this.$store.commit("updateState", {
name: this.pagename,
key: "update",
data: { data: rows },
});
}
},
edit(v) {
this.current = this.$copy(v)
this.detail = v.detail
this.current = this.$copy(v);
this.detail = v.detail;
},
askConfirm(v, 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},
title: 'Xóa ghi chú', width: '500px', height: '100px'}
this.obj = { v: v, i: i };
this.showmodal = {
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() {
let v = this.obj.v
let i = this.obj.i
let rs = await this.$deleteapi(this.api, v.id)
if(rs==='error') return
this.$delete(this.data, i)
let rows = this.$copy(this.$store[this.pagename].data)
let idx = this.$findIndex(rows, {id: this.row.id})
let copy = this.$copy(this.row)
copy.count_note -= 1
rows[idx] = copy
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}})
}
}
}
</script>
let v = this.obj.v;
let i = this.obj.i;
let rs = await this.$deleteapi(this.api, v.id);
if (rs === "error") return;
this.$delete(this.data, i);
let rows = this.$copy(this.$store[this.pagename].data);
let idx = this.$findIndex(rows, { id: this.row.id });
let copy = this.$copy(this.row);
copy.count_note -= 1;
rows[idx] = copy;
this.$store.commit("updateState", {
name: this.pagename,
key: "update",
data: { data: rows },
});
},
},
};
</script>

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,24 @@
<script setup>
const props = defineProps({
row: Object,
pagename: String
pagename: String,
});
const { $id } = useNuxtApp();
</script>
<template>
<div>
<DataView v-bind="{
setting: 'product-info',
pagename: $id(),
api: 'product',
params: {
filter: { prdbk__transaction__customer: props.row.id },
// 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',
}
}" />
<DataView
v-bind="{
setting: 'product-info',
pagename: $id(),
api: 'product',
params: {
filter: { prdbk__transaction__customer: props.row.id },
// 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>
</template>
</template>

View File

@@ -1,27 +1,40 @@
<template>
<div class="has-text-centered">
<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 class="mt-2 px-5 is-flex is-justify-content-center">
<ClientOnly>
<Qrcode
v-if="finalLink"
:key="finalLink"
v-if="finalLink"
:key="finalLink"
id="qrcode"
:value="finalLink"
:size="300"
/>
</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
</div>
</div>
<div class="mt-2 is-flex is-justify-content-center is-gap-1">
<a
@click="openLink()"
<a
@click="openLink()"
class="button is-light is-link is-rounded"
:title="isVietnamese ? 'Mở đường dẫn liên kết' : 'Open external link'"
v-if="finalLink"
@@ -31,8 +44,8 @@
</span>
</a>
<a
@click="download()"
<a
@click="download()"
class="button is-light is-link is-rounded"
:title="isVietnamese ? 'Tải Xuống QR Code' : 'Download QR Code'"
v-if="finalLink"
@@ -45,83 +58,85 @@
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useStore } from "@/stores/index";
const store = useStore();
const isVietnamese = computed(() => store.lang === "vi")
import { computed } from "vue";
import { useStore } from "@/stores/index";
const { $getpath, $snackbar } = useNuxtApp()
const props = defineProps({
row: Object,
link: String
})
const store = useStore();
const isVietnamese = computed(() => store.lang === "vi");
const finalLink = computed(() => {
if (props.link) {
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 ''
})
const { $getpath, $snackbar } = useNuxtApp();
const props = defineProps({
row: Object,
link: String,
});
function openLink() {
if (finalLink.value) {
window.open(finalLink.value, "_blank")
}
const finalLink = computed(() => {
if (props.link) {
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) {
return new Promise(resolve => setTimeout(resolve, ms));
if (!svg) {
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() {
if (!finalLink.value) return;
const serializer = new XMLSerializer();
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');
let attempts = 0;
while (!svg && attempts < 5) {
await sleep(100);
svg = document.getElementById('qrcode');
attempts++;
}
const image = new Image();
image.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 300;
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, 300, 300);
if (!svg) {
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 pngUrl = canvas.toDataURL("image/png");
const serializer = new XMLSerializer()
const svgData = serializer.serializeToString(svg)
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
const url = URL.createObjectURL(svgBlob)
const linkElement = document.createElement("a");
linkElement.href = pngUrl;
const image = new Image()
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = 300
canvas.height = 300
const ctx = canvas.getContext('2d')
ctx.drawImage(image, 0, 0, 300, 300)
const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : "qrcode.png";
linkElement.download = filename;
linkElement.click();
const pngUrl = canvas.toDataURL('image/png')
const linkElement = document.createElement('a')
linkElement.href = pngUrl
const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : 'qrcode.png'
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>
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>
<div>
<DataView v-if="vbind" v-bind="vbind"></DataView>
<DataView
v-if="vbind"
v-bind="vbind"
></DataView>
</div>
</template>
<script>
export default {
props: ['vbind']
}
</script>
props: ["vbind"],
};
</script>

View File

@@ -1,175 +1,279 @@
<template>
<div v-if="record">
<div class="columns is-multiline mx-0">
<div :class="`column is-3 ${viewport===1? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{findLang('code')}}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<input class="input has-text-black" disabled type="text" placeholder="" v-model="record.code">
</div>
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p>
</div>
</div>
<div class="columns is-multiline mx-0">
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{ findLang("code") }}<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<input
class="input has-text-black"
disabled
type="text"
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="field">
<label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<input class="input" type="text" placeholder="" v-model="record.fullname">
</div>
<p class="help is-danger" v-if="errors.fullname">{{errors.fullname}}</p>
</div>
<div :class="`column is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<input
class="input"
type="text"
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 :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 class="mt-2">
<button
class="button is-primary has-text-white"
@click="update()"
>
{{ findLang("save") }}
</button>
</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">
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</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>
<script setup>
import InputPhone from '~/components/common/InputPhone'
import InputEmail from '~/components/common/InputEmail'
import SearchBox from '~/components/SearchBox'
import { useStore } from '@/stores/index'
var props = defineProps({
pagename: String,
row: Object
})
const store = useStore()
const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } = useNuxtApp()
const emit = defineEmits(['update', 'dataevent'])
var viewport = store.viewport
var errors = ref({})
var record = ref()
var showmodal = undefined
var existedCustomer = undefined
async function initData() {
if(props.row) {
let conn = $findapi('company')
conn.params.filter = {id: props.row.company || props.row.customer__company || props.row.id}
let rs = await $getapi([conn])
let found = $find(rs, {name: 'company'})
if(found.data.rows.length>0) record.value = found.data.rows[0]
} else {
record.value = {}
import InputPhone from "~/components/common/InputPhone";
import InputEmail from "~/components/common/InputEmail";
import SearchBox from "~/components/SearchBox";
import { useStore } from "@/stores/index";
var props = defineProps({
pagename: String,
row: Object,
});
const store = useStore();
const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } =
useNuxtApp();
const emit = defineEmits(["update", "dataevent"]);
var viewport = store.viewport;
var errors = ref({});
var record = ref();
var showmodal = undefined;
var existedCustomer = undefined;
async function initData() {
if (props.row) {
let conn = $findapi("company");
conn.params.filter = {
id: props.row.company || props.row.customer__company || props.row.id,
};
let rs = await $getapi([conn]);
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) {
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.'
}
}
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>
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"
:key="i"
: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)"
>
{{ isVietnamese ? v.name : v.en }}
</div>
</template>
<div v-else class="field is-grouped is-grouped-multiline">
<div class="control" v-for="(v, i) in tabs" @click="changeTab(v)">
<div
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 :class="`py-3 px-3 ${getStyle(v)}`">
{{ isVietnamese ? v.name : v.en }}
@@ -54,7 +61,11 @@
</div>
</div>
<Modal @close="handleModalClose" v-bind="showmodal" v-if="showmodal"></Modal>
<Modal
@close="handleModalClose"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template>
<script setup>
import { computed, ref } from "vue";
@@ -99,11 +110,7 @@ function getStyle(v) {
function changeTab(v) {
if (tab.value === v.code) return;
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"
);
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");
tab.value = v.code;
}
@@ -115,7 +122,7 @@ function update(v) {
record = {
...v,
label: `${v.code} / ${v.fullname} / ${v.phone || ""}`,
}
};
emit("modalevent", { name: "dataevent", data: record });
if (!props.isEditMode) emit("close");
}
@@ -123,8 +130,7 @@ function update(v) {
<style scoped>
.title {
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial,
sans-serif !important;
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}
.button.is-large.is-fullwidth:hover {
color: white !important;

View File

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

View File

@@ -1,5 +1,8 @@
<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">
{{ isVietnamese ? "Chọn loại khách hàng" : "Select Customer Type" }}
</h3>
@@ -7,8 +10,8 @@
<div class="column is-6">
<button
:disabled="!$getEditRights('edit', { code: 'individual', category: 'submenu' })"
class="button is-large is-fullwidth"
style="height: 120px"
class="button is-large is-fullwidth"
style="height: 120px"
@click="selectCustomerType(1)"
>
<div class="has-text-centered">
@@ -22,10 +25,10 @@
</button>
</div>
<div class="column is-6">
<button
<button
:disabled="!$getEditRights('edit', { code: 'org', category: 'submenu' })"
class="button is-large is-fullwidth"
style="height: 120px"
class="button is-large is-fullwidth"
style="height: 120px"
@click="selectCustomerType(2)"
>
<div class="has-text-centered">
@@ -46,28 +49,60 @@
<div class="columns is-multiline">
<div class="column is-4">
<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">
<input class="input" type="text" v-model="record.fullname" />
<input
class="input"
type="text"
v-model="record.fullname"
/>
</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 class="column is-4">
<div class="field">
<label class="label">{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
<InputPhone v-bind="{ record: record, attr: 'phone' }" @phone="selected('phone', $event)"></InputPhone>
<p class="help is-danger" v-if="errors.phone">
<label class="label"
>{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label
>
<InputPhone
v-bind="{ record: record, attr: 'phone' }"
@phone="selected('phone', $event)"
></InputPhone>
<p
class="help is-danger"
v-if="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>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">Email</label>
<InputEmail v-bind="{ record: record, attr: 'email' }" @email="selected('email', $event)"></InputEmail>
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
<InputEmail
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>
@@ -75,28 +110,47 @@
<div class="column is-6">
<div class="field">
<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 class="column is-6">
<div class="field">
<label class="label">{{ isIndividual ? 'Địa chỉ thường trú' : 'Địa chỉ đăng ký' }}</label>
<input class="input" type="text" v-model="record.address" />
<label class="label">{{ isIndividual ? "Địa chỉ thường trú" : "Địa chỉ đăng ký" }}</label>
<input
class="input"
type="text"
v-model="record.address"
/>
</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="field">
<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 class="column is-6">
<div class="field">
<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>
@@ -104,46 +158,117 @@
<div class="columns is-multiline">
<div class="column is-3">
<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>
<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>
<label class="label"
>{{ 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 class="column is-3">
<div class="field">
<label class="label">{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
<input class="input" type="text" v-model="record.legal_code" />
<p class="help is-danger" v-if="errors.legal_code">{{ errors.legal_code }}</p>
<label class="label"
>{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label
>
<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 class="column is-3">
<div class="field">
<label class="label">{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
<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>
<label class="label"
>{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label
>
<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 class="column is-3">
<div class="field">
<label class="label">{{ dataLang && findFieldName("issued_place")[lang] }}</label>
<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] } }"
@option="selected('issued_place', $event)"></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] },
}"
@option="selected('issued_place', $event)"
></SearchBox>
</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="field">
<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 class="column is-3">
<div class="field">
<label class="label">{{ dataLang && findFieldName("birth_date")[lang] }}</label>
<Datepicker 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>
<Datepicker
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>
@@ -152,19 +277,33 @@
<div class="column is-12">
<div class="field">
<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 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 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">
<label class="label" v-if="i === 0">{{ findFieldName("select")[lang] }}</label>
<SearchBox
v-bind="{
<label
class="label"
v-if="i === 0"
>{{ findFieldName("select")[lang] }}</label
>
<SearchBox
v-bind="{
api: 'people',
field: 'label',
column: ['code', 'fullname', 'phone'],
@@ -172,34 +311,54 @@
optionid: v.people,
position: 'is-top-left',
addon: peopleAddon,
viewaddon: peopleviewAddon
}"
@option="selectPeople($event, v, i)"></SearchBox>
viewaddon: peopleviewAddon,
}"
@option="selectPeople($event, v, i)"
></SearchBox>
</div>
<div class="column is-4">
<label class="label" v-if="i === 0">{{ isIndividual ? 'Quan hệ' : 'Chức vụ' }}</label>
<SearchBox
v-bind="{
<label
class="label"
v-if="i === 0"
>{{ isIndividual ? "Quan hệ" : "Chức vụ" }}</label
>
<SearchBox
v-bind="{
api: 'relation',
field: store.lang === 'en' ? 'en' : 'name',
column: ['code', 'name', 'en'],
first: true,
optionid: v.relation,
column: ['code', 'name', 'en'],
first: true,
optionid: v.relation,
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)"
/>
</div>
<div class="column is-narrow">
<label class="label" 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()">
<label
class="label"
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">
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
</button>
<button class="button is-dark" @click="remove(v, i)">
<button
class="button is-dark"
@click="remove(v, i)"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
@@ -209,11 +368,25 @@
</div>
<div class="mt-5 buttons is-right">
<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>
<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>
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div>
</template>
</template>
@@ -226,7 +399,7 @@ import InputEmail from "~/components/common/InputEmail";
import SearchBox from "~/components/SearchBox";
import Datepicker from "~/components/datepicker/Datepicker";
import { useStore } from "~/stores/index";
import { isEqual, pick } from 'es-toolkit';
import { isEqual, pick } from "es-toolkit";
const emit = defineEmits(["close", "update", "modalevent"]);
const { $getdata, $patchapi, $insertapi, $deleteapi, $empty, $errPhone, $resetNull, $snackbar, $copy } = useNuxtApp();
@@ -260,21 +433,32 @@ const isOrganization = computed(() => selectedCustomerType.value === 2);
const filteredLegalTypes = computed(() => {
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",
width: "65%",
height: "500px",
title: store.lang === "en" ? "Related person" : "Người liên quan"
height: "500px",
title: store.lang === "en" ? "Related person" : "Người liên quan",
};
const peopleviewAddon = { ...peopleAddon };
function selectCustomerType(type) {
const typeNum = Number(type);
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) {
individualData.value = { dob: null, sex: 1 };
organizationData.value = {};
@@ -287,40 +471,61 @@ function selectCustomerType(type) {
}
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() {
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 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 selected = (f, v) => {
record.value[f] = v && typeof v === "object" ? v.id : v;
if (errors.value[f]) delete errors.value[f];
};
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 remove = (_v, i) => {
localPeople.value.splice(i, 1);
if (localPeople.value.length === 0) localPeople.value = [{}];
};
watch(people, (val) => {
localPeople.value = val.map(cp => pick(cp, ['id', 'people', 'relation']));
}, { deep: true })
watch(
people,
(val) => {
localPeople.value = val.map((cp) => pick(cp, ["id", "people", "relation"]));
},
{ deep: true },
);
function checkError() {
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)) {
errors.value.phone = isVietnamese.value ? "Số điện thoại không được bỏ trống" : "Phone is required";
} else {
const text = $errPhone(record.value.phone);
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";
return Object.keys(errors.value).length > 0;
@@ -349,7 +554,10 @@ async function update() {
}
if (record.value.legal_code) {
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.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;
const customerId = res.id;
@@ -367,7 +577,8 @@ async function update() {
if (isIndividual.value) {
let indPayload = $resetNull({ ...individualData.value });
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 if (isOrganization.value) {
let orgPayload = $resetNull({ ...organizationData.value });
@@ -393,22 +604,22 @@ async function update() {
commonPayload = { organization: organizationId };
}
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 validLocalPeople = localPeople.value.filter((lp) => lp.people && lp.relation).map((lp) => toRaw(lp));
const peopleKeys = people.value.map((p) => pick(p, ["id", "people", "relation"]));
// 1. check existing ids, if people or relation changes -> patch
const existingLocalPeople = validLocalPeople.filter(cp => Boolean(cp.id));
existingLocalPeople.forEach(lp => {
const match = peopleKeys.find(p => isEqual(p, lp));
const payload = { ...lp, ...commonPayload }
const existingLocalPeople = validLocalPeople.filter((cp) => Boolean(cp.id));
existingLocalPeople.forEach((lp) => {
const match = peopleKeys.find((p) => isEqual(p, lp));
const payload = { ...lp, ...commonPayload };
if (!match) {
$patchapi(apiName, payload);
}
});
// 2. if localPeople has and people doesn't -> insert
validLocalPeople.forEach(lp => {
validLocalPeople.forEach((lp) => {
if (!lp.id) {
const payload = { ...lp, ...commonPayload };
$insertapi(apiName, payload);
@@ -417,8 +628,8 @@ async function update() {
// 3. if people has and localPeople doesn't -> delete
if (peopleKeys.length !== 0 && validLocalPeople.length !== 0) {
peopleKeys.forEach(cp => {
const match = validLocalPeople.find(lp => cp.id === lp.id);
peopleKeys.forEach((cp) => {
const match = validLocalPeople.find((lp) => cp.id === lp.id);
if (!match) {
$deleteapi(apiName, cp.id);
}
@@ -427,17 +638,24 @@ async function update() {
// Ảnh
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);
$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("update", completeData);
setTimeout(() => emit("close"), 100);
} catch (e) { console.error(e); }
} catch (e) {
console.error(e);
}
}
async function initData() {
@@ -465,7 +683,12 @@ async function initData() {
const copyData = $copy(props.application);
const type = props.customerType || copyData.type || 1;
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 };
} else if (props.customerType) {
selectCustomerType(props.customerType);
@@ -473,4 +696,4 @@ async function initData() {
}
onMounted(() => initData());
</script>
</script>

View File

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

View File

@@ -1,5 +1,8 @@
<template>
<div :id="docid" v-if="record">
<div
:id="docid"
v-if="record"
>
<div>
<Caption v-bind="{ title: this.data && findFieldName('info')[this.lang] }"></Caption>
<div class="columns is-multiline mx-0">
@@ -7,7 +10,11 @@
<div class="field">
<label class="label"> khách hàng</label>
<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>
@@ -23,14 +30,21 @@
<div class="field">
<label class="label">Điện thoại</label>
<div class="control">
<span class="hyperlink" @click="openPhone()">{{ record.phone }}</span>
<span
class="hyperlink"
@click="openPhone()"
>{{ record.phone }}</span
>
</div>
</div>
</div>
<div class="column is-3 pb-1 px-0">
<div class="field">
<label class="label">Email</label>
<div class="control" style="word-break: break-all">
<div
class="control"
style="word-break: break-all"
>
{{ record.email || "/" }}
</div>
</div>
@@ -103,7 +117,11 @@
<div class="field">
<label class="label">Người tạo</label>
<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>
@@ -122,7 +140,11 @@
<div class="field">
<label class="label">Người cập nhật</label>
<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>
@@ -131,7 +153,7 @@
<label class="label">Thời gian cập nhật</label>
<div class="control">
<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>
</div>
</div>
@@ -143,58 +165,137 @@
<ImageGallery v-bind="{ row: record, api: 'customerfile', hideopt: true }"></ImageGallery>
</div>
<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
<div
v-if="this.relatedPeople && this.relatedPeople.length > 0"
v-for="relatedPerson in this.relatedPeople"
class="columns is-0 mb-2"
>
<span class="column is-2">{{ relatedPerson.people__code }}</span>
<span class="column is-4 ">
<span class="has-text-primary hyperlink"
<span class="column is-4">
<span
class="has-text-primary hyperlink"
@click="openRelatedPerson(relatedPerson.people)"
>{{ relatedPerson.people__fullname }}</span>
>{{ relatedPerson.people__fullname }}</span
>
<span> ({{ relatedPerson.relation__name }})</span>
</span>
<span class="column is-4">{{ relatedPerson.people__phone }}</span>
</div>
<div v-else class="has-text-grey">
Chưa {{ this.isIndividual ? 'người liên quan' : 'người đại diện pháp luật' }}
<div
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 v-if="record.count_product >0" class="mt-3">
<Caption class="mb-2" v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"></Caption>
<DataView v-bind="{
setting: 'customer-all-transaction',
pagename: this.$id(),
api: 'customer',
params: {
filter: { id: this.row.customer || this.row.id },
/* copied from 02-connection.js */
values:
'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',
distinct_values: {
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' } }
<div
v-if="record.count_product > 0"
class="mt-3"
>
<Caption
class="mb-2"
v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"
></Caption>
<DataView
v-bind="{
setting: 'customer-all-transaction',
pagename: this.$id(),
api: 'customer',
params: {
filter: { id: this.row.customer || this.row.id },
/* copied from 02-connection.js */
values:
'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',
distinct_values: {
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 class="mt-4 border-bottom" id="ignore"></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
class="mt-4 border-bottom"
id="ignore"
></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>
<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>
</template>
<script>
@@ -225,15 +326,19 @@ export default {
},
isIndividual() {
return this.record.type === 1;
}
},
},
async created() {
this.record = await this.$getdata("customer", { id: this.row.customer || this.row.id }, undefined, true);
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 {
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true);
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id });
const org = await this.$getdata("organization", { customer: this.row.customer || this.row.id }, undefined, true);
this.relatedPeople = await this.$getdata("legalrep", {
organization: org.id,
});
}
},
methods: {
@@ -270,10 +375,19 @@ export default {
this.record = this.$copy(v);
// refetch relatedPeople
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 {
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true);
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id });
const org = await this.$getdata(
"organization",
{ customer: this.row.customer || this.row.id },
undefined,
true,
);
this.relatedPeople = await this.$getdata("legalrep", {
organization: org.id,
});
}
},
openUser(userId) {
@@ -287,15 +401,15 @@ export default {
};
},
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 = {
component: "people/PeopleView",
vbind: { row: peopleRow },
title: 'Người liên quan',
width: '65%',
height: '400px',
}
}
title: "Người liên quan",
width: "65%",
height: "400px",
};
},
},
};
</script>

View File

@@ -1,17 +1,18 @@
<template>
<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"
<template>
<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"
style="border: 1px solid var(--bulma-grey-80)"
:style="image && 'border: none'">
:style="image && 'border: none'"
>
<div>
<span>{{text}}</span>
<span>{{ text }}</span>
</div>
</div>
</template>
<script>
export default {
props: ['text', 'image', 'type', 'size']
}
export default {
props: ["text", "image", "type", "size"],
};
</script>
<style>
.cbox {

View File

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

View File

@@ -4,8 +4,8 @@ const props = defineProps({
value: String,
color: String,
icon: String,
subheader: Object
})
subheader: Object,
});
</script>
<template>
<div class="cell">
@@ -14,14 +14,19 @@ const props = defineProps({
<div>
<p class="fs-14 has-text-grey mb-1">{{ name }}</p>
<p class="fsb-26 mb-1 has-text-black">{{ value }}</p>
<div v-if="subheader"
<div
v-if="subheader"
:class="[
'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
: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"
/>
<p class="fs-14">{{ subheader.value }}</p>
@@ -30,12 +35,15 @@ const props = defineProps({
<div
:class="[
'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>
</template>
</template>

View File

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

View File

@@ -3,7 +3,47 @@
class="relative w-full has-background-blue-95 rounded-lg is-clipped"
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
@@ -25,7 +65,11 @@
<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"
></path>
<circle cx="12" cy="10" r="3"></circle>
<circle
cx="12"
cy="10"
r="3"
></circle>
</svg>
</div>
<div
@@ -47,7 +91,11 @@
<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"
></path>
<circle cx="12" cy="10" r="3"></circle>
<circle
cx="12"
cy="10"
r="3"
></circle>
</svg>
</div>
<div
@@ -69,7 +117,11 @@
<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"
></path>
<circle cx="12" cy="10" r="3"></circle>
<circle
cx="12"
cy="10"
r="3"
></circle>
</svg>
</div>
<div
@@ -91,7 +143,11 @@
<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"
></path>
<circle cx="12" cy="10" r="3"></circle>
<circle
cx="12"
cy="10"
r="3"
></circle>
</svg>
</div>
<div
@@ -113,7 +169,11 @@
<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"
></path>
<circle cx="12" cy="10" r="3"></circle>
<circle
cx="12"
cy="10"
r="3"
></circle>
</svg>
</div>
<div
@@ -139,9 +199,7 @@
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg>
</div>
<div
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
</div>
</div>
<div
@@ -167,9 +225,7 @@
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg>
</div>
<div
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
</div>
</div>
<div
@@ -220,9 +276,7 @@
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
</svg>
</div>
<div
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
></div>
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
</div>
</div>
<div
@@ -244,7 +298,12 @@
<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"
></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>
@@ -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,);
}
.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 {
0% {
@@ -339,67 +398,67 @@
@keyframes move-random-1 {
0% {
transform: translateX(0) translateY(0)
transform: translateX(0) translateY(0);
}
33% {
transform: translateX(30px) translateY(24px)
transform: translateX(30px) translateY(24px);
}
66% {
transform: translateX(60px) translateY(12px)
transform: translateX(60px) translateY(12px);
}
100% {
transform: translateX(0) translateY(0)
transform: translateX(0) translateY(0);
}
}
@keyframes move-random-2 {
0% {
transform: translateX(0) translateY(0)
transform: translateX(0) translateY(0);
}
23% {
transform: translateX(-20px) translateY(-36px)
transform: translateX(-20px) translateY(-36px);
}
46% {
transform: translateX(0) translateY(22px)
transform: translateX(0) translateY(22px);
}
75% {
transform: translateX(30px) translateY(-12px)
transform: translateX(30px) translateY(-12px);
}
100% {
transform: translateX(0) translateY(0)
transform: translateX(0) translateY(0);
}
}
@keyframes move-random-3 {
0% {
transform: translateX(0) translateY(0)
transform: translateX(0) translateY(0);
}
10% {
transform: translateX(-30px) translateY(-15px)
transform: translateX(-30px) translateY(-15px);
}
30% {
transform: translateX(-10px) translateY(26px)
transform: translateX(-10px) translateY(26px);
}
50% {
transform: translateX(48px) translateY(-28px)
transform: translateX(48px) translateY(-28px);
}
80% {
transform: translateX(17px) translateY(10px)
transform: translateX(17px) translateY(10px);
}
100% {
transform: translateX(0) translateY(0)
transform: translateX(0) translateY(0);
}
}
.border-2 {
@@ -442,13 +501,20 @@
opacity: 40%;
}
.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));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-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));
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 {
--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,);
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,)
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-property: all;
@@ -474,4 +540,4 @@
.border-gray-300 {
border-color: var(--bulma-grey-85);
}
</style>
</style>

View File

@@ -1,15 +1,15 @@
<script setup>
import AvatarBox from '@/components/dashboard/AvatarBox.vue';
import AvatarBox from "@/components/dashboard/AvatarBox.vue";
const props = defineProps({
name: String,
status: String,
deliveries: Number,
deliveries_completed: Number,
})
});
</script>
<template>
<div
<div
class="is-flex is-gap-2 fs-14 p-3 rounded-lg"
:style="{
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>
</div>
<p class="fs-13 has-text-grey">Đơn: {{ deliveries_completed }}/{{ deliveries }}</p>
<progress
<progress
v-if="deliveries !== deliveries_completed"
class="progress is-small is-primary mt-2"
style="--bulma-size-small: 0.4rem;"
style="--bulma-size-small: 0.4rem"
:value="deliveries_completed"
:max="deliveries"
>
</progress>
></progress>
</div>
</div>
</template>

View File

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

View File

@@ -6,18 +6,16 @@ const props = defineProps({
value: Number,
icon: String,
color: String,
})
});
</script>
<template>
<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="[
'p-3 is-flex rounded-full',
`has-background-${color}-90`,
`has-text-${color}-40`,
]" >
<div :class="['p-3 is-flex rounded-full', `has-background-${color}-90`, `has-text-${color}-40`]">
<Icon
:name="icon"
:size="24"
@@ -30,4 +28,4 @@ const props = defineProps({
</div>
</div>
</div>
</template>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,33 @@
<script setup>
import TopProduct from '@/components/dashboard/TopProduct.vue';
import TopProduct from "@/components/dashboard/TopProduct.vue";
const products = [
{
name: 'Gạo ST25 - Bao 5kg',
name: "Gạo ST25 - Bao 5kg",
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,
revenue: 132000000
revenue: 132000000,
},
{
name: 'Đường tinh luyện - Bao 1kg',
name: "Đường tinh luyện - Bao 1kg",
sold_count: 856,
revenue: 98000000
revenue: 98000000,
},
{
name: 'Dầu ăn Neptune - Chai 1L',
name: "Dầu ăn Neptune - Chai 1L",
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,
revenue: 72000000
revenue: 72000000,
},
]
];
</script>
<template>
<div class="card">
@@ -35,14 +35,14 @@ const products = [
<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">
<TopProduct
v-for="product in products"
v-for="product in products"
:key="product.name"
v-bind="{
...product,
topRevenue: products[0].revenue
topRevenue: products[0].revenue,
}"
/>
</div>
</div>
</div>
</template>
</template>

View File

@@ -3,17 +3,16 @@ const props = defineProps({
name: String,
details: String,
level: Number,
type: String
})
type: String,
});
const color = computed(() => {
if (props.level === 1) return 'yellow';
if (props.level === 2) return 'orange';
if (props.level === 3) return 'red';
})
if (props.level === 1) return "yellow";
if (props.level === 2) return "orange";
if (props.level === 3) return "red";
});
</script>
<template>
<div
: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">
<Icon
<Icon
:name="type === 'time' ? 'material-symbols:clock-loader-40' : 'material-symbols:box-outline-rounded'"
:size="20"
:class="`has-text-${color}-45`"
@@ -33,4 +32,4 @@ const color = computed(() => {
</div>
</div>
</div>
</template>
</template>

View File

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

View File

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

View File

@@ -1,180 +1,312 @@
<template>
<div v-if="docid">
<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" v-if="type.code!=='tag'">
<label class="label">Outline</label>
<div class="field">
<label class="label">Đối tượng</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
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="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
class="field is-horizontal"
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 :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>
<li :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
: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>
</ul>
</div>
<template v-if="tab.code==='selected'">
<a v-for="(v,i) in tags" :key="i" @click="selected=v">
<div class="field is-grouped is-grouped-multiline mt-4">
<p class="control">
<a :class="v.class">
{{v.name}}
<template v-if="tab.code === 'selected'">
<a
v-for="(v, i) in tags"
:key="i"
@click="selected = v"
>
<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>
</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>
</template>
</template>
<template v-else-if="tab.code==='condition'">
<div class="mb-5" v-if="selected">
<b-radio v-for="(v,i) in conditions" :key="i" v-model="condition"
:native-value="v" @input="changeCondition(v)">
{{v.name}}
</b-radio>
</div>
<template v-if="condition? condition.code==='yes' : false">
<div class="field mt-3">
<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 trường thỏa mãn
</template>
</b-taginput>
<template v-else-if="tab.code === 'condition'">
<div
class="mb-5"
v-if="selected"
>
<b-radio
v-for="(v, i) in conditions"
:key="i"
v-model="condition"
:native-value="v"
@input="changeCondition(v)"
>
{{ v.name }}
</b-radio>
</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>
<template v-if="condition ? condition.code === 'yes' : false">
<div class="field mt-3">
<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">
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
<span class="tooltip">
{{v.name}}
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
</span>
<a
@dblclick="expression = expression ? expression + ' ' + v.name : v.name"
class="tag is-rounded"
v-for="(v, i) in tagsField"
:key="i"
>
<span class="tooltip">
{{ v.name }}
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
</span>
</a>
</div>
</div>
<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">
<input class="input" 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>
<input
class="input"
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>
</template>
</template>
<template v-else-if="tab.code==='option' && selected">
<div class="field is-horizontal border-bottom pb-2 mt-1">
</template>
</template>
<template v-else-if="tab.code === 'option' && selected">
<div class="field is-horizontal border-bottom pb-2 mt-1">
<div class="field-body">
<div class="field">
<label class="label fs-14">Màu nền </label>
<p class="control fs-14">
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioBGcolor"
:native-value="v" @input="changeStyle()">
{{v.name}}
</b-radio>
<b-radio
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
:key="i"
v-model="radioBGcolor"
:native-value="v"
@input="changeStyle()"
>
{{ v.name }}
</b-radio>
</p>
</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>
<p class="control fs-14">
<input type="color" v-model="bgcolor" @change="changeStyle()">
<input
type="color"
v-model="bgcolor"
@change="changeStyle()"
/>
</p>
</div>
</div>
</div>
<div class="field is-horizontal border-bottom pb-2">
<div class="field-body">
<div class="field">
<label class="label fs-14">Màu chữ </label>
<p class="control fs-14">
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioColor"
:native-value="v" @input="changeStyle()">
{{v.name}}
<b-radio
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
:key="i"
v-model="radioColor"
:native-value="v"
@input="changeStyle()"
>
{{ v.name }}
</b-radio>
</p>
</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>
<p class="control fs-14">
<input type="color" v-model="color" @change="changeStyle()">
<input
type="color"
v-model="color"
@change="changeStyle()"
/>
</p>
</div>
</div>
@@ -184,119 +316,189 @@
<div class="field">
<label class="label fs-14">Cỡ chữ </label>
<p class="control fs-14">
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioSize"
:native-value="v" @input="changeStyle()">
{{v.name}}
</b-radio>
<b-radio
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
:key="i"
v-model="radioSize"
:native-value="v"
@input="changeStyle()"
>
{{ v.name }}
</b-radio>
</p>
</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>
<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>
</div>
</div>
</div>
</template>
<template v-else-if="tab.code==='template'">
<p class="mb-3">
<a @click="copyContent()" class="mr-6">
<span class="icon-text">
<SvgIcon class="mr-2" v-bind="{name: 'copy.svg', type: 'primary', siz: 18}"></SvgIcon>
</template>
<template v-else-if="tab.code === 'template'">
<p class="mb-3">
<a
@click="copyContent()"
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>
</a>
<a @click="paste()" class="mr-6">
<a
@click="paste()"
class="mr-6"
>
<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>
</a>
</p>
<div>
<textarea class="textarea fs-14" rows="8" v-model="text" @dblclick="doCheck"></textarea>
</div>
<p class="mt-5">
<div>
<textarea
class="textarea fs-14"
rows="8"
v-model="text"
@dblclick="doCheck"
></textarea>
</div>
<p class="mt-5">
<span class="icon-text fsb-18">
Replace
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 22}"></SvgIcon>
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
</span>
</p>
<div class="field is-grouped mt-4">
<div class="control">
<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 class="control">
<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 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>
<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>
</template>
</div>
</template>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useStore } from '@/stores/index'
const store = useStore()
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp()
var props = defineProps({
pagename: String,
field: Object
})
var colorscheme = store.colorscheme
var colorchoice = store.colorchoice
var pageData = store[props.pagename]
var field = props.field
var type = undefined
var size = undefined
var types = [{code: 'span', name: 'span'}, {code: 'tag', name: 'tag'}]
var sizes = [{code: 'is-small', name: 'Nhỏ', value: 'is-size-6'}, {code: 'is-normal', name: 'Trung bình', value: 'is-size-5'},
{code: 'is-medium', name: 'Lớn', value: 'is-size-4'}]
var shapes = [{code: 'default', name: 'Mặc định'}, {code: 'is-rounded', name: 'Tròn góc'}]
var shape = undefined
var outlines = [{code: 'default', name: 'Mặc định'}, {code: 'is-outlined', name: 'Outline'}]
var outline = undefined
var conditions = [{code: 'no', name: 'Không áp dụng'}, {code: 'yes', name: 'Có áp dụng'}]
var condition = undefined
var tags = []
var selected = undefined
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: 'template', name: 'Bước 4: Mã lệnh & áp dụng'}]
var tab = ref(undefined)
var tagsField = []
var errors = []
var expression = ''
var text = ref(null)
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')
}
condition =conditions.find(v=>v.code==='no')
import { ref } from "vue";
import { useStore } from "@/stores/index";
const store = useStore();
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp();
var props = defineProps({
pagename: String,
field: Object,
});
var colorscheme = store.colorscheme;
var colorchoice = store.colorchoice;
var pageData = store[props.pagename];
var field = props.field;
var type = undefined;
var size = undefined;
var types = [
{ code: "span", name: "span" },
{ code: "tag", name: "tag" },
];
var sizes = [
{ code: "is-small", name: "Nhỏ", value: "is-size-6" },
{ code: "is-normal", name: "Trung bình", value: "is-size-5" },
{ code: "is-medium", name: "Lớn", value: "is-size-4" },
];
var shapes = [
{ code: "default", name: "Mặc định" },
{ code: "is-rounded", name: "Tròn góc" },
];
var shape = undefined;
var outlines = [
{ code: "default", name: "Mặc định" },
{ code: "is-outlined", name: "Outline" },
];
var outline = undefined;
var conditions = [
{ code: "no", name: "Không áp dụng" },
{ code: "yes", name: "Có áp dụng" },
];
var condition = undefined;
var tags = [];
var selected = undefined;
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: "template", name: "Bước 4: Mã lệnh & áp dụng" },
];
var tab = ref(undefined);
var tagsField = [];
var errors = [];
var expression = "";
var text = ref(null);
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) {
if($empty(newVal)) return
elsecheckExpression()
@@ -332,100 +534,98 @@ const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = use
}
}
},*/
function changeTab(v) {
tab.value = v
function changeTab(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() {
text.value = await navigator.clipboard.readText()
if (radioColor.code === "option" ? !$empty(color) : false) {
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() {
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; '
}
if(radioColor.code==='option'? !$empty(color) : false) {
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)
}
}
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>
} 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>
<div class="columns mx-0">
<div class="column is-2">
<Caption class="mb-2" 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()">
<Caption
class="mb-2"
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 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 class="column is-10 py-0 px-0">
<div class="tabs mb-3">
<ul>
<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>
</li>
</ul>
</div>
<div v-if="tab==='datatype'">
<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 class="py-1 border-bottom is-clickable" v-for="x in current.fields">
{{ 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 class="column is-10 py-0 px-0">
<div class="tabs mb-3">
<ul>
<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>
</li>
</ul>
</div>
<div v-if="tab === 'datatype'">
<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
class="py-1 border-bottom is-clickable"
v-for="x in current.fields"
>
{{ 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>
<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>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template>
<script setup>
import { useStore } from '@/stores/index'
const { $getdata, $getapi, $createField, $clone, $getpage, $empty, $copyToClipboard, $find, $multiSort, $download, $getpath } = useNuxtApp()
const store = useStore()
var pagename = 'pagedata3'
var pagedata = ref()
pagedata.value = $getpage()
pagedata.value.perPage = 10
store.commit(pagename, pagedata)
let list = ['LogEntry', 'Permission', 'ContentType', 'Session', 'Group']
var data = (await $getdata('getmodel')).filter(v=>list.findIndex(x=>x===v.model)<0)
data = $multiSort(data, {model: 'asc'})
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]
import { useStore } from "@/stores/index";
const {
$getdata,
$getapi,
$createField,
$clone,
$getpage,
$empty,
$copyToClipboard,
$find,
$multiSort,
$download,
$getpath,
} = useNuxtApp();
const store = useStore();
var pagename = "pagedata3";
var pagedata = ref();
pagedata.value = $getpage();
pagedata.value.perPage = 10;
store.commit(pagename, pagedata);
let list = ["LogEntry", "Permission", "ContentType", "Session", "Group"];
var data = (await $getdata("getmodel")).filter((v) => list.findIndex((x) => x === v.model) < 0);
data = $multiSort(data, { model: "asc" });
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) {
values = undefined
filter = undefined
sort = undefined
current.value = v
if(tab.value==='table') loadData()
values = undefined;
filter = undefined;
sort = undefined;
current.value = v;
if (tab.value === "table") loadData();
}
async function changeTab(v) {
tab.value = v.code
if(v.code==='table') loadData()
tab.value = v.code;
if (v.code === "table") loadData();
}
async function loadData() {
let vfilter = filter? filter.trim() : undefined
if(vfilter) {
let vfilter = filter ? filter.trim() : undefined;
if (vfilter) {
try {
vfilter = JSON.parse(vfilter)
vfilter = JSON.parse(vfilter);
} catch (error) {
alert('Cấu trúc filter lỗi')
vfilter = undefined
alert("Cấu trúc filter có lỗi");
vfilter = undefined;
}
}
let params = {values: $empty(values)? undefined : values.trim(), filter: filter, sort: $empty(sort)? undefined : sort.trim()}
let modelName = current.value.model
let found = {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()
let params = {
values: $empty(values) ? undefined : values.trim(),
filter: filter,
sort: $empty(sort) ? undefined : sort.trim(),
};
let modelName = current.value.model;
let found = {
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
const baseUrl = $getpath() + `${query.value.url}`
apiUrl.value = baseUrl
let vparams = !$empty(values)? {values: values} : null
if(!$empty(filter)) {
vparams = vparams? {values: values, filter:filter} : {filter:filter}
const baseUrl = $getpath() + `${query.value.url}`;
apiUrl.value = baseUrl;
let vparams = !$empty(values) ? { values: values } : null;
if (!$empty(filter)) {
vparams = vparams ? { values: values, filter: filter } : { filter: filter };
}
if(!$empty(sort)) {
if(vparams) {
vparams.sort = sort.trim()
if (!$empty(sort)) {
if (vparams) {
vparams.sort = sort.trim();
} else {
vparams = {sort: sort.trim()}
vparams = { sort: sort.trim() };
}
}
if(vparams) {
if (vparams) {
let url = new URL(baseUrl);
let searchParams = new URLSearchParams(vparams);
url.search = searchParams.toString();
apiUrl.value = baseUrl + url.search
}
apiUrl.value = baseUrl + url.search;
}
}
function showData() {
let arr = []
if(!$empty(values)) {
let arr1 = values.trim().split(',')
arr1.map(v=>{
let val = v.trim()
let field = $createField(val, val, 'string', true)
arr.push(field)
})
let arr = [];
if (!$empty(values)) {
let arr1 = values.trim().split(",");
arr1.map((v) => {
let val = v.trim();
let field = $createField(val, val, "string", true);
arr.push(field);
});
} else {
current.value.fields.map(v=>{
let field = $createField(v.name, v.name, 'string', true)
arr.push(field)
})
current.value.fields.map((v) => {
let field = $createField(v.name, v.name, "string", true);
arr.push(field);
});
}
let clone = $clone(pagedata.value)
clone.fields = arr
clone.data = datatable.value
pagedata.value = undefined
setTimeout(()=>pagedata.value = clone)
let clone = $clone(pagedata.value);
clone.fields = arr;
clone.data = datatable.value;
pagedata.value = undefined;
setTimeout(() => (pagedata.value = clone));
}
function copy() {
$copyToClipboard(JSON.stringify(query.value))
$copyToClipboard(JSON.stringify(query.value));
}
function openModel(x) {
showmodal.value = {component: 'datatable/ModelInfo', title: x.model, width: '70%', height: '600px',
vbind: {data: data, info: $find(data, {model: x.model})}}
showmodal.value = {
component: "datatable/ModelInfo",
title: x.model,
width: "70%",
height: "600px",
vbind: { data: data, info: $find(data, { model: x.model }) },
};
}
function downloadFile() {
$download(`${$getpath()}download/?name=datamodel.png&type=file`, 'datamodel.png')
$download(`${$getpath()}download/?name=datamodel.png&type=file`, "datamodel.png");
}
function findModel() {
if($empty(text)) return displayData.value = data
displayData.value = data.filter(v=>v.model.toLowerCase().indexOf(text.toLowerCase())>=0)
if ($empty(text)) return (displayData.value = data);
displayData.value = data.filter((v) => v.model.toLowerCase().indexOf(text.toLowerCase()) >= 0);
}
</script>
</script>

View File

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

View File

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

View File

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

View File

@@ -1,88 +1,127 @@
<template>
<div>
<template v-if="keys.length>0">
<div class="field is-horizontal" v-for="(v,i) in keys" :key="i">
<div class="field-body">
<div class="field is-narrow">
<div class="control">
<input 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]">
<template v-if="keys.length > 0">
<div
class="field is-horizontal"
v-for="(v, i) in keys"
:key="i"
>
<div class="field-body">
<div class="field is-narrow">
<div class="control">
<input
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 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>
<div class="mb-6" v-else>
<button class="button is-primary has-text-white" @click="addAttr()">Thêm thuộc tính</button>
<div
class="mb-6"
v-else
>
<button
class="button is-primary has-text-white"
@click="addAttr()"
>
Thêm thuộc tính
</button>
</div>
<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>
<Modal @close="comp=undefined" @update="doUpdate"
v-bind="{component: comp, width: '40%', height: '300px', vbind: vbind}" v-if="comp"></Modal>
<Modal
@close="comp = undefined"
@update="doUpdate"
v-bind="{ component: comp, width: '40%', height: '300px', vbind: vbind }"
v-if="comp"
></Modal>
</div>
</template>
<script>
export default {
props: ['field', 'close'],
data() {
return {
keys: [],
values: [],
comp: undefined,
vbind: undefined,
current: undefined
}
</template>
<script>
export default {
props: ["field", "close"],
data() {
return {
keys: [],
values: [],
comp: undefined,
vbind: 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() {
Object.keys(this.field).map(v=>{
this.keys.push(v)
this.values.push(this.field[v])
})
jsonData(v, i) {
this.current = { v: v, i: i };
this.vbind = {
field: this.$empty(this.values[i]) || typeof this.values[i] === "string" ? {} : this.values[i],
close: true,
};
this.comp = "datatable/FieldAttribute";
},
methods: {
doUpdate(v) {
this.values[this.current.i] = v
},
jsonData(v, i) {
this.current = {v: v, i: i}
this.vbind = {field: this.$empty(this.values[i]) || typeof this.values[i] === 'string'? {} : this.values[i], close: true}
this.comp = 'datatable/FieldAttribute'
},
addAttr() {
this.keys.push(undefined)
this.values.push(undefined)
},
remove(i) {
this.$remove(this.keys, i)
this.$remove(this.values, i)
},
update() {
let obj = {}
this.keys.map((v,i)=>{
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>
addAttr() {
this.keys.push(undefined);
this.values.push(undefined);
},
remove(i) {
this.$remove(this.keys, i);
this.$remove(this.values, i);
},
update() {
let obj = {};
this.keys.map((v, i) => {
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>
<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>
<div class="control">
<b-taginput
size="is-small"
size="is-small"
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"
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, 60)}} </span>
</template>
<template slot="empty">
Không trường thỏa mãn
<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>
</template>
<template slot="empty"> Không 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>
<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="mt-2" v-if="tagsField.length>0">
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
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>
<div
class="mt-2"
v-if="tagsField.length > 0"
>
<a
@dblclick="expression = expression ? expression + ' ' + v.name : v.name"
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>
</div>
<div class="field is-horizontal mt-3">
<div class="field-body">
<div 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>
<div class="field is-horizontal mt-3">
<div class="field-body">
<div
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">
<input class="input is-small" 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>
<input
class="input is-small"
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 class="field" v-else>
<label class="label"> Chuỗi tự <span class="has-text-danger"> * </span>
</label>
<div
class="field"
v-else
>
<label class="label"> Chuỗi tự <span class="has-text-danger"> * </span> </label>
<p class="control">
<input
class="input is-small"
@@ -63,27 +99,53 @@
{{ errors.find((v) => v.name === "searchText").msg }}
</p>
</div>
<div class="field is-narrow" v-if="filterType==='color'">
<label class="label fs-14"> màu <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input type="color" v-model="color" @change="changeStyle()">
</p>
<p 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
class="field is-narrow"
v-if="filterType === 'color'"
>
<label class="label fs-14"> màu <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
type="color"
v-model="color"
@change="changeStyle()"
/>
</p>
<p
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>
</template>
<script>
export default {
props: ['filterObj', 'filterType', 'pagename', 'field'],
props: ["filterObj", "filterType", "pagename", "field"],
data() {
return {
tagsField: [],
@@ -92,79 +154,113 @@ export default {
color: undefined,
size: undefined,
errors: [],
searchText: undefined
}
searchText: undefined,
};
},
created() {
this.color = this.filterObj.color
this.size = this.filterObj.size
this.expression = this.filterObj.expression? this.filterObj.expression : this.field.name
if(this.filterObj.tags) {
this.filterObj.tags.map(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))
this.color = this.filterObj.color;
this.size = this.filterObj.size;
this.expression = this.filterObj.expression ? this.filterObj.expression : this.field.name;
if (this.filterObj.tags) {
this.filterObj.tags.map((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));
},
watch: {
expression: function(newVal) {
if(this.$empty(newVal)) return
else this.changeStyle()
}
expression: function (newVal) {
if (this.$empty(newVal)) return;
else this.changeStyle();
},
},
computed: {
colorscheme: {
get: function() {return this.$store.state.colorscheme},
set: function(val) {this.$store.commit("updateColorScheme", {colorscheme: val})}
get: function () {
return this.$store.state.colorscheme;
},
set: function (val) {
this.$store.commit("updateColorScheme", { colorscheme: val });
},
},
pageData: {
get: function() {return this.$store.state[this.pagename]},
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
get: function () {
return this.$store.state[this.pagename];
},
set: function (val) {
this.$store.commit("updateStore", { name: this.pagename, data: val });
},
},
colorchoice: {
get: function() {return this.$store.state.colorchoice},
set: function(val) {this.$store.commit("updateColorChoice", {colorchoice: val})}
}
get: function () {
return this.$store.state.colorchoice;
},
set: function (val) {
this.$store.commit("updateColorChoice", { colorchoice: val });
},
},
},
methods: {
changeStyle() {
let check = this.field.format==='number'? this.checkExpression() : this.checkCondition()
if(!check) return
var row = this.field.format==='number'? {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)
let check = this.field.format === "number" ? this.checkExpression() : this.checkCondition();
if (!check) return;
var row =
this.field.format === "number"
? {
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() {
this.errors = []
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
if(this.filterType==='size' && this.$empty(this.size)) 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
this.errors = [];
if (this.filterType === "color" && this.$empty(this.color))
this.errors.push({ name: "color", message: "Chọn màu" });
if (this.filterType === "size" && this.$empty(this.size))
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() {
this.errors = []
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'})
let val = this.$copy(this.expression)
let exp = this.$copy(this.expression)
this.tagsField.forEach(v => {
let myRegExp = new RegExp(v.name, 'g')
val = val.replace(myRegExp, Math.random())
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])")
})
try {
let value = this.$calc(val)
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ệ'})
} else if(!(eval(value)===true || eval(value)===false)) {
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
this.errors = [];
if (this.filterType === "color" && this.$empty(this.color))
this.errors.push({ name: "color", message: "Chọn màu" });
if (this.filterType === "size" && this.$empty(this.size))
this.errors.push({ name: "size", message: "Nhập cỡ chữ" });
let val = this.$copy(this.expression);
let exp = this.$copy(this.expression);
this.tagsField.forEach((v) => {
let myRegExp = new RegExp(v.name, "g");
val = val.replace(myRegExp, Math.random());
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])");
});
try {
let value = this.$calc(val);
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ệ",
});
} 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) {
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
} catch (err) {
this.errors.push({
name: "expression",
message: "Biểu thức không hợp lệ",
});
}
return this.errors.length>0? false : true
}
}
}
</script>
return this.errors.length > 0 ? false : true;
},
},
};
</script>

View File

@@ -5,7 +5,12 @@
</p>
<div class="tabs is-boxed">
<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>
</li>
</ul>
@@ -14,17 +19,37 @@
<template v-if="tab.code === 'expression' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
<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
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }"
v-bind="{
filterObj: v,
filterType: 'color',
pagename: pagename,
field: openField,
}"
:ref="v.id"
@databack="doConditionFilter($event, 'bgcolor', v.id)"
/>
<p 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">
<p
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
</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
</a>
</p>
@@ -32,29 +57,77 @@
</template>
<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
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }"
v-bind="{
filterObj: v,
filterType: 'color',
pagename: pagename,
field: openField,
}"
:ref="v.id"
@databack="doConditionFilter($event, 'color', v.id)"
/>
<p class="fs-14 mt-1" :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
class="fs-14 mt-1"
: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>
</div>
</template>
<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
v-bind="{ filterObj: v, filterType: 'size', pagename: pagename, field: openField }"
v-bind="{
filterObj: v,
filterType: 'size',
pagename: pagename,
field: openField,
}"
:ref="v.id"
@databack="doConditionFilter($event, 'textsize', v.id)"
/>
<p class="fs-14 mt-1" :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
class="fs-14 mt-1"
: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>
</div>
</template>
@@ -62,21 +135,39 @@
<template v-else-if="tab.code === 'script' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
<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">
<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>
</a>
<a @click="paste()" class="mr-6">
<a
@click="paste()"
class="mr-6"
>
<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>
</a>
</p>
<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>
<p class="mt-5 mx-4">
<span class="icon-text fsb-18">
@@ -87,14 +178,29 @@
<div class="field is-grouped mx-4 mt-4">
<div class="control">
<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 class="control">
<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 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>
<p class="mt-5 pt-2 mx-4">
@@ -103,10 +209,24 @@
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
</span>
</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>
<TableOption v-bind="{ pagename: pagename }" v-else-if="sideBar === 'option'"> </TableOption>
<CreateTemplate v-else-if="sideBar === 'template'" v-bind="{ pagename: pagename, field: openField }">
<TableOption
v-bind="{ pagename: pagename }"
v-else-if="sideBar === 'option'"
>
</TableOption>
<CreateTemplate
v-else-if="sideBar === 'template'"
v-bind="{ pagename: pagename, field: openField }"
>
</CreateTemplate>
</template>
<script setup>

View File

@@ -1,5 +1,8 @@
<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">
Đang mở: <b>{{ $stripHtml(currentsetting.name, 40) }}</b>
</p>
@@ -7,7 +10,11 @@
<div class="field">
<label class="label fs-14">Chọn chế độ lưu <span class="has-text-danger"> * </span></label>
<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">
<SvgIcon
v-bind="{
@@ -22,7 +29,11 @@
<a @click="changeType('new')">
<span class="icon-text">
<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>
Tạo mới
</span>
@@ -33,16 +44,30 @@
<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>
<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 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 }}
</div>
</div>
<div class="field mt-4 px-0 mx-0">
<label class="label fs-14"> Mô tả </label>
<p class="control is-expanded">
<textarea class="textarea" rows="4" v-model="note"></textarea>
<textarea
class="textarea"
rows="4"
v-model="note"
></textarea>
</p>
</div>
<!--
@@ -60,11 +85,19 @@
</div>-->
</template>
<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." }}
</label>
<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>
</div>
</template>
@@ -118,7 +151,10 @@ async function saveSetting() {
let result;
if (radioSave.value === "new") {
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);
} else {

View File

@@ -1,52 +1,114 @@
<template>
<div class="tabs">
<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>
</li>
</ul>
</div>
<template v-if="tab === 'datatype'">
<Caption 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">
<Caption
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 }}
<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>
</template>
<template v-else>
<div class="columns mx-0 mb-0 pb-0">
<div class="column is-7">
<Caption class="mb-2" v-bind="{ title: 'Values', type: 'has-text-warning' }"></Caption>
<input class="input" rows="1" v-model="values" />
<Caption
class="mb-2"
v-bind="{ title: 'Values', type: 'has-text-warning' }"
></Caption>
<input
class="input"
rows="1"
v-model="values"
/>
</div>
<div class="column is-4s">
<Caption class="mb-2" v-bind="{ title: 'Filter', type: 'has-text-warning' }"></Caption>
<input class="input" rows="1" v-model="filter" />
<Caption
class="mb-2"
v-bind="{ title: 'Filter', type: 'has-text-warning' }"
></Caption>
<input
class="input"
rows="1"
v-model="filter"
/>
</div>
<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>
<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>
<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">
{{ query }}
<a class="has-text-primary ml-5" @click="copy()">copy</a>
<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>
<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>
<Caption class="mb-2" v-bind="{ title: 'Data', type: 'has-text-warning' }"></Caption>
<DataTable v-bind="{ pagename: pagename }" v-if="pagedata" />
<Caption
class="mb-2"
v-bind="{ title: 'Data', type: 'has-text-warning' }"
></Caption>
<DataTable
v-bind="{ pagename: pagename }"
v-if="pagedata"
/>
</div>
</template>
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</template>
<script setup>
import { useStore } from "@/stores/index";

View File

@@ -1,26 +1,45 @@
<template>
<div class="tabs is-boxed">
<ul>
<li :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
: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>
</ul>
</div>
<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"
:native-value="v.code">
<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>
<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"
:native-value="v.code"
>
<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>
<template v-else>
<div class="field px-0 mx-0">
<label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label>
<div class="control">
<!--<b-taginput
<template v-else>
<div class="field px-0 mx-0">
<label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label>
<div class="control">
<!--<b-taginput
size="is-small"
v-model="tags"
:data="fields.filter(v=>v.format==='number')"
@@ -39,296 +58,418 @@
Không trường thỏa mãn
</template>
</b-taginput>-->
</div>
<p class="help has-text-danger" 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">
</div>
<p
class="help has-text-danger"
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>
<div class="tags mb-2">
<span @dblclick="formula = formula? (formula + ' ' + v.name) : v.name" class="tag is-dark is-rounded is-clickable"
v-for="v in tags">
{{v.name}}
</span>
<span
@dblclick="formula = formula ? formula + ' ' + v.name : v.name"
class="tag is-dark is-rounded is-clickable"
v-for="v in tags"
>
{{ v.name }}
</span>
</div>
<div class="tags">
<span v-for="(v,i) in operator" :key="i">
<span @dblclick="addOperator(v)" class="tag is-primary is-rounded is-clickable mr-4">
<span class="fs-16">{{v.code}}</span>
<span
v-for="(v, i) in operator"
: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>
</div>
</div>
</template>
<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>
<p class="control">
<textarea class="textarea" rows="3" type="text" :placeholder="placeholder" v-model="formula"> </textarea>
</p>
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='formula')"> {{errors.find(v=>v.name==='formula').message}} </p>
</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"
<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>
<p class="control">
<textarea
class="textarea"
rows="3"
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>
:placeholder="placeholder"
v-model="formula"
>
</textarea>
</p>
<p
class="help has-text-danger"
v-if="errors.find((v) => v.name === 'formula')"
>
{{ errors.find((v) => v.name === "formula").message }}
</p>
</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 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 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 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>
<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>
<script setup>
import { useStore } from '@/stores/index'
import ScrollBox from '~/components/datatable/ScrollBox'
const emit = defineEmits(['modalevent'])
const store = useStore()
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp()
var props = defineProps({
pagename: String,
field: Object,
filters: Object,
filterData: Object,
width: String
})
const moneyunit = store.moneyunit
const datatype = store.datatype
var showmodal = ref()
var pagedata = store[props.pagename]
var selectUnit = moneyunit.find(v=>v.code==='one')
var data = []
var current = 1
var filterData = []
var loading = false
var fieldType = [{code: 'formula', name: 'Tạo công thức'}, {code: 'empty', name: 'Tạo cột rỗng'}]
var errors = []
var tags = []
var formula = undefined
var name = `f${$id().toLocaleLowerCase()}`
var label = undefined
var errors = []
var selectType = fieldType.find(v=>v.code==='empty')
var radioType = ref(datatype.find(v=>v.code==='string'))
var fields = []
var options = undefined
var columns = $copy(pagedata.fields.filter(v=>v.format==='number'))
var decimal = undefined
var choices = [{code: 'column', name: 'Dùng cột dữ liệu'}, {code: 'function', name: 'Dùng hàm số'}]
var choice = 'column'
var funcs = [{code: 'sum', name: 'Sum'}, {code: 'max', name: 'Max'}, {code: 'min', name: 'Min'}, {code: 'avg', name: 'Avg'}]
var func = 'sum'
var placeholder = 'Minh hoạ công thức: f10001 + f10002'
var args = undefined
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'},
{code: '&&', name: 'Và'}, {code: '||', name: 'Hoặc'}, {code: 'iif', name: 'Điều kiện rẽ nhánh'}]
function editLabel() {
if($empty(label)) return
showmodal.value = {component: 'datatable/EditLabel', width: '500px', height: '300px', vbind: {label: label}}
}
function close() {
showmodal.value = null
}
function changeLabel(evt) {
label = evt
showmodal.value = null
}
function changeType(v) {
radioType.value = v
}
function addFunc(v) {
formula = (formula? formula + ' ' : '') + v.name + '(C0: C2)'
}
function addOperator(v) {
let text = v.code==='iif'? 'a>b? c : d' : v.code
formula = `${formula || ''} ${text}`
}
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`
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.'})
import { useStore } from "@/stores/index";
import ScrollBox from "~/components/datatable/ScrollBox";
const emit = defineEmits(["modalevent"]);
const store = useStore();
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp();
var props = defineProps({
pagename: String,
field: Object,
filters: Object,
filterData: Object,
width: String,
});
const moneyunit = store.moneyunit;
const datatype = store.datatype;
var showmodal = ref();
var pagedata = store[props.pagename];
var selectUnit = moneyunit.find((v) => v.code === "one");
var data = [];
var current = 1;
var filterData = [];
var loading = false;
var fieldType = [
{ code: "formula", name: "Tạo công thức" },
{ code: "empty", name: "Tạo cột rỗng" },
];
var errors = [];
var tags = [];
var formula = undefined;
var name = `f${$id().toLocaleLowerCase()}`;
var label = undefined;
var errors = [];
var selectType = fieldType.find((v) => v.code === "empty");
var radioType = ref(datatype.find((v) => v.code === "string"));
var fields = [];
var options = undefined;
var columns = $copy(pagedata.fields.filter((v) => v.format === "number"));
var decimal = undefined;
var choices = [
{ code: "column", name: "Dùng cột dữ liệu" },
{ code: "function", name: "Dùng hàm số" },
];
var choice = "column";
var funcs = [
{ code: "sum", name: "Sum" },
{ code: "max", name: "Max" },
{ code: "min", name: "Min" },
{ code: "avg", name: "Avg" },
];
var func = "sum";
var placeholder = "Minh hoạ công thức: f10001 + f10002";
var args = undefined;
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" },
{ code: "&&", name: "Và" },
{ code: "||", name: "Hoặc" },
{ code: "iif", name: "Điều kiện rẽ nhánh" },
];
function editLabel() {
if ($empty(label)) return;
showmodal.value = {
component: "datatable/EditLabel",
width: "500px",
height: "300px",
vbind: { label: label },
};
}
function close() {
showmodal.value = null;
}
function changeLabel(evt) {
label = evt;
showmodal.value = null;
}
function changeType(v) {
radioType.value = v;
}
function addFunc(v) {
formula = (formula ? formula + " " : "") + v.name + "(C0: C2)";
}
function addOperator(v) {
let text = v.code === "iif" ? "a>b? c : d" : v.code;
formula = `${formula || ""} ${text}`;
}
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`;
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) {
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ệ'})
}
}
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')
} catch (err) {
console.log(err);
errors.push({ name: "formula", message: "Công thức không hợp lệ" });
}
function createEmptyField() {
errors = []
if(!$empty(name)? $empty(name.trim()) : true )
errors.push({name: 'name', message: 'Tên không được bỏ trống.'})
else if(pagedata.fields.find(v=>v.name.toLowerCase()===name.toLowerCase())) {
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 )
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>
} 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() {
errors = [];
if (!$empty(name) ? $empty(name.trim()) : true) errors.push({ name: "name", message: "Tên không được bỏ trống." });
else if (pagedata.fields.find((v) => v.name.toLowerCase() === name.toLowerCase())) {
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)
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>
<nav class="pagination mx-0" role="navigation" aria-label="pagination">
<ul class="pagination-list" v-if="pageInfo">
<li v-for="v in pageInfo">
<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>
<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>
<nav
class="pagination mx-0"
role="navigation"
aria-label="pagination"
>
<ul
class="pagination-list"
v-if="pageInfo"
>
<li v-for="v in pageInfo">
<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
>
<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>
<script setup>
const emit = defineEmits(['changepage'])
var props = defineProps({
data: Array,
perPage: Number
})
var currentPage = 1
var totalRows = props.data.length
var lastPage = parseInt(totalRows / props.perPage)
if(lastPage*props.perPage<totalRows) lastPage += 1
var pageInfo = ref()
function pages(current_page, last_page, onSides = 2) {
// pages
let pages = [];
// Loop through
for (let i = 1; i <= last_page; i++) {
// Define offset
let offset = (i == 1 || last_page) ? onSides + 1 : onSides;
// If added
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) ||
i == current_page || i == last_page) {
pages.push(i);
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
pages.push('...');
}
}
return pages;
const emit = defineEmits(["changepage"]);
var props = defineProps({
data: Array,
perPage: Number,
});
var currentPage = 1;
var totalRows = props.data.length;
var lastPage = parseInt(totalRows / props.perPage);
if (lastPage * props.perPage < totalRows) lastPage += 1;
var pageInfo = ref();
function pages(current_page, last_page, onSides = 2) {
// pages
let pages = [];
// Loop through
for (let i = 1; i <= last_page; i++) {
// Define offset
let offset = i == 1 || last_page ? onSides + 1 : onSides;
// If added
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) || i == current_page || i == last_page) {
pages.push(i);
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
pages.push("...");
}
function changePage(page) {
if(page==='...') return
currentPage = page
pageInfo.value = pages(page, lastPage, 2)
emit('changepage', page)
}
pageInfo.value = pages(1, lastPage, 2)
watch(() => props.data, (newVal, oldVal) => {
totalRows = props.data.length
lastPage = parseInt(totalRows / props.perPage)
if(lastPage*props.perPage<totalRows) lastPage += 1
pageInfo.value = pages(1, lastPage, 2)
})
function previous() {
if(currentPage>1) changePage(currentPage-1)
}
function next() {
if(currentPage<lastPage) changePage(currentPage+1)
}
</script>
}
return pages;
}
function changePage(page) {
if (page === "...") return;
currentPage = page;
pageInfo.value = pages(page, lastPage, 2);
emit("changepage", page);
}
pageInfo.value = pages(1, lastPage, 2);
watch(
() => props.data,
(newVal, oldVal) => {
totalRows = props.data.length;
lastPage = parseInt(totalRows / props.perPage);
if (lastPage * props.perPage < totalRows) lastPage += 1;
pageInfo.value = pages(1, lastPage, 2);
},
);
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>
<div class="px-2" :style="`max-height: ${maxheight}; overflow-y: auto;`">
<div
v-for="(v, i) in rows" :key="i"
:class="[
'field is-grouped py-1 my-0',
i !== rows.length - 1 && 'border-bottom'
]"
<div
class="px-2"
:style="`max-height: ${maxheight}; overflow-y: auto;`"
>
<div
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)">
{{ $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>
<p
class="control is-expanded py-0 fs-14 hyperlink"
@click="doClick(v, i)"
>
{{ $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>
</p>
<p class="control py-0" 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>
<p
class="control py-0"
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>
<span 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
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>
<span class="fs-13 has-text-grey" v-if="show.time">{{$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>
<span
class="fs-13 has-text-grey"
v-if="show.time"
>{{ $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>
<span class="tooltiptext">Mở trong tab mớ</span>
</span>
<span class="tooltip" 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>
<span
class="tooltip"
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>
<span class="tooltiptext">Đổi tên</span>
</span>
<span class="tooltip" 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>
<span
class="tooltip"
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>
<span class="tooltiptext">Xóa</span>
</span>
@@ -47,7 +83,7 @@
</template>
<script>
export default {
props: ['data', 'name', 'maxheight', 'perpage', 'sort', 'selects', 'keyval', 'show', 'notick'],
props: ["data", "name", "maxheight", "perpage", "sort", "selects", "keyval", "show", "notick"],
data() {
return {
currentPage: 1,
@@ -56,62 +92,64 @@ export default {
selected: [],
checked: {},
time: undefined,
array: []
}
array: [],
};
},
created() {
this.getdata()
this.getdata();
},
watch: {
data: function(newVal) {
this.getdata()
data: function (newVal) {
this.getdata();
},
selects: function (newVal) {
this.getSelect();
},
selects: function(newVal) {
this.getSelect()
}
},
methods: {
getdata() {
this.currentPage = 1
this.array = this.$copy(this.data)
if(this.sort!==false) {
let f = {}
let showtime = this.show? this.show.time : false
showtime? f['create_time'] = 'desc' : f[this.name] = 'asc'
this.$multiSort(this.array, f)
this.currentPage = 1;
this.array = this.$copy(this.data);
if (this.sort !== false) {
let f = {};
let showtime = this.show ? this.show.time : false;
showtime ? (f["create_time"] = "desc") : (f[this.name] = "asc");
this.$multiSort(this.array, f);
}
this.rows = this.array.slice(0, this.perpage)
this.getSelect()
this.rows = this.array.slice(0, this.perpage);
this.getSelect();
},
getSelect() {
if(!this.selects) return
this.selected = []
this.checked = {}
this.selects.map(v=>{
let idx = this.rows.findIndex(x=>x[this.keyval? this.keyval : this.name]===v)
if(idx>=0) {
this.selected.push(this.rows[idx])
this.checked[idx] = true
if (!this.selects) return;
this.selected = [];
this.checked = {};
this.selects.map((v) => {
let idx = this.rows.findIndex((x) => x[this.keyval ? this.keyval : this.name] === v);
if (idx >= 0) {
this.selected.push(this.rows[idx]);
this.checked[idx] = true;
}
})
});
},
doClick(v, i, type) {
this.checked[i] = this.checked[i]? false : true
this.checked = this.$copy(this.checked)
let idx = this.selected.findIndex(x=>x.id===v.id)
idx>=0? this.$remove(this.selected) : this.selected.push(v)
this.$emit('selected', v, type)
this.checked[i] = this.checked[i] ? false : true;
this.checked = this.$copy(this.checked);
let idx = this.selected.findIndex((x) => x.id === v.id);
idx >= 0 ? this.$remove(this.selected) : this.selected.push(v);
this.$emit("selected", v, type);
},
handleScroll(e) {
const bottom = e.target.scrollHeight - e.target.scrollTop -5 < e.target.clientHeight
if (bottom) {
if(this.total? this.total>this.rows.length : true) {
this.currentPage +=1
let arr = this.array.filter((ele,index) => (index>=(this.currentPage-1)*this.perpage && index<this.currentPage*this.perpage))
this.rows = this.rows.concat(arr)
const bottom = e.target.scrollHeight - e.target.scrollTop - 5 < e.target.clientHeight;
if (bottom) {
if (this.total ? this.total > this.rows.length : true) {
this.currentPage += 1;
let arr = this.array.filter(
(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>
</thead>
<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>
<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>{{ $stripHtml(v.label, 50) }}</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>
</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>
</a>
<a @click="askConfirm(v, i)">
@@ -29,7 +42,13 @@
</tr>
</tbody>
</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>
<script setup>
import { useStore } from "@/stores/index";

View File

@@ -1,112 +1,144 @@
<template>
<div class="field is-horizontal">
<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-body">
<div class="field">
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input type="color" :value="tablesetting.find(v=>v.code==='table-font-color').detail"
@change="changeSetting($event.target.value, 'table-font-color')">
</p>
<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">
<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 class="field is-horizontal mt-5">
<div class="field-body">
<div class="field">
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input type="color" :value="tablesetting.find(v=>v.code==='header-font-color').detail"
@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 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">
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
type="color"
:value="tablesetting.find((v) => v.code === 'table-font-color').detail"
@change="changeSetting($event.target.value, 'table-font-color')"
/>
</p>
</div>
</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 class="field-body">
<div class="field">
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
<p class="control fs-14">
<input
type="color"
:value="tablesetting.find((v) => v.code === 'header-font-color').detail"
@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>
</template>
<script setup>
import { useStore } from '@/stores/index'
const store = useStore()
var props = defineProps({
pagename: String
})
const { $copy, $clone, $empty } = useNuxtApp()
var pagedata = $clone(store[props.pagename])
var errors = []
var radioNote = 'no'
var tablesetting = pagedata.tablesetting
let found = tablesetting.find(v=>v.code==='note')
if(found? found.detail!=='@' : false) radioNote = 'yes'
function changeSetting(value, code) {
if(code==='note' && $empty(value)) return
let copy = $copy(tablesetting)
let found = copy.find(v=>v.code===code)
if(found) found.detail = value
else {
found = $copy(tablesetting.find(v=>v.code===code))
found.detail = value
copy.push(found)
}
tablesetting = copy
pagedata.tablesetting = tablesetting
store.commit(props.pagename, pagedata)
import { useStore } from "@/stores/index";
const store = useStore();
var props = defineProps({
pagename: String,
});
const { $copy, $clone, $empty } = useNuxtApp();
var pagedata = $clone(store[props.pagename]);
var errors = [];
var radioNote = "no";
var tablesetting = pagedata.tablesetting;
let found = tablesetting.find((v) => v.code === "note");
if (found ? found.detail !== "@" : false) radioNote = "yes";
function changeSetting(value, code) {
if (code === "note" && $empty(value)) return;
let copy = $copy(tablesetting);
let found = copy.find((v) => v.code === code);
if (found) found.detail = value;
else {
found = $copy(tablesetting.find((v) => v.code === code));
found.detail = value;
copy.push(found);
}
</script>
tablesetting = copy;
pagedata.tablesetting = tablesetting;
store.commit(props.pagename, pagedata);
}
</script>

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
<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>
<script setup>
const { $dayjs } = useNuxtApp()
const { $dayjs } = useNuxtApp();
const props = defineProps({
date: String,
color: String
})
</script>
color: String,
});
</script>

View File

@@ -1,10 +1,10 @@
<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>
<script setup>
const { $numtoString } = useNuxtApp()
const { $numtoString } = useNuxtApp();
const props = defineProps({
value: Number,
color: String
})
</script>
color: String,
});
</script>

View File

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

View File

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

View File

@@ -1,185 +1,270 @@
<template>
<div>
<div class="field is-grouped mb-4 border-bottom" v-if="1<0">
<div class="control pl-2" v-if="mode!=='simple'">
<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
class="field is-grouped mb-4 border-bottom"
v-if="1 < 0"
>
<div
class="control pl-2"
v-if="mode !== 'simple'"
>
<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 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 class="fsb-16 hyperlink" @click="type='years'">{{ caption || year }}</span>
<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 class="control pr-2" v-if="mode!=='simple'">
<a class="mr-1" @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
class="control pr-2"
v-if="mode !== 'simple'"
>
<a
class="mr-1"
@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 v-if="type==='days'">
<div :class="`columns mx-0 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
<div class="column px-3 py-1 has-text-centered" v-for="(m,h) in v.dates" :key="h" style="min-height: 100px;"
: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>
<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>
<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
class="column px-3 py-1 has-text-centered"
v-for="(m, h) in v.dates"
:key="h"
style="min-height: 100px"
: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>
<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>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</div>
</template>
<script>
export default {
props: ['date', 'events', 'mode', 'vyear', 'vmonth'],
props: ["date", "events", "mode", "vyear", "vmonth"],
data() {
return {
dates: [],
dateOfWeek: [{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"}],
dateOfWeek: [
{ 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: [],
today: this.$dayjs().format('YYYY/MM/DD'),
today: this.$dayjs().format("YYYY/MM/DD"),
year: undefined,
month: undefined,
type: 'days',
type: "days",
caption: undefined,
action: undefined,
curdate: undefined,
showmodal: undefined,
viewport: 5
}
viewport: 5,
};
},
created() {
this.showDate()
this.showDate();
},
watch: {
date: function(newVal) {
if(newVal) this.showDate()
date: function (newVal) {
if (newVal) this.showDate();
},
vmonth: function(newVal) {
this.showDate()
vmonth: function (newVal) {
this.showDate();
},
events: function (newVal) {
this.showDate();
},
events: function(newVal) {
this.showDate()
}
},
methods: {
async openEvent(event) {
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}}
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 },
};
},
compiledComponent(value) {
return {
template: `${value}`
}
template: `${value}`,
};
},
showDate() {
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined
this.year = this.$dayjs(this.curdate || this.today).year()
this.month = this.$dayjs(this.curdate || this.today).month() + 1
if(this.vyear) this.year = this.$copy(this.vyear)
if(this.vmonth) this.month = this.$copy(this.vmonth)
this.getDates()
this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
this.year = this.$dayjs(this.curdate || this.today).year();
this.month = this.$dayjs(this.curdate || this.today).month() + 1;
if (this.vyear) this.year = this.$copy(this.vyear);
if (this.vmonth) this.month = this.$copy(this.vmonth);
this.getDates();
},
chooseToday() {
this.$emit('date', this.today.replaceAll('/', '-'))
this.year = this.$dayjs(this.today).year()
this.month = this.$dayjs(this.today).month() + 1
this.getDates()
this.$emit("date", this.today.replaceAll("/", "-"));
this.year = this.$dayjs(this.today).year();
this.month = this.$dayjs(this.today).month() + 1;
this.getDates();
},
changeCaption(v) {
this.caption = v
this.caption = v;
},
selectMonth(v) {
this.month = v
this.getDates()
this.type = 'days'
this.month = v;
this.getDates();
this.type = "days";
},
selectYear(v) {
this.year = v
this.getDates()
this.type = 'days'
this.year = v;
this.getDates();
this.type = "days";
},
getDates() {
this.caption = undefined
this.dates = this.allDaysInMonth(this.year, this.month)
this.dates.map(v=>{
let event = this.events? this.$filter(this.events, {isodate: v.date}) : undefined
if(event.length>0) v.event = event
})
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}})
this.weeks.map(v=>{
v.dates = this.dates.filter(x=>x.week===v.week)
})
this.caption = undefined;
this.dates = this.allDaysInMonth(this.year, this.month);
this.dates.map((v) => {
let event = this.events ? this.$filter(this.events, { isodate: v.date }) : undefined;
if (event.length > 0) v.event = event;
});
this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
return { week: v.week };
});
this.weeks.map((v) => {
v.dates = this.dates.filter((x) => x.week === v.week);
});
},
nextMonth() {
let month = this.month + 1
if(month>12) {
month = 1
this.year += 1
let month = this.month + 1;
if (month > 12) {
month = 1;
this.year += 1;
}
this.month = month
this.getDates()
this.month = month;
this.getDates();
},
previousMonth() {
let month = this.month - 1
if(month===0) {
month = 12
this.year -= 1
let month = this.month - 1;
if (month === 0) {
month = 12;
this.year -= 1;
}
this.month = month
this.getDates()
this.month = month;
this.getDates();
},
nextYear() {
if(this.type==='years') return this.action = {name: 'next', id: this.$id()}
this.year += 1
this.getDates()
if (this.type === "years") return (this.action = { name: "next", id: this.$id() });
this.year += 1;
this.getDates();
},
previousYear() {
if(this.type==='years') return this.action = {name: 'previous', id: this.$id()}
this.year -= 1
this.getDates()
if (this.type === "years") return (this.action = { name: "previous", id: this.$id() });
this.year -= 1;
this.getDates();
},
choose(m) {
this.$emit('date', m.date.replaceAll('/', '-'))
this.$emit("date", m.date.replaceAll("/", "-"));
},
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) {
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
let arr = []
days.map(v=>{
for (let i = 0; i < 7; 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 dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD")
let mothCondition = this.$dayjs(date).month() +1
let currentMonth = month
let found = arr.find(x=>x.date===date)
if(!found) {
let dayOfWeek = this.$dayjs(date).day()
let week = this.$dayjs(date).week()
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, mothCondition: mothCondition, currentMonth: currentMonth}
arr.push(ele)
let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
let arr = [];
days.map((v) => {
for (let i = 0; i < 7; 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 dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
let mothCondition = this.$dayjs(date).month() + 1;
let currentMonth = month;
let found = arr.find((x) => x.date === date);
if (!found) {
let dayOfWeek = this.$dayjs(date).day();
let week = this.$dayjs(date).week();
let ele = {
date: date,
week: week,
day: v,
dayOfWeek: dayOfWeek,
dayPrint: dayPrint,
mothCondition: mothCondition,
currentMonth: currentMonth,
};
arr.push(ele);
}
}
})
return arr
}
}
}
</script>
});
return arr;
},
},
};
</script>

View File

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

View File

@@ -1,52 +1,63 @@
<template>
<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>
</div>
<div class="column">
<div class="column">
<div class="mb-4">
<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 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>
<a
class="mr-2"
@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>
<EventDetail v-bind="{events: events, vyear: vyear, vmonth: vmonth}"></EventDetail>
</div>
<EventDetail v-bind="{ events: events, vyear: vyear, vmonth: vmonth }"></EventDetail>
</div>
</div>
</template>
<script setup>
import EventSummary from '@/components/datepicker/EventSummary'
import EventDetail from '@/components/datepicker/EventDetail'
import EventSummary from "@/components/datepicker/EventSummary";
import EventDetail from "@/components/datepicker/EventDetail";
</script>
<script>
export default {
props: ['events', 'year', 'month'],
props: ["events", "year", "month"],
data() {
return {
vyear: this.year? this.$copy(this.year) : undefined,
vmonth: this.month? this.$copy(this.month) : undefined
}
vyear: this.year ? this.$copy(this.year) : undefined,
vmonth: this.month ? this.$copy(this.month) : undefined,
};
},
methods: {
next() {
let month = this.vmonth + 1
if(month>12) {
month = 1
this.vyear += 1
let month = this.vmonth + 1;
if (month > 12) {
month = 1;
this.vyear += 1;
}
this.vmonth = month
this.vmonth = month;
},
previous() {
let month = this.vmonth - 1
if(month===0) {
month = 12
this.vyear -= 1
let month = this.vmonth - 1;
if (month === 0) {
month = 12;
this.vyear -= 1;
}
this.vmonth = month
this.vmonth = month;
},
refresh() {
this.$emit('refresh')
}
}
}
</script>
this.$emit("refresh");
},
},
};
</script>

View File

@@ -1,189 +1,261 @@
<template>
<div>
<div class="field is-grouped mb-4 border-bottom">
<div class="control pl-2" v-if="mode!=='simple'">
<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
class="control pl-2"
v-if="mode !== 'simple'"
>
<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 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 class="fsb-16 hyperlink" @click="mode==='simple'? false : type='PickYear'">{{ caption || year }}</span>
<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 class="control pr-2" v-if="mode!=='simple'">
<a class="mr-1" @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 v-if="type==='days'">
<div class="columns is-mobile mx-0">
<div class="column px-2 py-1 has-text-grey-dark" 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>
<div
class="control pr-2"
v-if="mode !== 'simple'"
>
<a
class="mr-1"
@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>
<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 v-if="type === 'days'">
<div class="columns is-mobile mx-0">
<div
class="column px-2 py-1 has-text-grey-dark"
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>
</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 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>
<script>
export default {
components: {
PickMonth: () => import('@/components/datepicker/PickMonth'),
PickYear: () => import('@/components/datepicker/PickYear')
PickMonth: () => import("@/components/datepicker/PickMonth"),
PickYear: () => import("@/components/datepicker/PickYear"),
},
props: ['date', 'events', 'mode', 'vyear', 'vmonth'],
props: ["date", "events", "mode", "vyear", "vmonth"],
data() {
return {
dates: [],
dateOfWeek: [{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"}],
dateOfWeek: [
{ 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: [],
today: this.$dayjs().format('YYYY/MM/DD'),
today: this.$dayjs().format("YYYY/MM/DD"),
year: undefined,
month: undefined,
type: 'days',
type: "days",
caption: undefined,
action: undefined,
curdate: undefined,
showmodal: undefined
}
showmodal: undefined,
};
},
created() {
this.showDate()
this.showDate();
},
watch: {
date: function(newVal) {
if(newVal) this.showDate()
date: function (newVal) {
if (newVal) this.showDate();
},
events: function (newVal) {
this.showDate();
},
events: function(newVal) {
this.showDate()
}
},
methods: {
showDate() {
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined
this.year = this.$dayjs(this.curdate || this.today).year()
this.month = this.$dayjs(this.curdate || this.today).month() + 1
if(this.vyear) this.year = this.$copy(this.vyear)
if(this.vmonth) this.month = this.$copy(this.vmonth)
this.getDates()
this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
this.year = this.$dayjs(this.curdate || this.today).year();
this.month = this.$dayjs(this.curdate || this.today).month() + 1;
if (this.vyear) this.year = this.$copy(this.vyear);
if (this.vmonth) this.month = this.$copy(this.vmonth);
this.getDates();
},
chooseToday() {
this.$emit('date', this.today.replaceAll('/', '-'))
this.year = this.$dayjs(this.today).year()
this.month = this.$dayjs(this.today).month() + 1
this.getDates()
this.$emit("date", this.today.replaceAll("/", "-"));
this.year = this.$dayjs(this.today).year();
this.month = this.$dayjs(this.today).month() + 1;
this.getDates();
},
changeCaption(v) {
this.caption = v
this.caption = v;
},
selectMonth(v) {
this.month = v
this.getDates()
this.type = 'days'
this.month = v;
this.getDates();
this.type = "days";
},
selectYear(v) {
this.year = v
this.getDates()
this.type = 'days'
this.year = v;
this.getDates();
this.type = "days";
},
getDates() {
this.caption = undefined
this.dates = this.allDaysInMonth(this.year, this.month)
this.dates.map(v=>{
let event = this.events? this.$find(this.events, {isodate: v.date}) : undefined
if(event) v.event = event
})
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}})
this.weeks.map(v=>{
v.dates = this.dates.filter(x=>x.week===v.week)
})
this.caption = undefined;
this.dates = this.allDaysInMonth(this.year, this.month);
this.dates.map((v) => {
let event = this.events ? this.$find(this.events, { isodate: v.date }) : undefined;
if (event) v.event = event;
});
this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
return { week: v.week };
});
this.weeks.map((v) => {
v.dates = this.dates.filter((x) => x.week === v.week);
});
},
nextMonth() {
let month = this.month + 1
if(month>12) {
month = 1
this.year += 1
let month = this.month + 1;
if (month > 12) {
month = 1;
this.year += 1;
}
this.month = month
this.getDates()
this.month = month;
this.getDates();
},
previousMonth() {
let month = this.month - 1
if(month===0) {
month = 12
this.year -= 1
let month = this.month - 1;
if (month === 0) {
month = 12;
this.year -= 1;
}
this.month = month
this.getDates()
this.month = month;
this.getDates();
},
nextYear() {
if(this.type==='PickYear') return this.action = {name: 'next', id: this.$id()}
this.year += 1
this.getDates()
if (this.type === "PickYear") return (this.action = { name: "next", id: this.$id() });
this.year += 1;
this.getDates();
},
previousYear() {
if(this.type==='PickYear') return this.action = {name: 'previous', id: this.$id()}
this.year -= 1
this.getDates()
if (this.type === "PickYear") return (this.action = { name: "previous", id: this.$id() });
this.year -= 1;
this.getDates();
},
choose(m) {
this.$emit('date', m.date.replaceAll('/', '-'))
this.$emit("date", m.date.replaceAll("/", "-"));
},
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) {
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
let arr = []
days.map(v=>{
for (let i = 0; i < 7; 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 dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD")
let mothCondition = this.$dayjs(date).month() +1
let currentMonth = month
let found = arr.find(x=>x.date===date)
if(!found) {
let dayOfWeek = this.$dayjs(date).day()
let week = this.$dayjs(date).week()
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint,
mothCondition: mothCondition, currentMonth: currentMonth}
arr.push(ele)
let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
let arr = [];
days.map((v) => {
for (let i = 0; i < 7; 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 dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
let mothCondition = this.$dayjs(date).month() + 1;
let currentMonth = month;
let found = arr.find((x) => x.date === date);
if (!found) {
let dayOfWeek = this.$dayjs(date).day();
let week = this.$dayjs(date).week();
let ele = {
date: date,
week: week,
day: v,
dayOfWeek: dayOfWeek,
dayPrint: dayPrint,
mothCondition: mothCondition,
currentMonth: currentMonth,
};
arr.push(ele);
}
}
})
return arr
}
}
}
</script>
});
return arr;
},
},
};
</script>

View File

@@ -1,53 +1,91 @@
<template>
<div class="p-2" style="width: 300px">
<div
class="p-2"
style="width: 300px"
>
<div class="field is-grouped">
<div class="control pl-2">
<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>
<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 class="control is-expanded has-text-centered">
<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>
<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 class="control pr-2">
<a class="mr-1" @click="nextMonth()" v-if="type==='days'">
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon>
<a
class="mr-1"
@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>
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
</a>
</div>
</div>
<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
<div
v-for="(m, h) in dateOfWeek"
: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 }}
</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 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}}
<div
:class="`columns is-mobile mx-0 mb-1 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
v-for="(v, i) in weeks"
: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 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"
:class="[
'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-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 }}
@@ -58,133 +96,165 @@
<hr class="my-1" />
<div class="mt-2 fs-14">
<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>
<PickMonth v-else-if="type==='months'" @month="selectMonth"></PickMonth>
<PickYear v-else-if="type==='years'" v-bind="{ year, month, action }" @year="selectYear" @caption="changeCaption"></PickYear>
<PickMonth
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>
</template>
<script setup>
import PickMonth from '@/components/datepicker/PickMonth'
import PickYear from '@/components/datepicker/PickYear'
const { $id, $dayjs, $unique} = useNuxtApp()
const emit = defineEmits(['date'])
var props = defineProps({
date: String,
maxdate: String
})
var dates = []
var dateOfWeek = [{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"}]
var weeks = ref([])
var year= ref(undefined)
var month = undefined
var type = ref('days')
var caption = ref(undefined)
var action = ref(undefined)
var curdate = undefined
var today = new Date()
function showDate() {
curdate = props.date? props.date.replaceAll('-', '/') : undefined
year.value = $dayjs(curdate || today).year()
month = $dayjs(curdate || today).month() + 1
getDates()
}
function chooseToday() {
emit('date', $dayjs(today).format('YYYY-MM-DD') )//today.replaceAll('/', '-'))
year.value = $dayjs(today).year()
month = $dayjs(today).month() + 1
getDates()
}
function changeCaption(v) {
caption.value = v
}
function selectMonth(v) {
month = v
getDates()
type.value = 'days'
}
function selectYear(v) {
year.value = v
getDates()
type.value = 'days'
}
function getDates() {
caption.value = undefined
dates = allDaysInMonth(year.value, month)
weeks.value = $unique(dates, ['week']).map(v=>{return {week: v.week}})
weeks.value.map(v=>{
v.dates = dates.filter(x=>x.week===v.week)
})
}
function nextMonth() {
month = month + 1
if(month>12) {
month = 1
year.value += 1
}
getDates()
}
function previousMonth() {
month = month - 1
if(month===0) {
month = 12
year.value -= 1
}
getDates()
}
function nextYear() {
if(type.value==='years') return action.value = {name: 'next', id: $id()}
year.value += 1
getDates()
}
function previousYear() {
if(type.value==='years') return action.value = {name: 'previous', id: $id()}
year.value -= 1
getDates()
}
function choose(m) {
emit('date', m.date.replaceAll('/', '-'))
}
function createDate(v, x, y) {
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString())
}
function allDaysInMonth(year, month) {
let days = Array.from({length: $dayjs(createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
let arr = []
days.map(v=>{
for (let i = 0; i < 7; i++) {
let thedate = $dayjs(createDate(year, month, v)).weekday(i)
let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD")
let dayPrint = $dayjs(new Date(thedate.$d)).format("DD")
let monthCondition = $dayjs(date).month() +1
let currentMonth = month
let found = arr.find(x=>x.date===date)
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 = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, monthCondition: monthCondition,
currentMonth: currentMonth, disable: disable}
arr.push(ele)
}
import PickMonth from "@/components/datepicker/PickMonth";
import PickYear from "@/components/datepicker/PickYear";
const { $id, $dayjs, $unique } = useNuxtApp();
const emit = defineEmits(["date"]);
var props = defineProps({
date: String,
maxdate: String,
});
var dates = [];
var dateOfWeek = [
{ 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" },
];
var weeks = ref([]);
var year = ref(undefined);
var month = undefined;
var type = ref("days");
var caption = ref(undefined);
var action = ref(undefined);
var curdate = undefined;
var today = new Date();
function showDate() {
curdate = props.date ? props.date.replaceAll("-", "/") : undefined;
year.value = $dayjs(curdate || today).year();
month = $dayjs(curdate || today).month() + 1;
getDates();
}
function chooseToday() {
emit("date", $dayjs(today).format("YYYY-MM-DD")); //today.replaceAll('/', '-'))
year.value = $dayjs(today).year();
month = $dayjs(today).month() + 1;
getDates();
}
function changeCaption(v) {
caption.value = v;
}
function selectMonth(v) {
month = v;
getDates();
type.value = "days";
}
function selectYear(v) {
year.value = v;
getDates();
type.value = "days";
}
function getDates() {
caption.value = undefined;
dates = allDaysInMonth(year.value, month);
weeks.value = $unique(dates, ["week"]).map((v) => {
return { week: v.week };
});
weeks.value.map((v) => {
v.dates = dates.filter((x) => x.week === v.week);
});
}
function nextMonth() {
month = month + 1;
if (month > 12) {
month = 1;
year.value += 1;
}
getDates();
}
function previousMonth() {
month = month - 1;
if (month === 0) {
month = 12;
year.value -= 1;
}
getDates();
}
function nextYear() {
if (type.value === "years") return (action.value = { name: "next", id: $id() });
year.value += 1;
getDates();
}
function previousYear() {
if (type.value === "years") return (action.value = { name: "previous", id: $id() });
year.value -= 1;
getDates();
}
function choose(m) {
emit("date", m.date.replaceAll("/", "-"));
}
function createDate(v, x, y) {
return v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString());
}
function allDaysInMonth(year, month) {
let days = Array.from({ length: $dayjs(createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
let arr = [];
days.map((v) => {
for (let i = 0; i < 7; i++) {
let thedate = $dayjs(createDate(year, month, v)).weekday(i);
let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
let dayPrint = $dayjs(new Date(thedate.$d)).format("DD");
let monthCondition = $dayjs(date).month() + 1;
let currentMonth = month;
let found = arr.find((x) => x.date === date);
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;
}
})
return arr
let ele = {
date: date,
week: week,
day: v,
dayOfWeek: dayOfWeek,
dayPrint: dayPrint,
monthCondition: monthCondition,
currentMonth: currentMonth,
disable: disable,
};
arr.push(ele);
}
}
// display
showDate()
// change date
watch(() => props.date, (newVal, oldVal) => {
showDate()
})
});
return arr;
}
// display
showDate();
// change date
watch(
() => props.date,
(newVal, oldVal) => {
showDate();
},
);
</script>
<style scoped>
a {
color: var(--bulma-link-60);
}
</style>
</style>

View File

@@ -1,14 +1,19 @@
<template>
<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>
</template>
<script>
export default {
data() {
return {
months: [1,2,3,4,5,6,7,8,9,10,11,12]
}
}
}
</script>
months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
};
},
};
</script>

View File

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

View File

@@ -1,9 +1,9 @@
<script setup>
import Template1 from '@/lib/email/templates/Template1.vue';
import { render } from '@vue-email/render';
import { forEachAsync, isEqual } from 'es-toolkit';
import Template1 from "@/lib/email/templates/Template1.vue";
import { render } from "@vue-email/render";
import { forEachAsync, isEqual } from "es-toolkit";
const {
const {
$dayjs,
$getdata,
$insertapi,
@@ -19,9 +19,9 @@ const {
const payables = ref(null);
const defaultFilter = {
status: 1,
to_date__gte: $dayjs().format('YYYY-MM-DD'),
to_date__gte: $dayjs().format("YYYY-MM-DD"),
to_date__lte: undefined,
}
};
const filter = ref(defaultFilter);
const activeDateFilter = ref(null);
const key = ref(0);
@@ -35,35 +35,42 @@ function resetDateFilter() {
}
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;
});
watch(activeDateFilter, (val) => {
if (!val) {
filter.value = defaultFilter;
contents.value = null;
} else {
const cutoffDate = $dayjs().add(val.time, 'day').format('YYYY-MM-DD');
const filterField = `to_date__${val.lookup}`;
filter.value = {
...defaultFilter,
[filterField]: cutoffDate,
watch(
activeDateFilter,
(val) => {
if (!val) {
filter.value = defaultFilter;
contents.value = null;
} else {
const cutoffDate = $dayjs().add(val.time, "day").format("YYYY-MM-DD");
const filterField = `to_date__${val.lookup}`;
filter.value = {
...defaultFilter,
[filterField]: cutoffDate,
};
}
}
}, { deep: true })
},
{ deep: true },
);
const contents = ref(null);
const isSending = ref(false);
function sanitizeContentPayment(text, maxLength = 80) {
if (!text) return '';
if (!text) return "";
return text
.normalize('NFD') // bỏ dấu tiếng Việt
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ
.replace(/\s+/g, ' ')
.normalize("NFD") // bỏ dấu tiếng Việt
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
.replace(/\s+/g, " ")
.trim()
.slice(0, maxLength);
}
@@ -88,19 +95,19 @@ const buildContentPayment = (data) => {
cycle,
} = data;
if (customerType.toLowerCase() === 'cn') {
if (customerType.toLowerCase() === "cn") {
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 {
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 {
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
}
};
function replaceTemplateVars(html, paymentScheduleItem) {
const {
const {
txn_detail__transaction__product__trade_code,
txn_detail__transaction__customer__code,
txn_detail__transaction__customer__fullname,
@@ -112,118 +119,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
from_date,
to_date,
remain_amount,
cycle
cycle,
} = paymentScheduleItem;
return html
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '')
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '')
.replace(/\[year]/g, new Date().getFullYear() || '')
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
.replace(/\[year]/g, new Date().getFullYear() || "")
.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(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
.replace(
/\[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)) : ''}` ||
'',
)
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
.replace(
/\[customer\.legal_code\]/g,
txn_detail__transaction__customer__legal_code || '',
`${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\.contact_address\]/g,
txn_detail__transaction__customer__contact_address ||
txn_detail__transaction__customer__address ||
'',
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
)
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '')
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '')
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '')
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '')
.replace(
/\[payment_schedule\.amount_in_word\]/g,
$numberToVietnameseCurrency(remain_amount) || '',
)
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
.replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
.replace(
/\[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(
/\[payment_schedule\.note\]/g,
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
);
.replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
}
function quillToEmailHtml(html) {
return html
// ALIGN
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
return (
html
// ALIGN
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
// FONT SIZE
.replace(/ql-size-small/g, '')
.replace(/ql-size-large/g, '')
.replace(/ql-size-huge/g, '')
// FONT SIZE
.replace(/ql-size-small/g, "")
.replace(/ql-size-large/g, "")
.replace(/ql-size-huge/g, "")
// REMOVE EMPTY CLASS
.replace(/class=""/g, '')
;
// REMOVE EMPTY CLASS
.replace(/class=""/g, "")
);
}
const showmodal = ref(null);
function openConfirmModal() {
showmodal.value = {
component: 'dialog/Confirm',
title: 'Xác nhận',
width: '500px',
height: '100px',
component: "dialog/Confirm",
title: "Xác nhận",
width: "500px",
height: "100px",
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() {
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(
'payment_schedule',
undefined,
{
filter: filter.value,
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 paymentScheduleData = await $getdata("payment_schedule", undefined, {
filter: filter.value,
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(
'emailtemplate',
{ id: activeDateFilter.value.emailTemplate },
undefined,
true
);
const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
let message = emailTemplate.content.content;
contents.value = paymentScheduleData.map(paymentSchedule => {
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, '');
contents.value = paymentScheduleData.map((paymentSchedule) => {
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
const transfer = {
bank: {
code: 'MB',
name: 'MB Bank',
code: "MB",
name: "MB Bank",
},
account: {
number: '146768686868',
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA',
number: "146768686868",
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);
@@ -242,7 +230,7 @@ async function sendEmails() {
...emailTemplate.content,
content: undefined,
message: replaceTemplateVars(message, paymentSchedule),
}
};
});
await forEachAsync(contents.value, async (bigContent, i) => {
@@ -251,16 +239,16 @@ async function sendEmails() {
content: toRaw(bigContent),
previewMode: true,
};
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
tempEm.content.message = quillToEmailHtml(message);
let emailHtml = await render(Template1, tempEm);
// If no image URL provided, remove image section from HTML
if ((imageUrl ?? '').trim() === '') {
if ((imageUrl ?? "").trim() === "") {
emailHtml = emailHtml
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '')
.replace(/\n\s*\n\s*\n/g, '\n\n');
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
.replace(/\n\s*\n\s*\n/g, "\n\n");
}
// Replace keywords in HTML
@@ -268,55 +256,65 @@ async function sendEmails() {
if (keyword && keyword.length > 0) {
keyword.forEach(({ keyword, value }) => {
if (keyword && value) {
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g');
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
finalEmailHtml = finalEmailHtml.replace(regex, value);
}
});
}
const response = await $insertapi(
'sendemail',
"sendemail",
{
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
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,
false,
);
if (response !== null) {
await $insertapi('productnote', {
ref: paymentScheduleData[i].txn_detail__transaction__product,
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);
await $insertapi(
"productnote",
{
ref: paymentScheduleData[i].txn_detail__transaction__product,
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(() => {
$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;
}, 1000);
}
watch(filter, () => {
key.value += 1;
}, { deep: true })
watch(
filter,
() => {
key.value += 1;
},
{ deep: true },
);
</script>
<template>
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
<div class="buttons m-0">
<p>Đến hạn:</p>
<button
<button
v-for="payable in payables"
:key="payable.id"
@click="setDateFilter(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
v-if="activeDateFilter"
@click="resetDateFilter()"
class="button is-white"
@@ -324,7 +322,7 @@ watch(filter, () => {
Xoá lọc
</button>
</div>
<button
<button
v-if="activeDateFilter"
@click="openConfirmModal()"
:class="['button', 'is-light', { 'is-loading': isSending }]"
@@ -342,16 +340,18 @@ watch(filter, () => {
params: {
filter,
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',
}
}" />
<Modal
v-if="showmodal"
v-bind="showmodal"
@confirm="sendEmails()"
@close="showmodal = undefined"
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',
},
}"
/>
<!-- <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
<Template1
v-if="contents"
@@ -360,4 +360,4 @@ watch(filter, () => {
previewMode
/>
</div> -->
</template>
</template>

View File

@@ -1,9 +1,9 @@
<script setup>
import Template1 from '@/lib/email/templates/Template1.vue';
import { render } from '@vue-email/render';
import { forEachAsync, isEqual } from 'es-toolkit';
import Template1 from "@/lib/email/templates/Template1.vue";
import { render } from "@vue-email/render";
import { forEachAsync, isEqual } from "es-toolkit";
const {
const {
$dayjs,
$getdata,
$insertapi,
@@ -19,8 +19,8 @@ const {
const payables = ref(null);
const defaultFilter = {
status: 1,
to_date__lt: $dayjs().format('YYYY-MM-DD'),
}
to_date__lt: $dayjs().format("YYYY-MM-DD"),
};
const filter = ref(defaultFilter);
const activeDateFilter = ref(null);
const key = ref(0);
@@ -34,36 +34,42 @@ function resetDateFilter() {
}
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;
});
watch(activeDateFilter, (val) => {
if (!val) {
filter.value = defaultFilter;
contents.value = null;
} else {
const cutoffDate = $dayjs().subtract(val.time, 'day').format('YYYY-MM-DD');
const filterField = `to_date__${val.lookup === 'lte' ? 'gt' :
'lte'}`;
filter.value = {
...defaultFilter,
[filterField]: cutoffDate,
watch(
activeDateFilter,
(val) => {
if (!val) {
filter.value = defaultFilter;
contents.value = null;
} else {
const cutoffDate = $dayjs().subtract(val.time, "day").format("YYYY-MM-DD");
const filterField = `to_date__${val.lookup === "lte" ? "gt" : "lte"}`;
filter.value = {
...defaultFilter,
[filterField]: cutoffDate,
};
}
}
}, { deep: true })
},
{ deep: true },
);
const contents = ref(null);
const isSending = ref(false);
function sanitizeContentPayment(text, maxLength = 80) {
if (!text) return '';
if (!text) return "";
return text
.normalize('NFD') // bỏ dấu tiếng Việt
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ
.replace(/\s+/g, ' ')
.normalize("NFD") // bỏ dấu tiếng Việt
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
.replace(/\s+/g, " ")
.trim()
.slice(0, maxLength);
}
@@ -88,19 +94,19 @@ const buildContentPayment = (data) => {
cycle,
} = data;
if (customerType.toLowerCase() === 'cn') {
if (customerType.toLowerCase() === "cn") {
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 {
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 {
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
}
};
function replaceTemplateVars(html, paymentScheduleItem) {
const {
const {
txn_detail__transaction__product__trade_code,
txn_detail__transaction__customer__code,
txn_detail__transaction__customer__fullname,
@@ -112,118 +118,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
from_date,
to_date,
remain_amount,
cycle
cycle,
} = paymentScheduleItem;
return html
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '')
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '')
.replace(/\[year]/g, new Date().getFullYear() || '')
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
.replace(/\[year]/g, new Date().getFullYear() || "")
.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(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
.replace(
/\[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)) : ''}` ||
'',
)
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
.replace(
/\[customer\.legal_code\]/g,
txn_detail__transaction__customer__legal_code || '',
`${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\.contact_address\]/g,
txn_detail__transaction__customer__contact_address ||
txn_detail__transaction__customer__address ||
'',
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
)
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '')
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '')
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '')
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '')
.replace(
/\[payment_schedule\.amount_in_word\]/g,
$numberToVietnameseCurrency(remain_amount) || '',
)
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
.replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
.replace(
/\[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(
/\[payment_schedule\.note\]/g,
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
);
.replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
}
function quillToEmailHtml(html) {
return html
// ALIGN
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
return (
html
// ALIGN
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
// FONT SIZE
.replace(/ql-size-small/g, '')
.replace(/ql-size-large/g, '')
.replace(/ql-size-huge/g, '')
// FONT SIZE
.replace(/ql-size-small/g, "")
.replace(/ql-size-large/g, "")
.replace(/ql-size-huge/g, "")
// REMOVE EMPTY CLASS
.replace(/class=""/g, '')
;
// REMOVE EMPTY CLASS
.replace(/class=""/g, "")
);
}
const showmodal = ref(null);
function openConfirmModal() {
showmodal.value = {
component: 'dialog/Confirm',
title: 'Xác nhận',
width: '500px',
height: '100px',
component: "dialog/Confirm",
title: "Xác nhận",
width: "500px",
height: "100px",
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() {
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(
'payment_schedule',
undefined,
{
filter: filter.value,
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 paymentScheduleData = await $getdata("payment_schedule", undefined, {
filter: filter.value,
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(
'emailtemplate',
{ id: activeDateFilter.value.emailTemplate },
undefined,
true
);
const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
let message = emailTemplate.content.content;
contents.value = paymentScheduleData.map(paymentSchedule => {
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, '');
contents.value = paymentScheduleData.map((paymentSchedule) => {
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
const transfer = {
bank: {
code: 'MB',
name: 'MB Bank',
code: "MB",
name: "MB Bank",
},
account: {
number: '146768686868',
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA',
number: "146768686868",
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);
@@ -242,7 +229,7 @@ async function sendEmails() {
...emailTemplate.content,
content: undefined,
message: replaceTemplateVars(message, paymentSchedule),
}
};
});
await forEachAsync(contents.value, async (bigContent, i) => {
@@ -251,16 +238,16 @@ async function sendEmails() {
content: toRaw(bigContent),
previewMode: true,
};
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
tempEm.content.message = quillToEmailHtml(message);
let emailHtml = await render(Template1, tempEm);
// If no image URL provided, remove image section from HTML
if ((imageUrl ?? '').trim() === '') {
if ((imageUrl ?? "").trim() === "") {
emailHtml = emailHtml
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '')
.replace(/\n\s*\n\s*\n/g, '\n\n');
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
.replace(/\n\s*\n\s*\n/g, "\n\n");
}
// Replace keywords in HTML
@@ -268,55 +255,65 @@ async function sendEmails() {
if (keyword && keyword.length > 0) {
keyword.forEach(({ keyword, value }) => {
if (keyword && value) {
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g');
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
finalEmailHtml = finalEmailHtml.replace(regex, value);
}
});
}
const response = await $insertapi(
'sendemail',
"sendemail",
{
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
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,
false,
);
if (response !== null) {
await $insertapi('productnote', {
ref: paymentScheduleData[i].txn_detail__transaction__product,
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);
await $insertapi(
"productnote",
{
ref: paymentScheduleData[i].txn_detail__transaction__product,
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(() => {
$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;
}, 1000);
}
watch(filter, () => {
key.value += 1;
}, { deep: true })
watch(
filter,
() => {
key.value += 1;
},
{ deep: true },
);
</script>
<template>
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
<div class="buttons m-0">
<p>Quá hạn:</p>
<button
<button
v-for="payable in payables"
:key="payable.id"
@click="setDateFilter(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
v-if="activeDateFilter"
@click="resetDateFilter()"
class="button is-white"
@@ -324,7 +321,7 @@ watch(filter, () => {
Xoá lọc
</button>
</div>
<button
<button
v-if="activeDateFilter"
@click="openConfirmModal()"
:class="['button', 'is-light', { 'is-loading': isSending }]"
@@ -342,16 +339,18 @@ watch(filter, () => {
params: {
filter,
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',
}
}" />
<Modal
v-if="showmodal"
v-bind="showmodal"
@confirm="sendEmails()"
@close="showmodal = undefined"
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',
},
}"
/>
<!-- <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
<Template1
v-if="contents"
@@ -360,4 +359,4 @@ watch(filter, () => {
previewMode
/>
</div> -->
</template>
</template>

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