chore: install prettier
This commit is contained in:
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
**/*.min.js
|
||||||
|
my-bulma-project.css
|
||||||
|
my-bulma-project.css.map
|
||||||
24
.prettierrc
Normal file
24
.prettierrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"objectWrap": "preserve",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"semi": true,
|
||||||
|
"experimentalOperatorPosition": "end",
|
||||||
|
"experimentalTernaries": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleAttributePerLine": true,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"insertPragma": false,
|
||||||
|
"printWidth": 120,
|
||||||
|
"requirePragma": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"embeddedLanguageFormatting": "auto"
|
||||||
|
}
|
||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
@@ -14,9 +14,14 @@
|
|||||||
.blockdiv {
|
.blockdiv {
|
||||||
max-width: 1900px !important;
|
max-width: 1900px !important;
|
||||||
padding: 1rem 2rem 2rem;
|
padding: 1rem 2rem 2rem;
|
||||||
@include mobile { padding: 1rem; }
|
@include mobile {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.columns .column {
|
.columns .column {
|
||||||
@include mobile { padding-left: 0; padding-right: 0; }
|
@include mobile {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,4 @@
|
|||||||
// might break lots of stuff
|
// might break lots of stuff
|
||||||
// .skeleton-block:not(:last-child), .media:not(:last-child), .level:not(:last-child), .fixed-grid:not(:last-child), .grid:not(:last-child), .tabs:not(:last-child), .pagination:not(:last-child), .message:not(:last-child), .card:not(:last-child), .breadcrumb:not(:last-child), .field:not(:last-child), .file:not(:last-child), .title:not(:last-child), .subtitle:not(:last-child), .tags:not(:last-child), .table:not(:last-child), .table-container:not(:last-child), .progress:not(:last-child), .notification:not(:last-child), .content:not(:last-child), .buttons:not(:last-child), .box:not(:last-child), .block:not(:last-child) {
|
// .skeleton-block:not(:last-child), .media:not(:last-child), .level:not(:last-child), .fixed-grid:not(:last-child), .grid:not(:last-child), .tabs:not(:last-child), .pagination:not(:last-child), .message:not(:last-child), .card:not(:last-child), .breadcrumb:not(:last-child), .field:not(:last-child), .file:not(:last-child), .title:not(:last-child), .subtitle:not(:last-child), .tags:not(:last-child), .table:not(:last-child), .table-container:not(:last-child), .progress:not(:last-child), .notification:not(:last-child), .content:not(:last-child), .buttons:not(:last-child), .box:not(:last-child), .block:not(:last-child) {
|
||||||
// margin-bottom: inherit;
|
// margin-bottom: inherit;
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -2,61 +2,66 @@
|
|||||||
|
|
||||||
// Font size loops
|
// Font size loops
|
||||||
@for $i from 10 through 50 {
|
@for $i from 10 through 50 {
|
||||||
.fs-#{$i} { font-size: $i + px; }
|
.fs-#{$i} {
|
||||||
.fsb-#{$i} { font-size: $i + px; font-weight: bold; }
|
font-size: $i + px;
|
||||||
|
}
|
||||||
|
.fsb-#{$i} {
|
||||||
|
font-size: $i + px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-thin {
|
.font-thin {
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
.font-extralight {
|
.font-extralight {
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
}
|
}
|
||||||
.font-light {
|
.font-light {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
.font-normal {
|
.font-normal {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
.font-medium {
|
.font-medium {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.font-semibold {
|
.font-semibold {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.font-bold {
|
.font-bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.font-extrabold {
|
.font-extrabold {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
.font-black {
|
.font-black {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-xs {
|
.rounded-xs {
|
||||||
border-radius: 0.125rem;
|
border-radius: 0.125rem;
|
||||||
}
|
}
|
||||||
.rounded-sm {
|
.rounded-sm {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
.rounded-md {
|
.rounded-md {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
.rounded-lg {
|
.rounded-lg {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
.rounded-xl {
|
.rounded-xl {
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
.rounded-2xl {
|
.rounded-2xl {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
.rounded-3xl {
|
.rounded-3xl {
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
}
|
}
|
||||||
.rounded-4xl {
|
.rounded-4xl {
|
||||||
border-radius: 2rem;
|
border-radius: 2rem;
|
||||||
}
|
}
|
||||||
.rounded-none {
|
.rounded-none {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -83,20 +88,20 @@
|
|||||||
|
|
||||||
// ─── CSS custom properties ─────────────────────────────────────────────────
|
// ─── CSS custom properties ─────────────────────────────────────────────────
|
||||||
:root {
|
:root {
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
--container-3xs: 16rem;
|
--container-3xs: 16rem;
|
||||||
--container-2xs: 18rem;
|
--container-2xs: 18rem;
|
||||||
--container-xs: 20rem;
|
--container-xs: 20rem;
|
||||||
--container-sm: 24rem;
|
--container-sm: 24rem;
|
||||||
--container-md: 28rem;
|
--container-md: 28rem;
|
||||||
--container-lg: 32rem;
|
--container-lg: 32rem;
|
||||||
--container-xl: 36rem;
|
--container-xl: 36rem;
|
||||||
--container-2xl: 42rem;
|
--container-2xl: 42rem;
|
||||||
--container-3xl: 48rem;
|
--container-3xl: 48rem;
|
||||||
--container-4xl: 56rem;
|
--container-4xl: 56rem;
|
||||||
--container-5xl: 64rem;
|
--container-5xl: 64rem;
|
||||||
--container-6xl: 72rem;
|
--container-6xl: 72rem;
|
||||||
--container-7xl: 80rem;
|
--container-7xl: 80rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Shared mixin ──────────────────────────────────────────────────────────
|
// ─── Shared mixin ──────────────────────────────────────────────────────────
|
||||||
@@ -108,9 +113,16 @@
|
|||||||
|
|
||||||
// ─── Class types ───────────────────────────────────────────────────────────
|
// ─── Class types ───────────────────────────────────────────────────────────
|
||||||
$class-types: (
|
$class-types: (
|
||||||
"w": (width),
|
"w": (
|
||||||
"h": (height),
|
width,
|
||||||
"size": (width, height),
|
),
|
||||||
|
"h": (
|
||||||
|
height,
|
||||||
|
),
|
||||||
|
"size": (
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Numeric: w-0 → w-48, h-0 → h-48, size-0 → size-48 ───────────────────
|
// ─── Numeric: w-0 → w-48, h-0 → h-48, size-0 → size-48 ───────────────────
|
||||||
@@ -124,32 +136,110 @@ $class-types: (
|
|||||||
|
|
||||||
// ─── Fractions ─────────────────────────────────────────────────────────────
|
// ─── Fractions ─────────────────────────────────────────────────────────────
|
||||||
$fractions: (
|
$fractions: (
|
||||||
"1\\/2": (1, 2),
|
"1\\/2": (
|
||||||
"1\\/3": (1, 3),
|
1,
|
||||||
"2\\/3": (2, 3),
|
2,
|
||||||
"1\\/4": (1, 4),
|
),
|
||||||
"2\\/4": (2, 4),
|
"1\\/3": (
|
||||||
"3\\/4": (3, 4),
|
1,
|
||||||
"1\\/5": (1, 5),
|
3,
|
||||||
"2\\/5": (2, 5),
|
),
|
||||||
"3\\/5": (3, 5),
|
"2\\/3": (
|
||||||
"4\\/5": (4, 5),
|
2,
|
||||||
"1\\/6": (1, 6),
|
3,
|
||||||
"2\\/6": (2, 6),
|
),
|
||||||
"3\\/6": (3, 6),
|
"1\\/4": (
|
||||||
"4\\/6": (4, 6),
|
1,
|
||||||
"5\\/6": (5, 6),
|
4,
|
||||||
"1\\/12": (1, 12),
|
),
|
||||||
"2\\/12": (2, 12),
|
"2\\/4": (
|
||||||
"3\\/12": (3, 12),
|
2,
|
||||||
"4\\/12": (4, 12),
|
4,
|
||||||
"5\\/12": (5, 12),
|
),
|
||||||
"6\\/12": (6, 12),
|
"3\\/4": (
|
||||||
"7\\/12": (7, 12),
|
3,
|
||||||
"8\\/12": (8, 12),
|
4,
|
||||||
"9\\/12": (9, 12),
|
),
|
||||||
"10\\/12": (10, 12),
|
"1\\/5": (
|
||||||
"11\\/12": (11, 12),
|
1,
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
"2\\/5": (
|
||||||
|
2,
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
"3\\/5": (
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
"4\\/5": (
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
"1\\/6": (
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
"2\\/6": (
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
"3\\/6": (
|
||||||
|
3,
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
"4\\/6": (
|
||||||
|
4,
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
"5\\/6": (
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
"1\\/12": (
|
||||||
|
1,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"2\\/12": (
|
||||||
|
2,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"3\\/12": (
|
||||||
|
3,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"4\\/12": (
|
||||||
|
4,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"5\\/12": (
|
||||||
|
5,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"6\\/12": (
|
||||||
|
6,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"7\\/12": (
|
||||||
|
7,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"8\\/12": (
|
||||||
|
8,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"9\\/12": (
|
||||||
|
9,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"10\\/12": (
|
||||||
|
10,
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
"11\\/12": (
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@each $prefix, $props in $class-types {
|
@each $prefix, $props in $class-types {
|
||||||
@@ -163,13 +253,12 @@ $fractions: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Container sizes (w- only) ─────────────────────────────────────────────
|
// ─── Container sizes (w- only) ─────────────────────────────────────────────
|
||||||
$containers: (
|
$containers: ("3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl");
|
||||||
"3xs", "2xs", "xs", "sm", "md", "lg", "xl",
|
|
||||||
"2xl", "3xl", "4xl", "5xl", "6xl", "7xl"
|
|
||||||
);
|
|
||||||
|
|
||||||
@each $name in $containers {
|
@each $name in $containers {
|
||||||
.w-#{$name} { width: var(--container-#{$name}); }
|
.w-#{$name} {
|
||||||
|
width: var(--container-#{$name});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Shared keywords (auto, px, full, min, max, fit) ───────────────────────
|
// ─── Shared keywords (auto, px, full, min, max, fit) ───────────────────────
|
||||||
@@ -178,9 +267,9 @@ $shared-keywords: (
|
|||||||
"auto": auto,
|
"auto": auto,
|
||||||
"px": 1px,
|
"px": 1px,
|
||||||
"full": 100%,
|
"full": 100%,
|
||||||
"min": min-content,
|
"min": min-content,
|
||||||
"max": max-content,
|
"max": max-content,
|
||||||
"fit": fit-content,
|
"fit": fit-content,
|
||||||
);
|
);
|
||||||
|
|
||||||
@each $prefix, $props in $class-types {
|
@each $prefix, $props in $class-types {
|
||||||
@@ -210,5 +299,9 @@ $viewport-keywords: (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-screen { width: 100vw; }
|
.w-screen {
|
||||||
.h-screen { height: 100vh; }
|
width: 100vw;
|
||||||
|
}
|
||||||
|
.h-screen {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :class="`icon-text fsb-${props.size||17} ${props.type || 'has-text-primary'}`">
|
<span :class="`icon-text fsb-${props.size || 17} ${props.type || 'has-text-primary'}`">
|
||||||
<span>{{ title }}</span>
|
<span>{{ title }}</span>
|
||||||
<SvgIcon id="ignore" v-bind="{name: 'right.svg', type: props.type? props.type.replace('has-text-', '') : null,
|
<SvgIcon
|
||||||
size: (props.size>=30? props.size*0.7 : props.size) || 20, alt: 'Mũi tên chỉ hướng'}"></SvgIcon>
|
id="ignore"
|
||||||
|
v-bind="{
|
||||||
|
name: 'right.svg',
|
||||||
|
type: props.type ? props.type.replace('has-text-', '') : null,
|
||||||
|
size: (props.size >= 30 ? props.size * 0.7 : props.size) || 20,
|
||||||
|
alt: 'Mũi tên chỉ hướng',
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
size: Number,
|
size: Number,
|
||||||
title: String
|
title: String,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="#__nuxt > div">
|
<Teleport to="#__nuxt > div">
|
||||||
<div class="modal is-active" @click="doClick">
|
<div
|
||||||
|
class="modal is-active"
|
||||||
|
@click="doClick"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-background"
|
class="modal-background"
|
||||||
:style="`opacity:${count === 0 ? 0.7 : 0.3} !important;`"
|
:style="`opacity:${count === 0 ? 0.7 : 0.3} !important;`"
|
||||||
@@ -10,14 +13,23 @@
|
|||||||
:id="docid"
|
:id="docid"
|
||||||
:style="`width:${vWidth}; border-radius:16px;`"
|
:style="`width:${vWidth}; border-radius:16px;`"
|
||||||
>
|
>
|
||||||
<header class="modal-card-head my-0 py-2" v-if="title">
|
<header
|
||||||
|
class="modal-card-head my-0 py-2"
|
||||||
|
v-if="title"
|
||||||
|
>
|
||||||
<div style="width: 100%">
|
<div style="width: 100%">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control is-expanded has-text-left">
|
<div class="control is-expanded has-text-left">
|
||||||
<p class="fsb-18 has-text-primary" v-html="title"></p>
|
<p
|
||||||
|
class="fsb-18 has-text-primary"
|
||||||
|
v-html="title"
|
||||||
|
></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="control has-text-right">
|
<div class="control has-text-right">
|
||||||
<button class="delete is-medium" @click="closeModal()"></button>
|
<button
|
||||||
|
class="delete is-medium"
|
||||||
|
@click="closeModal()"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,14 +61,13 @@ const store = useStore();
|
|||||||
const { $id } = useNuxtApp();
|
const { $id } = useNuxtApp();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: String,
|
component: String,
|
||||||
width: String,
|
width: String,
|
||||||
height: String,
|
height: String,
|
||||||
vbind: Object,
|
vbind: Object,
|
||||||
title: String,
|
title: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const componentFiles = import.meta.glob("@/components/**/*.vue");
|
const componentFiles = import.meta.glob("@/components/**/*.vue");
|
||||||
|
|
||||||
const resolvedComponent = shallowRef(null);
|
const resolvedComponent = shallowRef(null);
|
||||||
@@ -68,10 +79,8 @@ function loadDynamicComponent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = `/components/${props.component}.vue`;
|
const fullPath = `/components/${props.component}.vue`;
|
||||||
|
|
||||||
const componentPath = Object.keys(componentFiles).find((path) =>
|
const componentPath = Object.keys(componentFiles).find((path) => path.endsWith(fullPath));
|
||||||
path.endsWith(fullPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (componentPath) {
|
if (componentPath) {
|
||||||
resolvedComponent.value = defineAsyncComponent(componentFiles[componentPath]);
|
resolvedComponent.value = defineAsyncComponent(componentFiles[componentPath]);
|
||||||
@@ -118,7 +127,7 @@ const doClick = function (e) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.documentElement.classList.add('is-clipped');
|
document.documentElement.classList.add("is-clipped");
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape") closeModal();
|
if (e.key === "Escape") closeModal();
|
||||||
});
|
});
|
||||||
@@ -128,6 +137,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
count--;
|
count--;
|
||||||
if (count === 0) document.documentElement.classList.remove('is-clipped');
|
if (count === 0) document.documentElement.classList.remove("is-clipped");
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,49 +1,110 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="field has-addons" :id="docid">
|
<div
|
||||||
|
class="field has-addons"
|
||||||
|
:id="docid"
|
||||||
|
>
|
||||||
<div class="control has-icons-left is-expanded">
|
<div class="control has-icons-left is-expanded">
|
||||||
<div :class="`dropdown ${ pos || ''} ${focused? 'is-active' : ''}`" style="width: 100%;">
|
<div
|
||||||
<div class="dropdown-trigger" style="width: 100%;">
|
:class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`"
|
||||||
<input
|
style="width: 100%"
|
||||||
:disabled="disabled"
|
>
|
||||||
:class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-dark' : ''}`"
|
<div
|
||||||
type="text"
|
class="dropdown-trigger"
|
||||||
@focus="setFocus"
|
style="width: 100%"
|
||||||
@blur="lostFocus"
|
>
|
||||||
@keyup.enter="pressEnter"
|
<input
|
||||||
@keyup="beginSearch"
|
:disabled="disabled"
|
||||||
|
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-dark' : ''}`"
|
||||||
|
type="text"
|
||||||
|
@focus="setFocus"
|
||||||
|
@blur="lostFocus"
|
||||||
|
@keyup.enter="pressEnter"
|
||||||
|
@keyup="beginSearch"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu" style="min-width: 100%" role="menu" @click="doClick()">
|
<div
|
||||||
<div class="dropdown-content px-3" style="min-width: 100%;">
|
class="dropdown-menu"
|
||||||
<p class="has-text-warning" v-if="data.length===0">{{ isVietnamese ? 'Không có giá trị thỏa mãn' : 'No matching values' }}</p>
|
style="min-width: 100%"
|
||||||
<ScrollBox v-bind="{data: data, name: field, fontsize: 14, maxheight: '200px', notick: true}" @selected="choose" v-else></ScrollBox>
|
role="menu"
|
||||||
|
@click="doClick()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown-content px-3"
|
||||||
|
style="min-width: 100%"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="has-text-warning"
|
||||||
|
v-if="data.length === 0"
|
||||||
|
>
|
||||||
|
{{ isVietnamese ? "Không có giá trị thỏa mãn" : "No matching values" }}
|
||||||
|
</p>
|
||||||
|
<ScrollBox
|
||||||
|
v-bind="{
|
||||||
|
data: data,
|
||||||
|
name: field,
|
||||||
|
fontsize: 14,
|
||||||
|
maxheight: '200px',
|
||||||
|
notick: true,
|
||||||
|
}"
|
||||||
|
@selected="choose"
|
||||||
|
v-else
|
||||||
|
></ScrollBox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="icon is-left">
|
<span class="icon is-left">
|
||||||
<SvgIcon v-bind="{name: 'magnify.svg', type: 'gray', size: 22}"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'magnify.svg', type: 'gray', size: 22 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="control" v-if="clearable && value">
|
<div
|
||||||
<button class="button is-primary px-2" @click="clearValue" style="height: 100%" type="button">
|
class="control"
|
||||||
<SvgIcon v-bind="{name: 'close.svg', type: 'white', size: 24}"></SvgIcon>
|
v-if="clearable && value"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-primary px-2"
|
||||||
|
@click="clearValue"
|
||||||
|
style="height: 100%"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'close.svg', type: 'white', size: 24 }"></SvgIcon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control" v-if="viewaddon">
|
<div
|
||||||
<button class="button is-dark px-2" @click="viewInfo()" style="height: 100%" type="button">
|
class="control"
|
||||||
<SvgIcon v-bind="{name: 'view.svg', type: 'white', size: 24}"></SvgIcon>
|
v-if="viewaddon"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-dark px-2"
|
||||||
|
@click="viewInfo()"
|
||||||
|
style="height: 100%"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 24 }"></SvgIcon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control" v-if="addon">
|
<div
|
||||||
<button class="button is-primary px-2" @click="addNew()" style="height: 100%" type="button">
|
class="control"
|
||||||
<SvgIcon v-bind="{name: 'add1.png', type: 'white', size: 24}"></SvgIcon>
|
v-if="addon"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-primary px-2"
|
||||||
|
@click="addNew()"
|
||||||
|
style="height: 100%"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 24 }"></SvgIcon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal @dataevent="dataevent" @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@dataevent="dataevent"
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -51,7 +112,22 @@
|
|||||||
import { useStore } from "@/stores/index";
|
import { useStore } from "@/stores/index";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['api', 'field', 'column', 'first', 'optionid', 'filter', 'addon', 'viewaddon', 'position', 'disabled', 'vdata', 'clearable', 'placeholder', 'searchfield'],
|
props: [
|
||||||
|
"api",
|
||||||
|
"field",
|
||||||
|
"column",
|
||||||
|
"first",
|
||||||
|
"optionid",
|
||||||
|
"filter",
|
||||||
|
"addon",
|
||||||
|
"viewaddon",
|
||||||
|
"position",
|
||||||
|
"disabled",
|
||||||
|
"vdata",
|
||||||
|
"clearable",
|
||||||
|
"placeholder",
|
||||||
|
"searchfield",
|
||||||
|
],
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
return { store };
|
return { store };
|
||||||
@@ -64,7 +140,7 @@ export default {
|
|||||||
value: undefined,
|
value: undefined,
|
||||||
selected: undefined,
|
selected: undefined,
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
params: this.api? this.$findapi(this.api)['params'] : undefined,
|
params: this.api ? this.$findapi(this.api)["params"] : undefined,
|
||||||
orgdata: undefined,
|
orgdata: undefined,
|
||||||
error: false,
|
error: false,
|
||||||
focused: false,
|
focused: false,
|
||||||
@@ -72,142 +148,142 @@ export default {
|
|||||||
count2: 0,
|
count2: 0,
|
||||||
docid: this.$id(),
|
docid: this.$id(),
|
||||||
pos: undefined,
|
pos: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isVietnamese() {
|
isVietnamese() {
|
||||||
return this.store.lang === "vi";
|
return this.store.lang === "vi";
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.getPos()
|
this.getPos();
|
||||||
if(this.vdata) {
|
if (this.vdata) {
|
||||||
this.orgdata = this.$copy(this.vdata)
|
this.orgdata = this.$copy(this.vdata);
|
||||||
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field]))
|
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
|
||||||
}
|
}
|
||||||
if(this.first) {
|
if (this.first) {
|
||||||
this.data = this.orgdata? this.$copy(this.orgdata) : await this.getData()
|
this.data = this.orgdata ? this.$copy(this.orgdata) : await this.getData();
|
||||||
if(this.optionid) {
|
if (this.optionid) {
|
||||||
let f = {}
|
let f = {};
|
||||||
f[this.field] = this.optionid
|
f[this.field] = this.optionid;
|
||||||
if(this.optionid>0) f = {id: this.optionid}
|
if (this.optionid > 0) f = { id: this.optionid };
|
||||||
this.selected = this.$find(this.data, f)
|
this.selected = this.$find(this.data, f);
|
||||||
if(this.selected && this.vdata) {
|
if (this.selected && this.vdata) {
|
||||||
return this.value = this.selected[this.field]
|
return (this.value = this.selected[this.field]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(this.optionid) {
|
} else if (this.optionid) {
|
||||||
this.selected = await this.$getdata(this.api, {id: this.optionid}, undefined, true)
|
this.selected = await this.$getdata(this.api, { id: this.optionid }, undefined, true);
|
||||||
}
|
}
|
||||||
if(this.selected) this.doSelect(this.selected)
|
if (this.selected) this.doSelect(this.selected);
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
optionid: function(newVal) {
|
optionid: function (newVal) {
|
||||||
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid})
|
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
|
||||||
if(this.selected) this.doSelect(this.selected)
|
if (this.selected) this.doSelect(this.selected);
|
||||||
else this.value = undefined
|
else this.value = undefined;
|
||||||
},
|
},
|
||||||
filter: async function(newVal) {
|
filter: async function (newVal) {
|
||||||
this.data = await this.getData()
|
this.data = await this.getData();
|
||||||
},
|
},
|
||||||
vdata: function(newval) {
|
vdata: function (newval) {
|
||||||
if(newval) {
|
if (newval) {
|
||||||
this.orgdata = this.$copy(this.vdata)
|
this.orgdata = this.$copy(this.vdata);
|
||||||
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field]))
|
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
|
||||||
this.data = this.$copy(this.orgdata)
|
this.data = this.$copy(this.orgdata);
|
||||||
this.selected = undefined
|
this.selected = undefined;
|
||||||
this.value = undefined
|
this.value = undefined;
|
||||||
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid})
|
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
|
||||||
if(this.selected) this.doSelect(this.selected)
|
if (this.selected) this.doSelect(this.selected);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
choose(v) {
|
choose(v) {
|
||||||
this.focused = false
|
this.focused = false;
|
||||||
this.count1 = 0
|
this.count1 = 0;
|
||||||
this.count2 = 0
|
this.count2 = 0;
|
||||||
this.doSelect(v)
|
this.doSelect(v);
|
||||||
},
|
},
|
||||||
setFocus() {
|
setFocus() {
|
||||||
this.focused = true
|
this.focused = true;
|
||||||
this.count1 = 0
|
this.count1 = 0;
|
||||||
this.count2 = 0
|
this.count2 = 0;
|
||||||
},
|
},
|
||||||
lostFocus() {
|
lostFocus() {
|
||||||
let self = this
|
let self = this;
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
if(self.focused && self.count1===0) self.focused = false
|
if (self.focused && self.count1 === 0) self.focused = false;
|
||||||
}, 200)
|
}, 200);
|
||||||
},
|
},
|
||||||
pressEnter() {
|
pressEnter() {
|
||||||
if(this.data.length===0) return
|
if (this.data.length === 0) return;
|
||||||
this.choose(this.data[0])
|
this.choose(this.data[0]);
|
||||||
},
|
},
|
||||||
doClick() {
|
doClick() {
|
||||||
this.count1 += 1
|
this.count1 += 1;
|
||||||
},
|
},
|
||||||
doSelect(option) {
|
doSelect(option) {
|
||||||
if(this.$empty(option)) return
|
if (this.$empty(option)) return;
|
||||||
this.$emit('option', option)
|
this.$emit("option", option);
|
||||||
this.$emit('modalevent', {name: 'option', data: option})
|
this.$emit("modalevent", { name: "option", data: option });
|
||||||
this.selected = option
|
this.selected = option;
|
||||||
this.value = this.selected[this.field]
|
this.value = this.selected[this.field];
|
||||||
},
|
},
|
||||||
clearValue() {
|
clearValue() {
|
||||||
this.value = undefined
|
this.value = undefined;
|
||||||
this.selected = undefined
|
this.selected = undefined;
|
||||||
this.$emit('option', null)
|
this.$emit("option", null);
|
||||||
this.$emit('modalevent', {name: 'option', data: null})
|
this.$emit("modalevent", { name: "option", data: null });
|
||||||
},
|
},
|
||||||
findObject(val) {
|
findObject(val) {
|
||||||
let rows = this.$copy(this.orgdata)
|
let rows = this.$copy(this.orgdata);
|
||||||
if(this.$empty(val)) this.data = rows
|
if (this.$empty(val)) this.data = rows;
|
||||||
else {
|
else {
|
||||||
let text = this.$nonAccent(val)
|
let text = this.$nonAccent(val);
|
||||||
this.data = rows.filter(v=>v.search.toLowerCase().indexOf(text.toLowerCase())>=0)
|
this.data = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getData() {
|
async getData() {
|
||||||
this.params.filter = this.filter
|
this.params.filter = this.filter;
|
||||||
let data = await this.$getdata(this.api, undefined, this.params)
|
let data = await this.$getdata(this.api, undefined, this.params);
|
||||||
return data
|
return data;
|
||||||
},
|
},
|
||||||
async getApi(val) {
|
async getApi(val) {
|
||||||
if(this.vdata) return this.findObject(val)
|
if (this.vdata) return this.findObject(val);
|
||||||
let text = val? val.toLowerCase() : ''
|
let text = val ? val.toLowerCase() : "";
|
||||||
let f = {}
|
let f = {};
|
||||||
|
|
||||||
// Sử dụng searchfield nếu có, nếu không thì dùng column
|
// Sử dụng searchfield nếu có, nếu không thì dùng column
|
||||||
const fieldsToSearch = this.searchfield || this.column;
|
const fieldsToSearch = this.searchfield || this.column;
|
||||||
|
|
||||||
fieldsToSearch.map(v=>{
|
fieldsToSearch.map((v) => {
|
||||||
f[`${v}__icontains`] = text
|
f[`${v}__icontains`] = text;
|
||||||
})
|
});
|
||||||
this.params.filter_or = f
|
this.params.filter_or = f;
|
||||||
if(this.filter) this.params.filter = this.$copy(this.filter)
|
if (this.filter) this.params.filter = this.$copy(this.filter);
|
||||||
let arr = await this.$getdata(this.api, undefined, this.params)
|
let arr = await this.$getdata(this.api, undefined, this.params);
|
||||||
this.data = this.$copy(arr)
|
this.data = this.$copy(arr);
|
||||||
},
|
},
|
||||||
beginSearch(e) {
|
beginSearch(e) {
|
||||||
let val = e.target.value
|
let val = e.target.value;
|
||||||
this.search = val
|
this.search = val;
|
||||||
if (this.timer) clearTimeout(this.timer)
|
if (this.timer) clearTimeout(this.timer);
|
||||||
this.timer = setTimeout(() => this.getApi(val), 150)
|
this.timer = setTimeout(() => this.getApi(val), 150);
|
||||||
},
|
},
|
||||||
addNew() {
|
addNew() {
|
||||||
this.showmodal = this.$copy(this.addon)
|
this.showmodal = this.$copy(this.addon);
|
||||||
},
|
},
|
||||||
dataevent(v) {
|
dataevent(v) {
|
||||||
console.log("SearchBox received dataevent:", v); // Debug log
|
console.log("SearchBox received dataevent:", v); // Debug log
|
||||||
|
|
||||||
if (!v || !v.id) {
|
if (!v || !v.id) {
|
||||||
console.error("Invalid data received in SearchBox:", v);
|
console.error("Invalid data received in SearchBox:", v);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tìm và cập nhật trong danh sách
|
// Tìm và cập nhật trong danh sách
|
||||||
let idx = this.$findIndex(this.data, {id: v.id})
|
let idx = this.$findIndex(this.data, { id: v.id });
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
// Nếu chưa có trong danh sách, thêm vào đầu
|
// Nếu chưa có trong danh sách, thêm vào đầu
|
||||||
this.data.unshift(v);
|
this.data.unshift(v);
|
||||||
@@ -217,10 +293,10 @@ export default {
|
|||||||
this.data[idx] = v;
|
this.data[idx] = v;
|
||||||
console.log("Updated existing item in data:", v);
|
console.log("Updated existing item in data:", v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cập nhật orgdata nếu có
|
// Cập nhật orgdata nếu có
|
||||||
if (this.orgdata) {
|
if (this.orgdata) {
|
||||||
let orgIdx = this.$findIndex(this.orgdata, {id: v.id});
|
let orgIdx = this.$findIndex(this.orgdata, { id: v.id });
|
||||||
if (orgIdx < 0) {
|
if (orgIdx < 0) {
|
||||||
this.orgdata.unshift(v);
|
this.orgdata.unshift(v);
|
||||||
// Thêm search field cho orgdata
|
// Thêm search field cho orgdata
|
||||||
@@ -234,42 +310,46 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// **Tự động select item vừa tạo/cập nhật**
|
// **Tự động select item vừa tạo/cập nhật**
|
||||||
this.doSelect(v);
|
this.doSelect(v);
|
||||||
|
|
||||||
// Đóng modal
|
// Đóng modal
|
||||||
this.showmodal = undefined;
|
this.showmodal = undefined;
|
||||||
|
|
||||||
console.log("SearchBox data after update:", this.data);
|
console.log("SearchBox data after update:", this.data);
|
||||||
},
|
},
|
||||||
viewInfo() {
|
viewInfo() {
|
||||||
if(!this.selected) return this.$dialog(this.isVietnamese ? 'Vui lòng lựa chọn trước khi xem thông tin.' : 'Please select before viewing', this.isVietnamese ? 'Thông báo' : 'Notice')
|
if (!this.selected)
|
||||||
let copy = this.$copy(this.viewaddon)
|
return this.$dialog(
|
||||||
copy.vbind = {row: this.selected}
|
this.isVietnamese ? "Vui lòng lựa chọn trước khi xem thông tin." : "Please select before viewing",
|
||||||
this.showmodal = copy
|
this.isVietnamese ? "Thông báo" : "Notice",
|
||||||
|
);
|
||||||
|
let copy = this.$copy(this.viewaddon);
|
||||||
|
copy.vbind = { row: this.selected };
|
||||||
|
this.showmodal = copy;
|
||||||
},
|
},
|
||||||
getPos() {
|
getPos() {
|
||||||
switch(this.position) {
|
switch (this.position) {
|
||||||
case 'is-top-left':
|
case "is-top-left":
|
||||||
this.pos = 'is-up is-left'
|
this.pos = "is-up is-left";
|
||||||
break;
|
break;
|
||||||
case 'is-top-right':
|
case "is-top-right":
|
||||||
this.pos = 'is-up is-right'
|
this.pos = "is-up is-right";
|
||||||
break;
|
break;
|
||||||
case 'is-bottom-left':
|
case "is-bottom-left":
|
||||||
this.pos = 'is-right'
|
this.pos = "is-right";
|
||||||
break;
|
break;
|
||||||
case 'is-bottom-right':
|
case "is-bottom-right":
|
||||||
this.pos = 'is-right'
|
this.pos = "is-right";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.field:not(:last-child) {
|
.field:not(:last-child) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* primary: $blue-dianne (#204853) */
|
/* primary: $blue-dianne (#204853) */
|
||||||
.svg-primary {
|
.svg-primary {
|
||||||
filter: invert(19%) sepia(18%) saturate(1514%) hue-rotate(151deg) brightness(97%) contrast(85%);
|
filter: invert(19%) sepia(18%) saturate(1514%) hue-rotate(151deg) brightness(97%) contrast(85%);
|
||||||
@@ -31,7 +30,7 @@ export default {
|
|||||||
|
|
||||||
/* findata/info/warning: $sirocco (#758385) */
|
/* findata/info/warning: $sirocco (#758385) */
|
||||||
/* Cả ba đều dùng chung bộ lọc này */
|
/* Cả ba đều dùng chung bộ lọc này */
|
||||||
.svg-findata,
|
.svg-findata,
|
||||||
.svg-info,
|
.svg-info,
|
||||||
.svg-warning {
|
.svg-warning {
|
||||||
filter: invert(56%) sepia(10%) saturate(301%) hue-rotate(167deg) brightness(92%) contrast(82%);
|
filter: invert(56%) sepia(10%) saturate(301%) hue-rotate(167deg) brightness(92%) contrast(82%);
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<span v-html="props.html"></span>
|
<span v-html="props.html"></span>
|
||||||
<span class="tooltiptext" v-html="props.tooltip"></span>
|
<span
|
||||||
|
class="tooltiptext"
|
||||||
|
v-html="props.tooltip"
|
||||||
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from "vue";
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
html: String,
|
html: String,
|
||||||
tooltip: String,
|
tooltip: String,
|
||||||
width: Number
|
width: Number,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="navbar has-shadow sticky px-3" style="top: 0" role="navigation">
|
<nav
|
||||||
|
class="navbar has-shadow sticky px-3"
|
||||||
|
style="top: 0"
|
||||||
|
role="navigation"
|
||||||
|
>
|
||||||
<div class="navbar-brand mr-5">
|
<div class="navbar-brand mr-5">
|
||||||
<span class="navbar-item is-gap-1">
|
<span class="navbar-item is-gap-1">
|
||||||
<div style="width: 16px; height: 16px" class="has-background-primary rounded-full"></div>
|
<div class="size-4 has-background-primary rounded-full"></div>
|
||||||
<span class="fs-17 font-semibold has-text-primary">{{ $dayjs().format('DD/MM') }}</span>
|
<span class="fs-17 font-semibold has-text-primary">{{ $dayjs().format("DD/MM") }}</span>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
class="navbar-item p-0 has-text-primary"
|
class="navbar-item p-0 has-text-primary"
|
||||||
@click="changeTab(leftmenu[0])"
|
@click="changeTab(leftmenu[0])"
|
||||||
>
|
>
|
||||||
<svg style="max-height: none; width: 44px" width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M40.5 6C59.0015 6 74 20.9985 74 39.5C74 58.0015 59.0015 73 40.5 73C21.9985 73 7 58.0015 7 39.5C7 20.9985 21.9985 6 40.5 6ZM17.9834 29.0654V48.2373H30.9395V44.8955H22.0371V40.3174H30.2373V36.9756H22.0371V32.4072H30.9014V29.0654H17.9834ZM33.8604 48.2373H37.9141V41.4404H40.873L44.5039 48.2373H48.9785L44.9092 40.7852C44.9677 40.7598 45.0277 40.7378 45.085 40.7109C46.1269 40.2242 46.9225 39.5252 47.4717 38.6143C48.0209 37.6969 48.2959 36.6012 48.2959 35.3281C48.2959 34.0613 48.0244 32.9595 47.4814 32.0234C46.9448 31.0812 46.1613 30.3545 45.1318 29.8428C44.1085 29.3248 42.8725 29.0655 41.4248 29.0654H33.8604V48.2373ZM50.8965 48.2373H54.9492V42.0215H58.3574C59.83 42.0214 61.0843 41.7499 62.1201 41.207C63.1623 40.6641 63.9577 39.9052 64.5068 38.9316C65.0559 37.9582 65.331 36.8354 65.3311 35.5625C65.3311 34.2895 65.0595 33.1659 64.5166 32.1924C63.9799 31.2127 63.2001 30.4476 62.1768 29.8984C61.1533 29.343 59.914 29.0654 58.46 29.0654H50.8965V48.2373ZM57.6826 32.3789C58.4689 32.3789 59.1182 32.5139 59.6299 32.7822C60.1416 33.0443 60.5228 33.4151 60.7725 33.8955C61.0283 34.3698 61.1562 34.9259 61.1562 35.5625C61.1562 36.1925 61.0281 36.7507 60.7725 37.2373C60.5228 37.7178 60.1416 38.0955 59.6299 38.3701C59.1245 38.6384 58.482 38.7734 57.7021 38.7734H54.9492V32.3789H57.6826ZM40.6475 32.3789C41.4274 32.3789 42.0733 32.4948 42.585 32.7256C43.1028 32.9502 43.4867 33.2811 43.7363 33.7178C43.9922 34.1546 44.1201 34.6916 44.1201 35.3281C44.1201 35.9584 43.9922 36.4858 43.7363 36.9102C43.4867 37.3344 43.1064 37.6531 42.5947 37.8652C42.083 38.0774 41.4399 38.1836 40.666 38.1836H37.9141V32.3789H40.6475Z" fill="currentColor"/>
|
style="max-height: none; width: 44px"
|
||||||
</svg>
|
width="80"
|
||||||
|
height="80"
|
||||||
|
viewBox="0 0 80 80"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M40.5 6C59.0015 6 74 20.9985 74 39.5C74 58.0015 59.0015 73 40.5 73C21.9985 73 7 58.0015 7 39.5C7 20.9985 21.9985 6 40.5 6ZM17.9834 29.0654V48.2373H30.9395V44.8955H22.0371V40.3174H30.2373V36.9756H22.0371V32.4072H30.9014V29.0654H17.9834ZM33.8604 48.2373H37.9141V41.4404H40.873L44.5039 48.2373H48.9785L44.9092 40.7852C44.9677 40.7598 45.0277 40.7378 45.085 40.7109C46.1269 40.2242 46.9225 39.5252 47.4717 38.6143C48.0209 37.6969 48.2959 36.6012 48.2959 35.3281C48.2959 34.0613 48.0244 32.9595 47.4814 32.0234C46.9448 31.0812 46.1613 30.3545 45.1318 29.8428C44.1085 29.3248 42.8725 29.0655 41.4248 29.0654H33.8604V48.2373ZM50.8965 48.2373H54.9492V42.0215H58.3574C59.83 42.0214 61.0843 41.7499 62.1201 41.207C63.1623 40.6641 63.9577 39.9052 64.5068 38.9316C65.0559 37.9582 65.331 36.8354 65.3311 35.5625C65.3311 34.2895 65.0595 33.1659 64.5166 32.1924C63.9799 31.2127 63.2001 30.4476 62.1768 29.8984C61.1533 29.343 59.914 29.0654 58.46 29.0654H50.8965V48.2373ZM57.6826 32.3789C58.4689 32.3789 59.1182 32.5139 59.6299 32.7822C60.1416 33.0443 60.5228 33.4151 60.7725 33.8955C61.0283 34.3698 61.1562 34.9259 61.1562 35.5625C61.1562 36.1925 61.0281 36.7507 60.7725 37.2373C60.5228 37.7178 60.1416 38.0955 59.6299 38.3701C59.1245 38.6384 58.482 38.7734 57.7021 38.7734H54.9492V32.3789H57.6826ZM40.6475 32.3789C41.4274 32.3789 42.0733 32.4948 42.585 32.7256C43.1028 32.9502 43.4867 33.2811 43.7363 33.7178C43.9922 34.1546 44.1201 34.6916 44.1201 35.3281C44.1201 35.9584 43.9922 36.4858 43.7363 36.9102C43.4867 37.3344 43.1064 37.6531 42.5947 37.8652C42.083 38.0774 41.4399 38.1836 40.666 38.1836H37.9141V32.3789H40.6475Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
@@ -28,30 +42,45 @@
|
|||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-menu" id="navMenu">
|
<div
|
||||||
<div class="navbar-start is-gap-1 is-align-items-center" >
|
class="navbar-menu"
|
||||||
<template v-for="(v, i) in leftmenu" :key="i" :id="v.code">
|
id="navMenu"
|
||||||
<a class="navbar-item rounded-lg is-clipped p-0" v-if="!v.submenu" @click="changeTab(v)">
|
>
|
||||||
<span :class="[
|
<div class="navbar-start is-gap-1 is-align-items-center">
|
||||||
'px-2 py-2 font-medium',
|
<template
|
||||||
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
|
v-for="(v, i) in leftmenu"
|
||||||
|
:key="i"
|
||||||
|
:id="v.code"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="!v.submenu"
|
||||||
|
:class="[
|
||||||
|
'navbar-item rounded-lg is-clipped font-medium',
|
||||||
|
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30',
|
||||||
]"
|
]"
|
||||||
|
style="font-size: 13.5px"
|
||||||
|
@click="changeTab(v)"
|
||||||
|
>
|
||||||
|
{{ v[lang] }}
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="navbar-item rounded-lg has-dropdown is-hoverable"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:class="[
|
||||||
|
'navbar-link rounded-lg is-arrowless font-medium',
|
||||||
|
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30',
|
||||||
|
]"
|
||||||
|
@click="changeTab(v)"
|
||||||
style="font-size: 13.5px"
|
style="font-size: 13.5px"
|
||||||
>
|
>
|
||||||
{{ v[lang] }}
|
<p class="is-flex is-align-items-center">
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<div class="navbar-item rounded-lg has-dropdown is-hoverable" v-else>
|
|
||||||
<a class="navbar-link rounded-lg p-0" @click="changeTab(v)">
|
|
||||||
<p
|
|
||||||
:class="[
|
|
||||||
'px-2 py-2 rounded-lg font-medium is-flex is-align-items-center',
|
|
||||||
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
|
|
||||||
]"
|
|
||||||
style="font-size: 13.5px"
|
|
||||||
>
|
|
||||||
<span>{{ v[lang] }}</span>
|
<span>{{ v[lang] }}</span>
|
||||||
<Icon name="material-symbols:keyboard-arrow-down-rounded" :size="20" />
|
<Icon
|
||||||
|
name="material-symbols:keyboard-arrow-down-rounded"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
@@ -76,19 +105,23 @@
|
|||||||
</a>
|
</a>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<a class="navbar-item">
|
<a class="navbar-item is-flex is-gap-2 is-justify-content-space-between is-align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="fs-13">Xin chào,</p>
|
<p class="fs-13">Xin chào,</p>
|
||||||
<p class="fs-14 font-bold">Quản lý</p>
|
<p class="fs-14 font-bold">Quản lý</p>
|
||||||
</div>
|
</div>
|
||||||
<Avatarbox text="Q" type="findata" size="two" />
|
<Avatarbox
|
||||||
|
text="Q"
|
||||||
|
type="findata"
|
||||||
|
size="two"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Avatarbox from '@/components/common/Avatarbox.vue';
|
import Avatarbox from "@/components/common/Avatarbox.vue";
|
||||||
import { watch } from "vue";
|
import { watch } from "vue";
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -99,229 +132,229 @@ const lang = ref($store.lang);
|
|||||||
const menu = [
|
const menu = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'dashboard',
|
code: "dashboard",
|
||||||
vi: 'Dashboard',
|
vi: "Dashboard",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Dashboard',
|
base: "Dashboard",
|
||||||
component: 'DashboardMaster',
|
component: "DashboardMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'orders',
|
code: "orders",
|
||||||
vi: 'Đơn hàng',
|
vi: "Đơn hàng",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Orders',
|
base: "Orders",
|
||||||
component: 'OrdersMaster',
|
component: "OrdersMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'inventory',
|
code: "inventory",
|
||||||
vi: 'Tồn kho',
|
vi: "Tồn kho",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Inventory',
|
base: "Inventory",
|
||||||
component: 'InventoryMaster',
|
component: "InventoryMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'rights',
|
code: "rights",
|
||||||
vi: 'Phân quyền',
|
vi: "Phân quyền",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Rights',
|
base: "Rights",
|
||||||
component: 'RightsMaster',
|
component: "RightsMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'POS',
|
code: "POS",
|
||||||
vi: 'POS',
|
vi: "POS",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'POS',
|
base: "POS",
|
||||||
component: 'POSMaster',
|
component: "POSMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'receipts',
|
code: "receipts",
|
||||||
vi: 'Hoá đơn',
|
vi: "Hoá đơn",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Receipts',
|
base: "Receipts",
|
||||||
component: 'ReceiptsMaster',
|
component: "ReceiptsMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'imports',
|
code: "imports",
|
||||||
vi: 'Nhập hàng',
|
vi: "Nhập hàng",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Imports',
|
base: "Imports",
|
||||||
component: 'ImportsMaster',
|
component: "ImportsMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'exports',
|
code: "exports",
|
||||||
vi: 'Xuất hàng',
|
vi: "Xuất hàng",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Exports',
|
base: "Exports",
|
||||||
component: 'ExportsMaster',
|
component: "ExportsMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'inventory-transfer',
|
code: "inventory-transfer",
|
||||||
vi: 'Chuyển kho',
|
vi: "Chuyển kho",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'InventoryTransfer',
|
base: "InventoryTransfer",
|
||||||
component: 'InventoryTransferMaster',
|
component: "InventoryTransferMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'inventory-count',
|
code: "inventory-count",
|
||||||
vi: 'Kiểm kho',
|
vi: "Kiểm kho",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'InventoryCount',
|
base: "InventoryCount",
|
||||||
component: 'InventoryCountMaster',
|
component: "InventoryCountMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'cash-book',
|
code: "cash-book",
|
||||||
vi: 'Sổ quỹ',
|
vi: "Sổ quỹ",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'CashBook',
|
base: "CashBook",
|
||||||
component: 'CashBookMaster',
|
component: "CashBookMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'topmenu',
|
category: "topmenu",
|
||||||
classify: 'left',
|
classify: "left",
|
||||||
code: 'report',
|
code: "report",
|
||||||
vi: 'Báo cáo',
|
vi: "Báo cáo",
|
||||||
link: null,
|
link: null,
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'submenu',
|
category: "submenu",
|
||||||
classify: 'report',
|
classify: "report",
|
||||||
code: 'ncc',
|
code: "ncc",
|
||||||
vi: 'NCC',
|
vi: "NCC",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'NCC',
|
base: "NCC",
|
||||||
component: 'NCCMaster',
|
component: "NCCMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'submenu',
|
category: "submenu",
|
||||||
classify: 'report',
|
classify: "report",
|
||||||
code: 'customers',
|
code: "customers",
|
||||||
vi: 'Khách hàng',
|
vi: "Khách hàng",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Customers',
|
base: "Customers",
|
||||||
component: 'CustomersMaster',
|
component: "CustomersMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'submenu',
|
category: "submenu",
|
||||||
classify: 'report',
|
classify: "report",
|
||||||
code: 'goods',
|
code: "goods",
|
||||||
vi: 'Hàng hoá',
|
vi: "Hàng hoá",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Goods',
|
base: "Goods",
|
||||||
component: 'GoodsMaster',
|
component: "GoodsMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'submenu',
|
category: "submenu",
|
||||||
classify: 'report',
|
classify: "report",
|
||||||
code: 'report-cash-book',
|
code: "report-cash-book",
|
||||||
vi: 'Sổ quỹ',
|
vi: "Sổ quỹ",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'ReportCashBook',
|
base: "ReportCashBook",
|
||||||
component: 'ReportCashBookMaster',
|
component: "ReportCashBookMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
category: 'submenu',
|
category: "submenu",
|
||||||
classify: 'report',
|
classify: "report",
|
||||||
code: 'finance',
|
code: "finance",
|
||||||
vi: 'Tài chính',
|
vi: "Tài chính",
|
||||||
link: null,
|
link: null,
|
||||||
detail: {
|
detail: {
|
||||||
base: 'Finance',
|
base: "Finance",
|
||||||
component: 'FinanceMaster',
|
component: "FinanceMaster",
|
||||||
},
|
},
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
// if($store.rights.length>0) {
|
// if($store.rights.length>0) {
|
||||||
// menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
|
// menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
|
||||||
// }
|
// }
|
||||||
if(menu.length===0) {
|
if (menu.length === 0) {
|
||||||
$snackbar($store.lang==='vi'? 'Bạn không có quyền truy cập' : 'You do not have permission to access.')
|
$snackbar($store.lang === "vi" ? "Bạn không có quyền truy cập" : "You do not have permission to access.");
|
||||||
}
|
}
|
||||||
// menu.map(v=>{
|
// menu.map(v=>{
|
||||||
// let arr = $filter($store.common, {category: 'submenu', classify: v.code})
|
// let arr = $filter($store.common, {category: 'submenu', classify: v.code})
|
||||||
@@ -330,8 +363,8 @@ if(menu.length===0) {
|
|||||||
// }
|
// }
|
||||||
// v.submenu = arr.length>0? arr : null
|
// v.submenu = arr.length>0? arr : null
|
||||||
// })
|
// })
|
||||||
var leftmenu = $filter(menu, {category: 'topmenu', classify: 'left'})
|
var leftmenu = $filter(menu, { category: "topmenu", classify: "left" });
|
||||||
var currentTab = ref(leftmenu.length>0? leftmenu[0] : undefined)
|
var currentTab = ref(leftmenu.length > 0 ? leftmenu[0] : undefined);
|
||||||
var subTab = ref();
|
var subTab = ref();
|
||||||
var tabConfig = $find(menu, { code: "configuration" });
|
var tabConfig = $find(menu, { code: "configuration" });
|
||||||
var avatar = ref();
|
var avatar = ref();
|
||||||
@@ -364,11 +397,16 @@ function changeTab(tab, subtab) {
|
|||||||
router.push({ query: query });
|
router.push({ query: query });
|
||||||
}
|
}
|
||||||
function openProfile() {
|
function openProfile() {
|
||||||
let modal = { component: "user/Profile", width: "1100px", height: "360px", title: $store.lang==='vi'? 'Thông tin cá nhân' : '"User profile"' };
|
let modal = {
|
||||||
|
component: "user/Profile",
|
||||||
|
width: "1100px",
|
||||||
|
height: "360px",
|
||||||
|
title: $store.lang === "vi" ? "Thông tin cá nhân" : '"User profile"',
|
||||||
|
};
|
||||||
$store.commit("showmodal", modal);
|
$store.commit("showmodal", modal);
|
||||||
}
|
}
|
||||||
let found = route.query.tab? $find(menu, {code: route.query.tab}) : undefined
|
let found = route.query.tab ? $find(menu, { code: route.query.tab }) : undefined;
|
||||||
if(found || currentTab.value) changeTab(found || currentTab.value)
|
if (found || currentTab.value) changeTab(found || currentTab.value);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!$store.login) return;
|
if (!$store.login) return;
|
||||||
avatar.value = {
|
avatar.value = {
|
||||||
@@ -391,7 +429,7 @@ watch(
|
|||||||
};
|
};
|
||||||
isAdmin.value = $store.login.type__code === "admin";
|
isAdmin.value = $store.login.type__code === "admin";
|
||||||
lang.value = $store.lang;
|
lang.value = $store.lang;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -413,20 +451,30 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1023px) {
|
||||||
|
.navbar-brand {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-item,
|
||||||
|
a.navbar-link {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-dropdown {
|
||||||
|
box-shadow: 0 0.2em 0 hsla(var(--bulma-scheme-h), var(--bulma-scheme-s), var(--bulma-scheme-invert-l), 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-item:hover {
|
.navbar-item:hover {
|
||||||
.navbar-link {
|
.navbar-link {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.navbar-item > .navbar-link:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,48 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="record">
|
<div v-if="record">
|
||||||
<div class="columns is-multiline mx-0 mt-1" id="printable">
|
<div
|
||||||
<div class="column is-5">
|
class="columns is-multiline mx-0 mt-1"
|
||||||
<div class="field">
|
id="printable"
|
||||||
<label class="label">{{ $lang('code') }}:</label>
|
>
|
||||||
<div class="control">
|
<div class="column is-5">
|
||||||
{{ `${record.code}` }}
|
<div class="field">
|
||||||
|
<label class="label">{{ $lang("code") }}:</label>
|
||||||
|
<div class="control">
|
||||||
|
{{ `${record.code}` }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="column is-7">
|
||||||
<div class="column is-7">
|
<div class="field">
|
||||||
<div class="field">
|
<label class="label">{{ $lang("account-type") }}:</label>
|
||||||
<label class="label">{{ $lang('account-type') }}:</label>
|
<div class="control">
|
||||||
<div class="control">
|
{{ `${record.type__code} / ${record.type__name}` }}
|
||||||
{{ `${record.type__code} / ${record.type__name}` }}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="column is-5">
|
||||||
<div class="column is-5">
|
<div class="field">
|
||||||
<div class="field">
|
<label class="label">{{ $lang("currency") }}:</label>
|
||||||
<label class="label">{{ $lang('currency') }}:</label>
|
<div class="control">
|
||||||
<div class="control">
|
{{ `${record.currency__code} / ${record.currency__name}` }}
|
||||||
{{ `${record.currency__code} / ${record.currency__name}` }}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="column is-7">
|
||||||
<div class="column is-7">
|
<div class="field">
|
||||||
<div class="field">
|
<label class="label">{{ $lang("balance") }}:</label>
|
||||||
<label class="label">{{ $lang('balance') }}:</label>
|
<div class="control">
|
||||||
<div class="control">
|
{{ $numtoString(record.balance) }}
|
||||||
{{ $numtoString(record.balance) }}
|
</div>
|
||||||
</div>
|
<!--<p class="help is-findata">{{$vnmoney($formatNumber(record.balance))}}</p>-->
|
||||||
<!--<p class="help is-findata">{{$vnmoney($formatNumber(record.balance))}}</p>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-5">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $lang('open-date') }}:</label>
|
|
||||||
<div class="control">
|
|
||||||
{{ `${$dayjs(record.create_time).format('DD/MM/YYYY')}` }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="column is-5">
|
||||||
<!--<div class="column is-7">
|
<div class="field">
|
||||||
|
<label class="label">{{ $lang("open-date") }}:</label>
|
||||||
|
<div class="control">
|
||||||
|
{{ `${$dayjs(record.create_time).format("DD/MM/YYYY")}` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--<div class="column is-7">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Chi nhánh:</label>
|
<label class="label">Chi nhánh:</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
@@ -50,30 +53,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="border-bottom"></div>
|
||||||
|
<div
|
||||||
|
class="mt-5"
|
||||||
|
id="ignore"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="$exportpdf('printable', record.code)"
|
||||||
|
>
|
||||||
|
{{ $lang("print") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-bottom"></div>
|
|
||||||
<div class="mt-5" id="ignore">
|
|
||||||
<button class="button is-primary has-text-white" @click="$exportpdf('printable', record.code)">{{$lang('print')}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['row'],
|
props: ["row"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
errors: {},
|
errors: {},
|
||||||
record: undefined
|
record: undefined,
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.record = await this.$getdata("internalaccount", { id: this.row.account || this.row.id }, undefined, true);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selected(attr, obj) {
|
||||||
|
this.record[attr] = obj;
|
||||||
|
if (attr === "_type") this.category = obj.category__code;
|
||||||
},
|
},
|
||||||
async created() {
|
},
|
||||||
this.record = await this.$getdata('internalaccount', {id: this.row.account || this.row.id}, undefined, true)
|
};
|
||||||
},
|
</script>
|
||||||
methods: {
|
|
||||||
selected(attr, obj) {
|
|
||||||
this.record[attr] = obj
|
|
||||||
if(attr==='_type') this.category = obj.category__code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,50 +1,60 @@
|
|||||||
<!-- components/dialog/ConfirmDeleteEntry.vue -->
|
|
||||||
<template>
|
<template>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
|
<div class="mb-3 p-3">
|
||||||
<div class=" mb-3 p-3">
|
<p class="is-size-5 has-text-weight-semibold mb-4">Bạn có chắc chắn muốn xóa bút toán này?</p>
|
||||||
<p class="is-size-5 has-text-weight-semibold mb-4">
|
<p class="mt-3 has-text-danger has-text-weight-semibold">
|
||||||
Bạn có chắc chắn muốn xóa bút toán này?
|
Hành động này <strong>không thể hoàn tác</strong>.<br />
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="mt-3 has-text-danger has-text-weight-semibold">
|
|
||||||
Hành động này <strong>không thể hoàn tác</strong>.<br>
|
|
||||||
Dữ liệu liên quan (nếu có) sẽ bị xóa vĩnh viễn.
|
Dữ liệu liên quan (nếu có) sẽ bị xóa vĩnh viễn.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field is-grouped is-justify-content-center">
|
<div class="field is-grouped is-justify-content-center">
|
||||||
<!-- Captcha addon group - shown only when captcha is not confirmed -->
|
<!-- Captcha addon group - shown only when captcha is not confirmed -->
|
||||||
<p class="control" v-if="!isConfirmed">
|
<div
|
||||||
|
class="control"
|
||||||
|
v-if="!isConfirmed"
|
||||||
|
>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Nhập mã xác nhận"
|
placeholder="Nhập mã xác nhận"
|
||||||
v-model="userInputCaptcha"
|
v-model="userInputCaptcha"
|
||||||
@keyup.enter="isConfirmed && confirmDelete()"
|
@keyup.enter="isConfirmed && confirmDelete()"
|
||||||
>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<a class="button is-static has-text-weight-bold has-background-grey-lighter"
|
<a
|
||||||
style="font-family: 'Courier New', monospace; letter-spacing: 2px;">
|
class="button is-static has-text-weight-bold has-background-grey-lighter"
|
||||||
|
style="font-family: "Courier New", monospace; letter-spacing: 2px"
|
||||||
|
>
|
||||||
{{ captchaCode }}
|
{{ captchaCode }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button" @click="generateCaptcha" title="Tạo mã mới">
|
<button
|
||||||
|
class="button"
|
||||||
|
@click="generateCaptcha"
|
||||||
|
title="Tạo mã mới"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon name="refresh.svg" type="primary" :size="23" />
|
<SvgIcon
|
||||||
|
name="refresh.svg"
|
||||||
|
type="primary"
|
||||||
|
:size="23"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<!-- Confirm button - shown only when captcha IS confirmed -->
|
<!-- Confirm button - shown only when captcha IS confirmed -->
|
||||||
<p class="control" v-if="isConfirmed">
|
<p
|
||||||
|
class="control"
|
||||||
|
v-if="isConfirmed"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="button is-danger"
|
class="button is-danger"
|
||||||
:class="{ 'is-loading': isDeleting }"
|
:class="{ 'is-loading': isDeleting }"
|
||||||
@@ -69,75 +79,70 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from "vue";
|
||||||
import { useNuxtApp } from '#app'
|
import { useNuxtApp } from "#app";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
entryId: {
|
entryId: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'deleted'])
|
const emit = defineEmits(["close", "deleted"]);
|
||||||
|
|
||||||
const { $snackbar ,$insertapi} = useNuxtApp()
|
const { $snackbar, $insertapi } = useNuxtApp();
|
||||||
const isDeleting = ref(false)
|
const isDeleting = ref(false);
|
||||||
const captchaCode = ref('')
|
const captchaCode = ref("");
|
||||||
const userInputCaptcha = ref('')
|
const userInputCaptcha = ref("");
|
||||||
|
|
||||||
const isConfirmed = computed(() => {
|
const isConfirmed = computed(() => {
|
||||||
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== ''
|
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== "";
|
||||||
})
|
});
|
||||||
|
|
||||||
const generateCaptcha = () => {
|
const generateCaptcha = () => {
|
||||||
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase()
|
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase();
|
||||||
userInputCaptcha.value = ''
|
userInputCaptcha.value = "";
|
||||||
}
|
};
|
||||||
|
|
||||||
// Initial generation
|
// Initial generation
|
||||||
generateCaptcha()
|
generateCaptcha();
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (isDeleting.value || !isConfirmed.value) return
|
if (isDeleting.value || !isConfirmed.value) return;
|
||||||
isDeleting.value = true
|
isDeleting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Gọi API xóa theo đúng endpoint delete-entry/{id}
|
// Gọi API xóa theo đúng endpoint delete-entry/{id}
|
||||||
const result = await $insertapi('deleteentry', {id: props.entryId})
|
const result = await $insertapi("deleteentry", { id: props.entryId });
|
||||||
|
|
||||||
if (result === 'error' || !result) {
|
if (result === "error" || !result) {
|
||||||
throw new Error('API xóa trả về lỗi')
|
throw new Error("API xóa trả về lỗi");
|
||||||
}
|
}
|
||||||
|
|
||||||
$snackbar(
|
$snackbar(`Đã xóa bút toán ID ${props.entryId} thành công`, "Thành công", "Success");
|
||||||
`Đã xóa bút toán ID ${props.entryId} thành công`,
|
|
||||||
'Thành công',
|
|
||||||
'Success'
|
|
||||||
)
|
|
||||||
|
|
||||||
emit('deleted', props.entryId)
|
|
||||||
emit('close')
|
|
||||||
|
|
||||||
|
emit("deleted", props.entryId);
|
||||||
|
emit("close");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Xóa bút toán thất bại:', err)
|
console.error("Xóa bút toán thất bại:", err);
|
||||||
|
|
||||||
|
let errorMsg = "Không thể xóa bút toán. Vui lòng thử lại.";
|
||||||
|
|
||||||
let errorMsg = 'Không thể xóa bút toán. Vui lòng thử lại.'
|
|
||||||
|
|
||||||
// Nếu backend trả về thông báo cụ thể
|
// Nếu backend trả về thông báo cụ thể
|
||||||
if (err?.response?.data?.detail) {
|
if (err?.response?.data?.detail) {
|
||||||
errorMsg = err.response.data.detail
|
errorMsg = err.response.data.detail;
|
||||||
} else if (err?.response?.data?.non_field_errors) {
|
} else if (err?.response?.data?.non_field_errors) {
|
||||||
errorMsg = err.response.data.non_field_errors.join(' ')
|
errorMsg = err.response.data.non_field_errors.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
$snackbar(errorMsg, 'Lỗi', 'Danger')
|
$snackbar(errorMsg, "Lỗi", "Danger");
|
||||||
} finally {
|
} finally {
|
||||||
isDeleting.value = false
|
isDeleting.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('close')
|
emit("close");
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
@date="selected('fdate', $event)"
|
@date="selected('fdate', $event)"
|
||||||
></Datepicker>
|
></Datepicker>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_date"
|
||||||
|
>
|
||||||
|
{{ errors.issued_date }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
@@ -21,11 +26,19 @@
|
|||||||
@date="selected('tdate', $event)"
|
@date="selected('tdate', $event)"
|
||||||
></Datepicker>
|
></Datepicker>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_date"
|
||||||
|
>
|
||||||
|
{{ errors.issued_date }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataView v-bind="vbind" v-if="vbind" />
|
<DataView
|
||||||
|
v-bind="vbind"
|
||||||
|
v-if="vbind"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
const { $dayjs, $id } = useNuxtApp();
|
const { $dayjs, $id } = useNuxtApp();
|
||||||
@@ -37,9 +50,9 @@ const vbind = ref(null);
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData();
|
loadData();
|
||||||
})
|
});
|
||||||
|
|
||||||
function selected(attr, value) {
|
function selected(attr, value) {
|
||||||
if (attr === "fdate") fdate.value = value;
|
if (attr === "fdate") fdate.value = value;
|
||||||
else tdate.value = value;
|
else tdate.value = value;
|
||||||
loadData();
|
loadData();
|
||||||
@@ -56,18 +69,27 @@ function loadData() {
|
|||||||
values:
|
values:
|
||||||
"customer__code,customer__fullname,customer__type__name,customer__legal_type__name,customer__legal_code",
|
"customer__code,customer__fullname,customer__type__name,customer__legal_type__name,customer__legal_code",
|
||||||
distinct_values: {
|
distinct_values: {
|
||||||
sum_sale_price: { type: "Sum", field: "product__prdbk__transaction__sale_price" },
|
sum_sale_price: {
|
||||||
sum_received: { type: "Sum", field: "product__prdbk__transaction__amount_received" },
|
type: "Sum",
|
||||||
sum_remain: { type: "Sum", field: "product__prdbk__transaction__amount_remain" },
|
field: "product__prdbk__transaction__sale_price",
|
||||||
|
},
|
||||||
|
sum_received: {
|
||||||
|
type: "Sum",
|
||||||
|
field: "product__prdbk__transaction__amount_received",
|
||||||
|
},
|
||||||
|
sum_remain: {
|
||||||
|
type: "Sum",
|
||||||
|
field: "product__prdbk__transaction__amount_remain",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
summary: "annotate",
|
summary: "annotate",
|
||||||
filter: {
|
filter: {
|
||||||
date__gte: fdate.value,
|
date__gte: fdate.value,
|
||||||
date__lte: tdate.value
|
date__lte: tdate.value,
|
||||||
},
|
},
|
||||||
sort: "-sum_remain",
|
sort: "-sum_remain",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
@date="selected('fdate', $event)"
|
@date="selected('fdate', $event)"
|
||||||
></Datepicker>
|
></Datepicker>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_date"
|
||||||
|
>
|
||||||
|
{{ errors.issued_date }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
@@ -21,11 +26,19 @@
|
|||||||
@date="selected('tdate', $event)"
|
@date="selected('tdate', $event)"
|
||||||
></Datepicker>
|
></Datepicker>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_date"
|
||||||
|
>
|
||||||
|
{{ errors.issued_date }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataView v-bind="vbind" v-if="vbind" />
|
<DataView
|
||||||
|
v-bind="vbind"
|
||||||
|
v-if="vbind"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
@@ -60,13 +73,21 @@ export default {
|
|||||||
values:
|
values:
|
||||||
"product,product__prdbk__transaction__amount_received,product__trade_code,product__prdbk__transaction__sale_price,product__zone_type__name,customer,customer__code,customer__fullname",
|
"product,product__prdbk__transaction__amount_received,product__trade_code,product__prdbk__transaction__sale_price,product__zone_type__name,customer,customer__code,customer__fullname",
|
||||||
distinct_values: {
|
distinct_values: {
|
||||||
sumCR: { type: "Sum", filter: { type__code: "CR" }, field: "amount" },
|
sumCR: {
|
||||||
sumDR: { type: "Sum", filter: { type__code: "DR" }, field: "amount" },
|
type: "Sum",
|
||||||
|
filter: { type__code: "CR" },
|
||||||
|
field: "amount",
|
||||||
|
},
|
||||||
|
sumDR: {
|
||||||
|
type: "Sum",
|
||||||
|
filter: { type__code: "DR" },
|
||||||
|
field: "amount",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
summary: "annotate",
|
summary: "annotate",
|
||||||
filter: { date__gte: this.fdate, date__lte: this.tdate },
|
filter: { date__gte: this.fdate, date__lte: this.tdate },
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
pagename: 'debt_report',
|
pagename: 'debt_report',
|
||||||
api: 'transaction',
|
api: 'transaction',
|
||||||
timeopt: { time: 36000, disable: ['add'] },
|
timeopt: { time: 36000, disable: ['add'] },
|
||||||
filter: { phase: 3 }
|
filter: { phase: 3 },
|
||||||
}"
|
}"
|
||||||
@option="handleTimeOption"
|
@option="handleTimeOption"
|
||||||
@excel="exportExcel"
|
@excel="exportExcel"
|
||||||
@@ -15,17 +15,26 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
<div v-if="loading" class="has-text-centered py-6">
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="has-text-centered py-6"
|
||||||
|
>
|
||||||
<p class="has-text-grey mb-3">Đang tải dữ liệu...</p>
|
<p class="has-text-grey mb-3">Đang tải dữ liệu...</p>
|
||||||
<progress class="progress is-small is-primary" max="100"></progress>
|
<progress
|
||||||
|
class="progress is-small is-primary"
|
||||||
|
max="100"
|
||||||
|
></progress>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="" v-else>
|
<div
|
||||||
|
class=""
|
||||||
|
v-else
|
||||||
|
>
|
||||||
<!-- Table -->
|
<!-- Table -->
|
||||||
<div
|
<div
|
||||||
v-if="filteredRows.length > 0"
|
v-if="filteredRows.length > 0"
|
||||||
class="table-container"
|
class="table-container"
|
||||||
style="overflow-x: auto; max-width: 100%;"
|
style="overflow-x: auto; max-width: 100%"
|
||||||
>
|
>
|
||||||
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth debt-table">
|
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth debt-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -37,22 +46,40 @@
|
|||||||
>
|
>
|
||||||
STT
|
STT
|
||||||
</th>
|
</th>
|
||||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
<th
|
||||||
|
rowspan="2"
|
||||||
|
class="fixed-col has-background-primary has-text-white"
|
||||||
|
>
|
||||||
Mã KH
|
Mã KH
|
||||||
</th>
|
</th>
|
||||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
<th
|
||||||
|
rowspan="2"
|
||||||
|
class="fixed-col has-background-primary has-text-white"
|
||||||
|
>
|
||||||
Mã Căn
|
Mã Căn
|
||||||
</th>
|
</th>
|
||||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
<th
|
||||||
|
rowspan="2"
|
||||||
|
class="fixed-col has-background-primary has-text-white"
|
||||||
|
>
|
||||||
Ngày ký HĐ
|
Ngày ký HĐ
|
||||||
</th>
|
</th>
|
||||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
<th
|
||||||
|
rowspan="2"
|
||||||
|
class="fixed-col has-background-primary has-text-white"
|
||||||
|
>
|
||||||
Giá trị HĐMB
|
Giá trị HĐMB
|
||||||
</th>
|
</th>
|
||||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
<th
|
||||||
|
rowspan="2"
|
||||||
|
class="fixed-col has-background-primary has-text-white"
|
||||||
|
>
|
||||||
Tiền nộp theo HĐV/TTTHNV
|
Tiền nộp theo HĐV/TTTHNV
|
||||||
</th>
|
</th>
|
||||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
<th
|
||||||
|
rowspan="2"
|
||||||
|
class="fixed-col has-background-primary has-text-white"
|
||||||
|
>
|
||||||
Tỷ lệ
|
Tỷ lệ
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
@@ -66,13 +93,19 @@
|
|||||||
{{ sch.label }}
|
{{ sch.label }}
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th rowspan="2" class="has-background-primary has-text-white">
|
<th
|
||||||
|
rowspan="2"
|
||||||
|
class="has-background-primary has-text-white"
|
||||||
|
>
|
||||||
Số tiền quá hạn
|
Số tiền quá hạn
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
<template
|
||||||
|
v-for="(sch, si) in scheduleHeaders"
|
||||||
|
:key="si"
|
||||||
|
>
|
||||||
<th class="has-background-primary has-text-white sub-header">Ngày</th>
|
<th class="has-background-primary has-text-white sub-header">Ngày</th>
|
||||||
<th class="has-background-primary has-text-white sub-header">Số tiền</th>
|
<th class="has-background-primary has-text-white sub-header">Số tiền</th>
|
||||||
<th class="has-background-primary has-text-white sub-header">Lũy kế sang đợt</th>
|
<th class="has-background-primary has-text-white sub-header">Lũy kế sang đợt</th>
|
||||||
@@ -84,13 +117,20 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(row, ri) in filteredRows" :key="ri">
|
<tr
|
||||||
|
v-for="(row, ri) in filteredRows"
|
||||||
|
:key="ri"
|
||||||
|
>
|
||||||
<!-- Fixed columns -->
|
<!-- Fixed columns -->
|
||||||
<td class="fixed-col has-text-centered">{{ ri + 1 }}</td>
|
<td class="fixed-col has-text-centered">{{ ri + 1 }}</td>
|
||||||
<td class="fixed-col">{{ row.customer_code }}</td>
|
<td class="fixed-col">{{ row.customer_code }}</td>
|
||||||
<td class="fixed-col has-text-weight-semibold has-text-primary">{{ row.trade_code }}</td>
|
<td class="fixed-col has-text-weight-semibold has-text-primary">
|
||||||
|
{{ row.trade_code }}
|
||||||
|
</td>
|
||||||
<td class="fixed-col">{{ row.contract_date }}</td>
|
<td class="fixed-col">{{ row.contract_date }}</td>
|
||||||
<td class="fixed-col has-text-right">{{ fmt(row.sale_price) }}</td>
|
<td class="fixed-col has-text-right">
|
||||||
|
{{ fmt(row.sale_price) }}
|
||||||
|
</td>
|
||||||
<td class="fixed-col has-text-right has-text-weight-semibold has-background-warning-light">
|
<td class="fixed-col has-text-right has-text-weight-semibold has-background-warning-light">
|
||||||
{{ fmt(row.ttthnv_paid) }}
|
{{ fmt(row.ttthnv_paid) }}
|
||||||
</td>
|
</td>
|
||||||
@@ -99,11 +139,18 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Scrollable columns -->
|
<!-- Scrollable columns -->
|
||||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
<template
|
||||||
|
v-for="(sch, si) in scheduleHeaders"
|
||||||
|
:key="si"
|
||||||
|
>
|
||||||
<template v-if="row.schedules[si]">
|
<template v-if="row.schedules[si]">
|
||||||
<td>{{ row.schedules[si].to_date }}</td>
|
<td>{{ row.schedules[si].to_date }}</td>
|
||||||
<td class="has-text-right">{{ fmt(row.schedules[si].amount) }}</td>
|
<td class="has-text-right">
|
||||||
<td class="has-text-right has-text-info">{{ fmt(row.schedules[si].luy_ke_sang_dot) }}</td>
|
{{ fmt(row.schedules[si].amount) }}
|
||||||
|
</td>
|
||||||
|
<td class="has-text-right has-text-info">
|
||||||
|
{{ fmt(row.schedules[si].luy_ke_sang_dot) }}
|
||||||
|
</td>
|
||||||
<td class="has-text-right has-text-weight-semibold has-text-success">
|
<td class="has-text-right has-text-weight-semibold has-text-success">
|
||||||
{{ fmt(row.schedules[si].thuc_thanh_toan) }}
|
{{ fmt(row.schedules[si].thuc_thanh_toan) }}
|
||||||
</td>
|
</td>
|
||||||
@@ -121,7 +168,13 @@
|
|||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<td colspan="6" class="has-text-centered has-text-grey-light" style="font-style: italic">—</td>
|
<td
|
||||||
|
colspan="6"
|
||||||
|
class="has-text-centered has-text-grey-light"
|
||||||
|
style="font-style: italic"
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</td>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -129,25 +182,35 @@
|
|||||||
class="has-text-right has-text-weight-bold"
|
class="has-text-right has-text-weight-bold"
|
||||||
:class="Number(row.overdue) > 0 ? 'has-background-danger-light' : ''"
|
:class="Number(row.overdue) > 0 ? 'has-background-danger-light' : ''"
|
||||||
>
|
>
|
||||||
{{ Number(row.overdue) > 0 ? fmt(row.overdue) : '—' }}
|
{{ Number(row.overdue) > 0 ? fmt(row.overdue) : "—" }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="has-background-light">
|
<tr class="has-background-light">
|
||||||
<td :colspan="7" class="fixed-col has-text-right has-text-weight-bold">TỔNG CỘNG:</td>
|
<td
|
||||||
|
:colspan="7"
|
||||||
|
class="fixed-col has-text-right has-text-weight-bold"
|
||||||
|
>
|
||||||
|
TỔNG CỘNG:
|
||||||
|
</td>
|
||||||
|
|
||||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
<template
|
||||||
|
v-for="(sch, si) in scheduleHeaders"
|
||||||
|
:key="si"
|
||||||
|
>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td class="has-text-right has-text-weight-semibold">{{ fmt(colSum(si, 'amount')) }}</td>
|
<td class="has-text-right has-text-weight-semibold">
|
||||||
|
{{ fmt(colSum(si, "amount")) }}
|
||||||
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td class="has-text-right has-text-weight-semibold has-text-success">
|
<td class="has-text-right has-text-weight-semibold has-text-success">
|
||||||
{{ fmt(colSum(si, 'thuc_thanh_toan')) }}
|
{{ fmt(colSum(si, "thuc_thanh_toan")) }}
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td class="has-text-right has-text-weight-semibold has-text-danger">
|
<td class="has-text-right has-text-weight-semibold has-text-danger">
|
||||||
{{ fmt(colSum(si, 'amount_remain')) }}
|
{{ fmt(colSum(si, "amount_remain")) }}
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -160,7 +223,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty -->
|
<!-- Empty -->
|
||||||
<div v-else class="has-text-centered py-6">
|
<div
|
||||||
|
v-else
|
||||||
|
class="has-text-centered py-6"
|
||||||
|
>
|
||||||
<p class="has-text-grey">Không có dữ liệu</p>
|
<p class="has-text-grey">Không có dữ liệu</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,283 +234,279 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from "vue";
|
||||||
import TimeOption from '~/components/datatable/TimeOption'
|
import TimeOption from "~/components/datatable/TimeOption";
|
||||||
const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp()
|
const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp();
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false);
|
||||||
const rows = ref([])
|
const rows = ref([]);
|
||||||
const filteredRows = ref([])
|
const filteredRows = ref([]);
|
||||||
const scheduleHeaders = ref([])
|
const scheduleHeaders = ref([]);
|
||||||
|
|
||||||
const currentFilter = ref(null)
|
const currentFilter = ref(null);
|
||||||
const currentSearch = ref(null)
|
const currentSearch = ref(null);
|
||||||
|
|
||||||
function handleTimeOption(option) {
|
function handleTimeOption(option) {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
currentFilter.value = null
|
currentFilter.value = null;
|
||||||
currentSearch.value = null
|
currentSearch.value = null;
|
||||||
applyFilters()
|
applyFilters();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option.filter) {
|
if (option.filter) {
|
||||||
currentFilter.value = option.filter
|
currentFilter.value = option.filter;
|
||||||
currentSearch.value = null
|
currentSearch.value = null;
|
||||||
applyFilters()
|
applyFilters();
|
||||||
} else if (option.filter_or) {
|
} else if (option.filter_or) {
|
||||||
currentFilter.value = null
|
currentFilter.value = null;
|
||||||
currentSearch.value = option.filter_or
|
currentSearch.value = option.filter_or;
|
||||||
applyFilters()
|
applyFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
let filtered = [...rows.value]
|
let filtered = [...rows.value];
|
||||||
|
|
||||||
if (currentFilter.value && currentFilter.value.create_time__date__gte) {
|
if (currentFilter.value && currentFilter.value.create_time__date__gte) {
|
||||||
const filterDate = new Date(currentFilter.value.create_time__date__gte)
|
const filterDate = new Date(currentFilter.value.create_time__date__gte);
|
||||||
filtered = filtered.filter(row => {
|
filtered = filtered.filter((row) => {
|
||||||
const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null
|
const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null;
|
||||||
return contractDate && contractDate >= filterDate
|
return contractDate && contractDate >= filterDate;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSearch.value) {
|
if (currentSearch.value) {
|
||||||
const searchTerms = Object.values(currentSearch.value).map(v =>
|
const searchTerms = Object.values(currentSearch.value).map((v) =>
|
||||||
String(v).toLowerCase().replace('__icontains', '')
|
String(v).toLowerCase().replace("__icontains", ""),
|
||||||
)
|
);
|
||||||
filtered = filtered.filter(row => {
|
filtered = filtered.filter((row) => {
|
||||||
const searchableText = [
|
const searchableText = [
|
||||||
row.customer_code,
|
row.customer_code,
|
||||||
row.customer_name,
|
row.customer_name,
|
||||||
row.trade_code,
|
row.trade_code,
|
||||||
row.contract_date,
|
row.contract_date,
|
||||||
String(row.sale_price)
|
String(row.sale_price),
|
||||||
].join(' ').toLowerCase()
|
]
|
||||||
return searchTerms.some(term => searchableText.includes(term))
|
.join(" ")
|
||||||
})
|
.toLowerCase();
|
||||||
|
return searchTerms.some((term) => searchableText.includes(term));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredRows.value = filtered
|
filteredRows.value = filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
rows.value = []
|
rows.value = [];
|
||||||
filteredRows.value = []
|
filteredRows.value = [];
|
||||||
scheduleHeaders.value = []
|
scheduleHeaders.value = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const txnConn = $copy($findapi('transaction'))
|
const txnConn = $copy($findapi("transaction"));
|
||||||
txnConn.params = {
|
txnConn.params = {
|
||||||
filter: { phase: 3 },
|
filter: { phase: 3 },
|
||||||
values: 'id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase',
|
values:
|
||||||
sort: 'id'
|
"id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase",
|
||||||
}
|
sort: "id",
|
||||||
|
};
|
||||||
|
|
||||||
const detailConn = $copy($findapi('reservation'))
|
const detailConn = $copy($findapi("reservation"));
|
||||||
detailConn.params = {
|
detailConn.params = {
|
||||||
filter: { transaction__phase: 3, phase: 3 },
|
filter: { transaction__phase: 3, phase: 3 },
|
||||||
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status',
|
values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
|
||||||
sort: 'transaction'
|
sort: "transaction",
|
||||||
}
|
};
|
||||||
|
|
||||||
const schConn = $copy($findapi('payment_schedule'))
|
const schConn = $copy($findapi("payment_schedule"));
|
||||||
schConn.params = {
|
schConn.params = {
|
||||||
filter: { txn_detail__phase: 3 },
|
filter: { txn_detail__phase: 3 },
|
||||||
values:
|
values:
|
||||||
'id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received',
|
"id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received",
|
||||||
sort: 'txn_detail__transaction,cycle'
|
sort: "txn_detail__transaction,cycle",
|
||||||
}
|
};
|
||||||
|
|
||||||
const ttthnvConn = $copy($findapi('reservation'))
|
const ttthnvConn = $copy($findapi("reservation"));
|
||||||
ttthnvConn.params = {
|
ttthnvConn.params = {
|
||||||
filter: { transaction__phase: 3, phase: 4 },
|
filter: { transaction__phase: 3, phase: 4 },
|
||||||
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status',
|
values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
|
||||||
sort: 'transaction'
|
sort: "transaction",
|
||||||
}
|
};
|
||||||
|
|
||||||
const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([
|
const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([txnConn, detailConn, schConn, ttthnvConn]);
|
||||||
txnConn,
|
|
||||||
detailConn,
|
|
||||||
schConn,
|
|
||||||
ttthnvConn
|
|
||||||
])
|
|
||||||
|
|
||||||
const transactions = txnRs?.data?.rows || []
|
const transactions = txnRs?.data?.rows || [];
|
||||||
const details = detailRs?.data?.rows || []
|
const details = detailRs?.data?.rows || [];
|
||||||
const schedules = schRs?.data?.rows || []
|
const schedules = schRs?.data?.rows || [];
|
||||||
const ttthnvList = ttthnvRs?.data?.rows || []
|
const ttthnvList = ttthnvRs?.data?.rows || [];
|
||||||
|
|
||||||
if (!transactions.length) {
|
if (!transactions.length) {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TTTHNV map
|
// TTTHNV map
|
||||||
const ttthnvMap = {}
|
const ttthnvMap = {};
|
||||||
ttthnvList.forEach(t => {
|
ttthnvList.forEach((t) => {
|
||||||
const tid = t.transaction
|
const tid = t.transaction;
|
||||||
ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0)
|
ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0);
|
||||||
})
|
});
|
||||||
|
|
||||||
// Group schedules by transaction
|
// Group schedules by transaction
|
||||||
const schByTxn = {}
|
const schByTxn = {};
|
||||||
schedules.forEach(s => {
|
schedules.forEach((s) => {
|
||||||
const tid = s.txn_detail__transaction
|
const tid = s.txn_detail__transaction;
|
||||||
if (!schByTxn[tid]) schByTxn[tid] = []
|
if (!schByTxn[tid]) schByTxn[tid] = [];
|
||||||
schByTxn[tid].push(s)
|
schByTxn[tid].push(s);
|
||||||
})
|
});
|
||||||
|
|
||||||
// Tìm số đợt tối đa
|
// Tìm số đợt tối đa
|
||||||
let maxCycles = 0
|
let maxCycles = 0;
|
||||||
Object.values(schByTxn).forEach(list => {
|
Object.values(schByTxn).forEach((list) => {
|
||||||
const paymentList = list.filter(s => Number(s.cycle) > 0)
|
const paymentList = list.filter((s) => Number(s.cycle) > 0);
|
||||||
if (paymentList.length > maxCycles) maxCycles = paymentList.length
|
if (paymentList.length > maxCycles) maxCycles = paymentList.length;
|
||||||
})
|
});
|
||||||
|
|
||||||
scheduleHeaders.value = Array.from({ length: maxCycles }, (_, i) => ({
|
scheduleHeaders.value = Array.from({ length: maxCycles }, (_, i) => ({
|
||||||
label: `L0${i + 1}`,
|
label: `L0${i + 1}`,
|
||||||
index: i
|
index: i,
|
||||||
}))
|
}));
|
||||||
|
|
||||||
rows.value = transactions.map(txn => {
|
rows.value = transactions.map((txn) => {
|
||||||
const txnSchedules = (schByTxn[txn.id] || [])
|
const txnSchedules = (schByTxn[txn.id] || [])
|
||||||
.filter(s => Number(s.cycle) > 0)
|
.filter((s) => Number(s.cycle) > 0)
|
||||||
.sort((a, b) => Number(a.cycle) - Number(b.cycle))
|
.sort((a, b) => Number(a.cycle) - Number(b.cycle));
|
||||||
|
|
||||||
const ttthnvPaid = ttthnvMap[txn.id] || 0
|
const ttthnvPaid = ttthnvMap[txn.id] || 0;
|
||||||
const salePriceNum = Number(txn.sale_price || 0)
|
const salePriceNum = Number(txn.sale_price || 0);
|
||||||
|
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
// QUAN TRỌNG: Theo yêu cầu mới nhất của bạn
|
// QUAN TRỌNG: Theo yêu cầu mới nhất của bạn
|
||||||
// Lũy kế HĐCN = TTTHNV
|
// Lũy kế HĐCN = TTTHNV
|
||||||
const luyKe = ttthnvPaid
|
const luyKe = ttthnvPaid;
|
||||||
// ───────────────────────────────────────────────
|
// ───────────────────────────────────────────────
|
||||||
|
|
||||||
// Phân bổ TTTHNV dần vào từng đợt → tính lũy kế sang đợt
|
// Phân bổ TTTHNV dần vào từng đợt → tính lũy kế sang đợt
|
||||||
let remainingTTTHNV = ttthnvPaid
|
let remainingTTTHNV = ttthnvPaid;
|
||||||
|
|
||||||
const schedulesWithCalc = txnSchedules.map(sch => {
|
const schedulesWithCalc = txnSchedules.map((sch) => {
|
||||||
const scheduleAmount = Number(sch.amount || 0)
|
const scheduleAmount = Number(sch.amount || 0);
|
||||||
|
|
||||||
// Lũy kế sang đợt = min(remaining TTTHNV, số tiền đợt)
|
// Lũy kế sang đợt = min(remaining TTTHNV, số tiền đợt)
|
||||||
const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount)
|
const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount);
|
||||||
|
|
||||||
// Số tiền đã thực thanh toán = paid_amount - lũy kế sang đợt
|
// Số tiền đã thực thanh toán = paid_amount - lũy kế sang đợt
|
||||||
const paidAmountFromSchedule = Number(sch.paid_amount || 0)
|
const paidAmountFromSchedule = Number(sch.paid_amount || 0);
|
||||||
const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot)
|
const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot);
|
||||||
|
|
||||||
// Dư nợ = số tiền đợt - lũy kế sang đợt - thực thanh toán
|
// Dư nợ = số tiền đợt - lũy kế sang đợt - thực thanh toán
|
||||||
const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan)
|
const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan);
|
||||||
|
|
||||||
remainingTTTHNV -= luyKeSangDot
|
remainingTTTHNV -= luyKeSangDot;
|
||||||
remainingTTTHNV = Math.max(0, remainingTTTHNV)
|
remainingTTTHNV = Math.max(0, remainingTTTHNV);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
to_date: sch.to_date ? $dayjs(sch.to_date).format('DD/MM/YYYY') : '—',
|
to_date: sch.to_date ? $dayjs(sch.to_date).format("DD/MM/YYYY") : "—",
|
||||||
amount: scheduleAmount,
|
amount: scheduleAmount,
|
||||||
luy_ke_sang_dot: luyKeSangDot,
|
luy_ke_sang_dot: luyKeSangDot,
|
||||||
thuc_thanh_toan: thucThanhToan,
|
thuc_thanh_toan: thucThanhToan,
|
||||||
amount_remain: amountRemain,
|
amount_remain: amountRemain,
|
||||||
status: sch.status
|
status: sch.status,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
// Tính quá hạn
|
// Tính quá hạn
|
||||||
const todayDate = new Date()
|
const todayDate = new Date();
|
||||||
const overdue = txnSchedules.reduce((sum, sch, idx) => {
|
const overdue = txnSchedules.reduce((sum, sch, idx) => {
|
||||||
const toDate = sch.to_date ? new Date(sch.to_date) : null
|
const toDate = sch.to_date ? new Date(sch.to_date) : null;
|
||||||
const remain = schedulesWithCalc[idx]?.amount_remain || 0
|
const remain = schedulesWithCalc[idx]?.amount_remain || 0;
|
||||||
if (toDate && toDate < todayDate && remain > 0) {
|
if (toDate && toDate < todayDate && remain > 0) {
|
||||||
return sum + remain
|
return sum + remain;
|
||||||
}
|
}
|
||||||
return sum
|
return sum;
|
||||||
}, 0)
|
}, 0);
|
||||||
|
|
||||||
const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null)
|
const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customer_code: txn.customer__code || '',
|
customer_code: txn.customer__code || "",
|
||||||
customer_name: txn.customer__fullname || '',
|
customer_name: txn.customer__fullname || "",
|
||||||
trade_code: txn.product__trade_code || txn.code || '',
|
trade_code: txn.product__trade_code || txn.code || "",
|
||||||
contract_date: txn.date ? $dayjs(txn.date).format('DD/MM/YYYY') : '—',
|
contract_date: txn.date ? $dayjs(txn.date).format("DD/MM/YYYY") : "—",
|
||||||
contract_date_raw: txn.date,
|
contract_date_raw: txn.date,
|
||||||
sale_price: salePriceNum,
|
sale_price: salePriceNum,
|
||||||
ttthnv_paid: ttthnvPaid,
|
ttthnv_paid: ttthnvPaid,
|
||||||
luy_ke: luyKe, // ← chính là TTTHNV
|
luy_ke: luyKe, // ← chính là TTTHNV
|
||||||
schedules: paddedSchedules,
|
schedules: paddedSchedules,
|
||||||
overdue: overdue
|
overdue: overdue,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
filteredRows.value = rows.value
|
filteredRows.value = rows.value;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('BaoCaoCongNo error:', e)
|
console.error("BaoCaoCongNo error:", e);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(val) {
|
function fmt(val) {
|
||||||
const n = Number(val)
|
const n = Number(val);
|
||||||
if (isNaN(n) || (!n && n !== 0)) return '—'
|
if (isNaN(n) || (!n && n !== 0)) return "—";
|
||||||
return n.toLocaleString('vi-VN')
|
return n.toLocaleString("vi-VN");
|
||||||
}
|
}
|
||||||
|
|
||||||
function pct(num, denom) {
|
function pct(num, denom) {
|
||||||
const n = Number(num)
|
const n = Number(num);
|
||||||
const d = Number(denom)
|
const d = Number(denom);
|
||||||
if (!d || isNaN(n)) return '—'
|
if (!d || isNaN(n)) return "—";
|
||||||
return (n / d * 100).toFixed(1) + '%'
|
return ((n / d) * 100).toFixed(1) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
function pctClass(paid, amount) {
|
function pctClass(paid, amount) {
|
||||||
const p = Number(paid)
|
const p = Number(paid);
|
||||||
const a = Number(amount)
|
const a = Number(amount);
|
||||||
if (isNaN(p) || isNaN(a) || !a) return ''
|
if (isNaN(p) || isNaN(a) || !a) return "";
|
||||||
const ratio = p / a
|
const ratio = p / a;
|
||||||
if (ratio >= 1) return 'has-text-success'
|
if (ratio >= 1) return "has-text-success";
|
||||||
if (ratio >= 0.5) return 'has-text-info'
|
if (ratio >= 0.5) return "has-text-info";
|
||||||
return 'has-text-danger'
|
return "has-text-danger";
|
||||||
}
|
}
|
||||||
|
|
||||||
function colSum(scheduleIndex, field) {
|
function colSum(scheduleIndex, field) {
|
||||||
return filteredRows.value.reduce((sum, row) => {
|
return filteredRows.value.reduce((sum, row) => {
|
||||||
const sch = row.schedules[scheduleIndex]
|
const sch = row.schedules[scheduleIndex];
|
||||||
return sum + (sch ? Number(sch[field] || 0) : 0)
|
return sum + (sch ? Number(sch[field] || 0) : 0);
|
||||||
}, 0)
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalOverdue = computed(() =>
|
const totalOverdue = computed(() => filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0));
|
||||||
filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
function exportExcel() {
|
function exportExcel() {
|
||||||
const headers = [
|
const headers = [
|
||||||
'STT',
|
"STT",
|
||||||
'Mã KH',
|
"Mã KH",
|
||||||
'Mã Căn',
|
"Mã Căn",
|
||||||
'Ngày ký HĐ',
|
"Ngày ký HĐ",
|
||||||
'Giá trị HĐMB',
|
"Giá trị HĐMB",
|
||||||
'Tiền nộp TTTHNV',
|
"Tiền nộp TTTHNV",
|
||||||
'Lũy kế tiền về HĐCN',
|
"Lũy kế tiền về HĐCN",
|
||||||
'Tỷ lệ HĐCN'
|
"Tỷ lệ HĐCN",
|
||||||
]
|
];
|
||||||
|
|
||||||
scheduleHeaders.value.forEach(h => {
|
scheduleHeaders.value.forEach((h) => {
|
||||||
headers.push(
|
headers.push(
|
||||||
`${h.label} - Ngày`,
|
`${h.label} - Ngày`,
|
||||||
`${h.label} - Số tiền`,
|
`${h.label} - Số tiền`,
|
||||||
`${h.label} - Lũy kế sang`,
|
`${h.label} - Lũy kế sang`,
|
||||||
`${h.label} - Số tiền đã thực thanh toán`,
|
`${h.label} - Số tiền đã thực thanh toán`,
|
||||||
`${h.label} - Tỷ lệ`,
|
`${h.label} - Tỷ lệ`,
|
||||||
`${h.label} - Dư nợ`
|
`${h.label} - Dư nợ`,
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
headers.push('Số tiền quá hạn')
|
headers.push("Số tiền quá hạn");
|
||||||
|
|
||||||
const data = filteredRows.value.map((row, i) => {
|
const data = filteredRows.value.map((row, i) => {
|
||||||
const base = [
|
const base = [
|
||||||
@@ -455,11 +517,11 @@ function exportExcel() {
|
|||||||
fmt(row.sale_price),
|
fmt(row.sale_price),
|
||||||
fmt(row.ttthnv_paid),
|
fmt(row.ttthnv_paid),
|
||||||
fmt(row.luy_ke),
|
fmt(row.luy_ke),
|
||||||
pct(row.luy_ke, row.sale_price)
|
pct(row.luy_ke, row.sale_price),
|
||||||
]
|
];
|
||||||
|
|
||||||
scheduleHeaders.value.forEach((_, si) => {
|
scheduleHeaders.value.forEach((_, si) => {
|
||||||
const sch = row.schedules[si]
|
const sch = row.schedules[si];
|
||||||
if (sch) {
|
if (sch) {
|
||||||
base.push(
|
base.push(
|
||||||
sch.to_date,
|
sch.to_date,
|
||||||
@@ -467,31 +529,31 @@ function exportExcel() {
|
|||||||
fmt(sch.luy_ke_sang_dot),
|
fmt(sch.luy_ke_sang_dot),
|
||||||
fmt(sch.thuc_thanh_toan),
|
fmt(sch.thuc_thanh_toan),
|
||||||
pct(sch.thuc_thanh_toan, sch.amount),
|
pct(sch.thuc_thanh_toan, sch.amount),
|
||||||
fmt(sch.amount_remain)
|
fmt(sch.amount_remain),
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
base.push('', '', '', '', '', '')
|
base.push("", "", "", "", "", "");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
base.push(fmt(row.overdue))
|
base.push(fmt(row.overdue));
|
||||||
return base
|
return base;
|
||||||
})
|
});
|
||||||
|
|
||||||
const csvRows = [headers, ...data]
|
const csvRows = [headers, ...data];
|
||||||
const csv = csvRows.map(r => r.map(c => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',')).join('\n')
|
const csv = csvRows.map((r) => r.map((c) => `"${String(c ?? "").replace(/"/g, '""')}"`).join(",")).join("\n");
|
||||||
|
|
||||||
const BOM = '\uFEFF'
|
const BOM = "\uFEFF";
|
||||||
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' })
|
const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8;" });
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a')
|
const a = document.createElement("a");
|
||||||
a.href = url
|
a.href = url;
|
||||||
a.download = `bao-cao-cong-no-${$dayjs().format('YYYYMMDD')}.csv`
|
a.download = `bao-cao-cong-no-${$dayjs().format("YYYYMMDD")}.csv`;
|
||||||
a.click()
|
a.click();
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => loadData())
|
onMounted(() => loadData());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -578,4 +640,4 @@ onMounted(() => loadData())
|
|||||||
.fixed-col {
|
.fixed-col {
|
||||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.08);
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<DataView v-bind="{api: 'internalaccount', setting: store.lang==='en'? 'internal-account-en' : 'internal-account', pagename: pagename,
|
<DataView
|
||||||
modal: {title: 'Tài khoản', component: 'accounting/AccountView', width: '50%', 'height': '300px'}}" />
|
v-bind="{
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" />
|
api: 'internalaccount',
|
||||||
</div>
|
setting: store.lang === 'en' ? 'internal-account-en' : 'internal-account',
|
||||||
|
pagename: pagename,
|
||||||
|
modal: {
|
||||||
|
title: 'Tài khoản',
|
||||||
|
component: 'accounting/AccountView',
|
||||||
|
width: '50%',
|
||||||
|
height: '300px',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { useStore } from '~/stores/index'
|
import { useStore } from "~/stores/index";
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
return {store}
|
return { store };
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
pagename: 'pagedata32'
|
pagename: "pagedata32",
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
deposit() {
|
deposit() {
|
||||||
this.showmodal = {component: 'accounting/InternalDeposit', title: 'Nộp tiền tài khoản nội bộ', width: '40%', height: '300px',
|
this.showmodal = {
|
||||||
vbind: {pagename: this.pagename}}
|
component: "accounting/InternalDeposit",
|
||||||
|
title: "Nộp tiền tài khoản nội bộ",
|
||||||
|
width: "40%",
|
||||||
|
height: "300px",
|
||||||
|
vbind: { pagename: this.pagename },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
doClick() {
|
doClick() {
|
||||||
this.$approvalcode()
|
this.$approvalcode();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,176 +1,288 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="columns is-multiline mx-0">
|
<div class="columns is-multiline mx-0">
|
||||||
<div class="column is-8">
|
<div class="column is-8">
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $lang('account') }}<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<SearchBox v-bind="{api:'internalaccount', field:'label', column:['label'], first: true, optionid: record.account}"
|
|
||||||
:disabled="record.account" @option="selected('_account', $event)" v-if="!record.id"></SearchBox>
|
|
||||||
<span v-else>{{record.account__code}}</span>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors._account">{{ errors._account }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label">{{ $lang("account") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
>Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label
|
<div class="control">
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
api: 'internalaccount',
|
||||||
|
field: 'label',
|
||||||
|
column: ['label'],
|
||||||
|
first: true,
|
||||||
|
optionid: record.account,
|
||||||
|
}"
|
||||||
|
:disabled="record.account"
|
||||||
|
@option="selected('_account', $event)"
|
||||||
|
v-if="!record.id"
|
||||||
|
></SearchBox>
|
||||||
|
<span v-else>{{ record.account__code }}</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors._account"
|
||||||
>
|
>
|
||||||
|
{{ errors._account }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<Datepicker
|
<Datepicker
|
||||||
v-bind="{ record: record, attr: 'date', maxdate: new Date()}"
|
v-bind="{ record: record, attr: 'date', maxdate: new Date() }"
|
||||||
@date="selected('date', $event)"
|
@date="selected('date', $event)"
|
||||||
></Datepicker>
|
></Datepicker>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_date"
|
||||||
|
>
|
||||||
|
{{ errors.issued_date }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
|
||||||
<label class="label">Thu / chi<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<SearchBox v-bind="{api:'entrytype', field:'name', column:['name'], first: true, optionid: record.type}"
|
|
||||||
:disabled="record.type" @option="selected('_type', $event)" v-if="!record.id"></SearchBox>
|
|
||||||
<span v-else>{{record.type__name}}</span>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{$lang('amount-only')}}<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<InputNumber v-bind="{record: record, attr: 'amount', placeholder: ''}" @number="selected('amount', $event)"></InputNumber>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors.amount">{{errors.amount}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Phương thức thanh toán<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<SearchBox v-bind="{api:'entrycategory', field:'name', column:['name'], first: true, optionid: record.type}"
|
|
||||||
:disabled="record.type" @option="selected('_category', $event)" v-if="!record.id"></SearchBox>
|
|
||||||
<span v-else>{{record.type__name}}</span>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Sản phẩm<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<SearchBox v-bind="{
|
|
||||||
api: 'product',
|
|
||||||
field: 'label',
|
|
||||||
searchfield: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
|
||||||
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
|
||||||
first: true
|
|
||||||
}" @option="selected('product', $event)" />
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Khách hàng<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<SearchBox v-bind="{
|
|
||||||
api: 'customer',
|
|
||||||
field: 'label',
|
|
||||||
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
|
|
||||||
column: ['code', 'fullname', 'phone', 'legal_code'],
|
|
||||||
first: true
|
|
||||||
}" @option="selected('customer', $event)" />
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-12">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $lang('content') }}<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<textarea class="textarea" rows="2" v-model="record.content"></textarea>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors.content">{{errors.content}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Mã tham chiếu</label>
|
<label class="label">Thu / chi<b class="ml-1 has-text-danger">*</b></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input has-text-black" type="text" placeholder="Tối đa 30 ký tự" v-model="record.ref">
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
api: 'entrytype',
|
||||||
|
field: 'name',
|
||||||
|
column: ['name'],
|
||||||
|
first: true,
|
||||||
|
optionid: record.type,
|
||||||
|
}"
|
||||||
|
:disabled="record.type"
|
||||||
|
@option="selected('_type', $event)"
|
||||||
|
v-if="!record.id"
|
||||||
|
></SearchBox>
|
||||||
|
<span v-else>{{ record.type__name }}</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors._type"
|
||||||
|
>
|
||||||
|
{{ errors._type }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $lang("amount-only") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<InputNumber
|
||||||
|
v-bind="{ record: record, attr: 'amount', placeholder: '' }"
|
||||||
|
@number="selected('amount', $event)"
|
||||||
|
></InputNumber>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.amount"
|
||||||
|
>
|
||||||
|
{{ errors.amount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Phương thức thanh toán<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
api: 'entrycategory',
|
||||||
|
field: 'name',
|
||||||
|
column: ['name'],
|
||||||
|
first: true,
|
||||||
|
optionid: record.type,
|
||||||
|
}"
|
||||||
|
:disabled="record.type"
|
||||||
|
@option="selected('_category', $event)"
|
||||||
|
v-if="!record.id"
|
||||||
|
></SearchBox>
|
||||||
|
<span v-else>{{ record.type__name }}</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors._type"
|
||||||
|
>
|
||||||
|
{{ errors._type }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Sản phẩm<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
api: 'product',
|
||||||
|
field: 'label',
|
||||||
|
searchfield: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
||||||
|
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
||||||
|
first: true,
|
||||||
|
}"
|
||||||
|
@option="selected('product', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors._type"
|
||||||
|
>
|
||||||
|
{{ errors._type }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Khách hàng<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
api: 'customer',
|
||||||
|
field: 'label',
|
||||||
|
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
|
||||||
|
column: ['code', 'fullname', 'phone', 'legal_code'],
|
||||||
|
first: true,
|
||||||
|
}"
|
||||||
|
@option="selected('customer', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors._type"
|
||||||
|
>
|
||||||
|
{{ errors._type }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-12">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $lang("content") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
rows="2"
|
||||||
|
v-model="record.content"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.content"
|
||||||
|
>
|
||||||
|
{{ errors.content }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Mã tham chiếu</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input has-text-black"
|
||||||
|
type="text"
|
||||||
|
placeholder="Tối đa 30 ký tự"
|
||||||
|
v-model="record.ref"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="column is-12"
|
||||||
|
v-if="entry"
|
||||||
|
>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Chứng từ đi kèm (nếu có)</label>
|
||||||
|
<div class="control">
|
||||||
|
<FileGallery
|
||||||
|
v-bind="{
|
||||||
|
row: entry,
|
||||||
|
pagename: pagename,
|
||||||
|
api: 'entryfile',
|
||||||
|
info: false,
|
||||||
|
}"
|
||||||
|
></FileGallery>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-5 ml-3"
|
||||||
|
v-if="!entry"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
:class="['button is-primary has-text-white mr-2', isUpdating && 'is-loading']"
|
||||||
|
@click="confirm()"
|
||||||
|
>
|
||||||
|
{{ $lang("confirm") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
@close="showContractModal = undefined"
|
||||||
|
v-bind="showContractModal"
|
||||||
|
v-if="showContractModal"
|
||||||
|
></Modal>
|
||||||
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
@confirm="update()"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-12" v-if="entry">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Chứng từ đi kèm (nếu có)</label>
|
|
||||||
<div class="control">
|
|
||||||
<FileGallery v-bind="{row: entry, pagename: pagename, api: 'entryfile', info: false}"></FileGallery>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 ml-3" v-if="!entry">
|
|
||||||
<button
|
|
||||||
:class="[
|
|
||||||
'button is-primary has-text-white mr-2',
|
|
||||||
isUpdating && 'is-loading'
|
|
||||||
]"
|
|
||||||
@click="confirm()">{{$lang('confirm')}}</button>
|
|
||||||
</div>
|
|
||||||
<Modal @close="showContractModal=undefined" v-bind="showContractModal" v-if="showContractModal"></Modal>
|
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" @confirm="update()" v-if="showmodal"></Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { useStore } from '~/stores/index'
|
import { useStore } from "~/stores/index";
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
return {store}
|
return { store };
|
||||||
},
|
},
|
||||||
props: ['pagename', 'row', 'option'],
|
props: ["pagename", "row", "option"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
record: {date: this.$dayjs().format('YYYY-MM-DD')},
|
record: { date: this.$dayjs().format("YYYY-MM-DD") },
|
||||||
errors: {},
|
errors: {},
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
showContractModal: undefined,
|
showContractModal: undefined,
|
||||||
entry: undefined
|
entry: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if(!this.option) return
|
if (!this.option) return;
|
||||||
this.record.account = this.option.account
|
this.record.account = this.option.account;
|
||||||
this.record.type = this.option.type
|
this.record.type = this.option.type;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selected(attr, obj) {
|
selected(attr, obj) {
|
||||||
this.record[attr] = obj
|
this.record[attr] = obj;
|
||||||
this.record = this.$copy(this.record)
|
this.record = this.$copy(this.record);
|
||||||
},
|
},
|
||||||
checkError() {
|
checkError() {
|
||||||
this.errors = {}
|
this.errors = {};
|
||||||
if(this.$empty(this.record._account)) this.errors._account = 'Chưa chọn tài khoản'
|
if (this.$empty(this.record._account)) this.errors._account = "Chưa chọn tài khoản";
|
||||||
if(this.$empty(this.record._type)) this.errors._type = 'Chưa chọn loại hạch toán'
|
if (this.$empty(this.record._type)) this.errors._type = "Chưa chọn loại hạch toán";
|
||||||
if(this.$empty(this.record.amount)) this.errors.amount = 'Chưa nhập số tiền'
|
if (this.$empty(this.record.amount)) this.errors.amount = "Chưa nhập số tiền";
|
||||||
else if(this.$formatNumber(this.record.amount)<=0) this.errors.amount = 'Số tiền phải > 0'
|
else if (this.$formatNumber(this.record.amount) <= 0) this.errors.amount = "Số tiền phải > 0";
|
||||||
if(this.$empty(this.record.content)) this.errors.content = 'Chưa nhập nội dung'
|
if (this.$empty(this.record.content)) this.errors.content = "Chưa nhập nội dung";
|
||||||
if(Object.keys(this.errors).length>0) return true
|
if (Object.keys(this.errors).length > 0) return true;
|
||||||
if(this.record._type.code==='DR' && (this.record._account.balance<this.$formatNumber(this.record.amount))) {
|
if (this.record._type.code === "DR" && this.record._account.balance < this.$formatNumber(this.record.amount)) {
|
||||||
this.errors._account = 'Số dư tài khoản không đủ để trích nợ'
|
this.errors._account = "Số dư tài khoản không đủ để trích nợ";
|
||||||
}
|
}
|
||||||
return Object.keys(this.errors).length>0
|
return Object.keys(this.errors).length > 0;
|
||||||
},
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
if(this.checkError()) return
|
if (this.checkError()) return;
|
||||||
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10},
|
this.showmodal = {
|
||||||
title: this.$lang('confirm'), width: '500px', height: '100px'}
|
component: `dialog/Confirm`,
|
||||||
|
vbind: { content: this.$lang("confirm-action"), duration: 10 },
|
||||||
|
title: this.$lang("confirm"),
|
||||||
|
width: "500px",
|
||||||
|
height: "100px",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
async update() {
|
async update() {
|
||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
@@ -179,28 +291,29 @@ export default {
|
|||||||
amount: this.record.amount,
|
amount: this.record.amount,
|
||||||
content: this.record.content,
|
content: this.record.content,
|
||||||
type: this.record._type.code,
|
type: this.record._type.code,
|
||||||
category: this.record._category ? this.record._category.id : 1, user: this.store.login.id,
|
category: this.record._category ? this.record._category.id : 1,
|
||||||
ref: this.row ? this.row.code : (!this.$empty(this.record.ref) ? this.record.ref.trim() : null),
|
user: this.store.login.id,
|
||||||
|
ref: this.row ? this.row.code : !this.$empty(this.record.ref) ? this.record.ref.trim() : null,
|
||||||
customer: this.record.customer ? this.record.customer.id : null,
|
customer: this.record.customer ? this.record.customer.id : null,
|
||||||
product: this.record.product ? this.record.product.id : null,
|
product: this.record.product ? this.record.product.id : null,
|
||||||
date: this.$empty(this.record.date) ? null : this.record.date
|
date: this.$empty(this.record.date) ? null : this.record.date,
|
||||||
}
|
};
|
||||||
let rs1 = await this.$insertapi('accountentry', obj1, undefined, false)
|
let rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
|
||||||
if(rs1==='error') return
|
if (rs1 === "error") return;
|
||||||
|
|
||||||
if (this.record._category.id === 2) {
|
if (this.record._category.id === 2) {
|
||||||
const genDoc = await this.$generateDocument({
|
const genDoc = await this.$generateDocument({
|
||||||
doc_code: 'PHIEU_THU_TIEN_MAT',
|
doc_code: "PHIEU_THU_TIEN_MAT",
|
||||||
entry_id: rs1.id,
|
entry_id: rs1.id,
|
||||||
output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}`
|
output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.$insertapi('file', {
|
await this.$insertapi("file", {
|
||||||
name: genDoc.data.pdf,
|
name: genDoc.data.pdf,
|
||||||
user: this.store.login.id,
|
user: this.store.login.id,
|
||||||
type: 4,
|
type: 4,
|
||||||
size: 1000,
|
size: 1000,
|
||||||
file: genDoc.data.pdf // or genDoc.data.pdf
|
file: genDoc.data.pdf, // or genDoc.data.pdf
|
||||||
});
|
});
|
||||||
|
|
||||||
this.showContractModal = {
|
this.showContractModal = {
|
||||||
@@ -209,20 +322,27 @@ export default {
|
|||||||
width: "95%",
|
width: "95%",
|
||||||
height: "95vh",
|
height: "95vh",
|
||||||
vbind: {
|
vbind: {
|
||||||
directDocument: genDoc.data
|
directDocument: genDoc.data,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entry = rs1
|
this.entry = rs1;
|
||||||
if(this.pagename) {
|
if (this.pagename) {
|
||||||
let data = await this.$getdata('internalaccount', {code__in: [this.record._account.code]})
|
let data = await this.$getdata("internalaccount", {
|
||||||
this.$updatepage(this.pagename, data)
|
code__in: [this.record._account.code],
|
||||||
|
});
|
||||||
|
this.$updatepage(this.pagename, data);
|
||||||
}
|
}
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
this.$emit('modalevent', {name: 'entry', data: rs1})
|
this.$emit("modalevent", { name: "entry", data: rs1 });
|
||||||
this.$dialog(`Hạch toán <b>${this.record._type.name}</b> số tiền <b>${this.$numtoString(this.record.amount)}</b> vào tài khoản <b>${this.record._account.code}</b> thành công.`, 'Thành công', 'Success', 10)
|
this.$dialog(
|
||||||
}
|
`Hạch toán <b>${this.record._type.name}</b> số tiền <b>${this.$numtoString(this.record.amount)}</b> vào tài khoản <b>${this.record._account.code}</b> thành công.`,
|
||||||
}
|
"Thành công",
|
||||||
}
|
"Success",
|
||||||
</script>
|
10,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,247 +1,326 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="record" id="printable">
|
<div
|
||||||
<Caption v-bind="{ title: $lang('info') }" />
|
v-if="record"
|
||||||
<div class="columns is-multiline is-2 m-0">
|
id="printable"
|
||||||
<div class="column is-3">
|
>
|
||||||
<div class="field">
|
<Caption v-bind="{ title: $lang('info') }" />
|
||||||
<label class="label">{{ $lang('code') }}:</label>
|
<div class="columns is-multiline is-2 m-0">
|
||||||
<div class="control">
|
<div class="column is-3">
|
||||||
<span>{{ record.code }}</span>
|
<div class="field">
|
||||||
</div>
|
<label class="label">{{ $lang("code") }}:</label>
|
||||||
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p>
|
<div class="control">
|
||||||
</div>
|
<span>{{ record.code }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<p
|
||||||
<div class="field">
|
class="help is-danger"
|
||||||
<label class="label">Tài khoản:</label>
|
v-if="errors.type"
|
||||||
<div class="control">
|
>
|
||||||
<span>{{ record.account__code }}</span>
|
{{ errors.type }}
|
||||||
</div>
|
</p>
|
||||||
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="column is-3">
|
||||||
<div class="column is-3">
|
<div class="field">
|
||||||
<div class="field">
|
<label class="label">Tài khoản:</label>
|
||||||
<label class="label">Ngày hạch toán:</label>
|
<div class="control">
|
||||||
<div class="control">
|
<span>{{ record.account__code }}</span>
|
||||||
{{ $dayjs(record.date).format('DD/MM/YYYY') }}
|
</div>
|
||||||
</div>
|
<p
|
||||||
</div>
|
class="help is-danger"
|
||||||
</div>
|
v-if="errors.type"
|
||||||
<div class="column is-3">
|
>
|
||||||
<div class="field">
|
{{ errors.type }}
|
||||||
<label class="label">{{ $lang('amount-only') }}:</label>
|
</p>
|
||||||
<div class="control">
|
</div>
|
||||||
{{ $numtoString(record.amount) }}
|
</div>
|
||||||
</div>
|
<div class="column is-3">
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<label class="label">Ngày hạch toán:</label>
|
||||||
<div class="column is-3">
|
<div class="control">
|
||||||
<div class="field">
|
{{ $dayjs(record.date).format("DD/MM/YYYY") }}
|
||||||
<label class="label">Thu / chi:</label>
|
</div>
|
||||||
<div class="control">
|
</div>
|
||||||
{{ record.type__name }}
|
</div>
|
||||||
</div>
|
<div class="column is-3">
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<label class="label">{{ $lang("amount-only") }}:</label>
|
||||||
<div class="column is-3">
|
<div class="control">
|
||||||
<div class="field">
|
{{ $numtoString(record.amount) }}
|
||||||
<label class="label">Dư trước:</label>
|
</div>
|
||||||
<div class="control">
|
</div>
|
||||||
{{ $numtoString(record.balance_before) }}
|
</div>
|
||||||
</div>
|
<div class="column is-3">
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<label class="label">Thu / chi:</label>
|
||||||
<div class="column is-3">
|
<div class="control">
|
||||||
<div class="field">
|
{{ record.type__name }}
|
||||||
<label class="label">Dư sau:</label>
|
</div>
|
||||||
<div class="control">
|
</div>
|
||||||
{{ $numtoString(record.balance_after) }}
|
</div>
|
||||||
</div>
|
<div class="column is-3">
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<label class="label">Dư trước:</label>
|
||||||
<div class="column is-3">
|
<div class="control">
|
||||||
<div class="field">
|
{{ $numtoString(record.balance_before) }}
|
||||||
<label class="label">Mã sản phẩm:</label>
|
</div>
|
||||||
<div class="control">
|
</div>
|
||||||
{{ record.product__trade_code }}
|
</div>
|
||||||
</div>
|
<div class="column is-3">
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<label class="label">Dư sau:</label>
|
||||||
<div class="column is-3">
|
<div class="control">
|
||||||
<div class="field">
|
{{ $numtoString(record.balance_after) }}
|
||||||
<label class="label">Mã khách hàng:</label>
|
</div>
|
||||||
<div class="control">
|
</div>
|
||||||
{{ record.customer__code }}
|
</div>
|
||||||
</div>
|
<div class="column is-3">
|
||||||
</div>
|
<div class="field">
|
||||||
</div>
|
<label class="label">Mã 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">Mã khách hàng:</label>
|
||||||
|
<div class="control">
|
||||||
|
{{ record.customer__code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Người hạch toán:</label>
|
<label class="label">Người hạch toán:</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{{ `${record.inputer__fullname}` }}
|
{{ `${record.inputer__fullname}` }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-3">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $lang('time') }}:</label>
|
|
||||||
<div class="control">
|
|
||||||
{{ `${$dayjs(record.create_time).format('DD/MM/YYYY')}` }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-3">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Ref:</label>
|
|
||||||
<div class="control">
|
|
||||||
{{ `${record.ref || '/'}` }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-8">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $lang('content') }}:</label>
|
|
||||||
<div class="control">
|
|
||||||
{{ `${record.content}` }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- PHẦN THÔNG TIN PHÂN BỔ -->
|
<div class="column is-3">
|
||||||
<Caption v-bind="{ title: 'Thông tin phân bổ' }" />
|
<div class="field">
|
||||||
<!-- BẢNG CHI TIẾT PHÂN BỔ -->
|
<label class="label">{{ $lang("time") }}:</label>
|
||||||
<div v-if="record.allocation_detail && record.allocation_detail.length > 0" class="mt-4">
|
<div class="control">
|
||||||
<div class="table-container">
|
{{ `${$dayjs(record.create_time).format("DD/MM/YYYY")}` }}
|
||||||
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
|
</div>
|
||||||
<thead>
|
|
||||||
<tr class="">
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">STT</th>
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">Mã lịch</th>
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">Loại</th>
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">Tổng phân bổ</th>
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">Gốc</th>
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">Phạt</th>
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">Miễn lãi</th>
|
|
||||||
<th class="has-background-primary has-text-white has-text-centered">Ngày phân bổ</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="(item, index) in record.allocation_detail" :key="index">
|
|
||||||
<td class="has-text-centered">{{ index + 1 }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="tag is-link is-light">{{ item.schedule_code || item.schedule_id }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="has-text-centered">
|
|
||||||
<span v-if="item.type === 'REDUCTION'" class="tag is-warning">Miễn lãi</span>
|
|
||||||
<span v-else class="tag is-success">Thanh toán</span>
|
|
||||||
</td>
|
|
||||||
<td class="has-text-right">
|
|
||||||
<strong>{{ $numtoString(item.amount) }}</strong>
|
|
||||||
</td>
|
|
||||||
<td class="has-text-right">
|
|
||||||
<span v-if="item.principal" class="has-text-info has-text-weight-semibold">
|
|
||||||
{{ $numtoString(item.principal) }}
|
|
||||||
</span>
|
|
||||||
<span v-else class="has-text-grey-light">-</span>
|
|
||||||
</td>
|
|
||||||
<td class="has-text-right">
|
|
||||||
<span v-if="item.penalty" class="has-text-danger has-text-weight-semibold">
|
|
||||||
{{ $numtoString(item.penalty) }}
|
|
||||||
</span>
|
|
||||||
<span v-else class="has-text-grey-light">-</span>
|
|
||||||
</td>
|
|
||||||
<td class="has-text-right">
|
|
||||||
<span v-if="item.penalty" class="has-text-danger has-text-weight-semibold">
|
|
||||||
{{ $numtoString(item.penalty_reduce) }}
|
|
||||||
</span>
|
|
||||||
<span v-else class="has-text-grey-light">-</span>
|
|
||||||
</td>
|
|
||||||
<td class="has-text-centered">{{ $dayjs(item.date).format('DD/MM/YYYY HH:mm:ss') }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr class="has-background-light">
|
|
||||||
<td colspan="3" class="has-text-right has-text-weight-bold">Tổng cộng:</td>
|
|
||||||
<td class="has-text-right has-text-weight-bold">{{ $numtoString(totalAllocated) }}</td>
|
|
||||||
<td class="has-text-right has-text-weight-bold has-text-info">{{
|
|
||||||
$numtoString(totalPrincipal) }}</td>
|
|
||||||
<td class="has-text-right has-text-weight-bold has-text-danger">{{
|
|
||||||
$numtoString(totalPenalty) }}</td>
|
|
||||||
<td class="has-text-right has-text-weight-bold has-text-danger">{{
|
|
||||||
$numtoString(totalPenaltyReduce) }}</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="notification is-info is-light mt-4">
|
</div>
|
||||||
<p class="has-text-centered">Chưa có dữ liệu phân bổ cho bút toán này.</p>
|
<div class="column is-3">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Ref:</label>
|
||||||
|
<div class="control">
|
||||||
|
{{ `${record.ref || "/"}` }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Caption class="mt-5 " v-bind="{ title: 'Chứng từ' }"></Caption>
|
<div class="column is-8">
|
||||||
<FileGallery v-bind="{ row: record, api: 'entryfile' }"></FileGallery>
|
<div class="field">
|
||||||
<div class="mt-5" id="ignore">
|
<label class="label">{{ $lang("content") }}:</label>
|
||||||
<button class="button is-primary has-text-white mr-2" @click="$exportpdf('printable', record.code, 'a4', 'landscape')">{{ $lang('print') }}</button>
|
<div class="control">
|
||||||
<button v-if="record.category === 2" class="button is-light" @click="viewPhieuThuTienMat">Xem phiếu thu</button>
|
{{ `${record.content}` }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PHẦN THÔNG TIN PHÂN BỔ -->
|
||||||
|
<Caption v-bind="{ title: 'Thông tin phân bổ' }" />
|
||||||
|
<!-- BẢNG CHI TIẾT PHÂN BỔ -->
|
||||||
|
<div
|
||||||
|
v-if="record.allocation_detail && record.allocation_detail.length > 0"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr class="">
|
||||||
|
<th class="has-background-primary has-text-white has-text-centered">STT</th>
|
||||||
|
<th class="has-background-primary has-text-white has-text-centered">Mã 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 có dữ liệu phân bổ cho bút toán này.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Caption
|
||||||
|
class="mt-5"
|
||||||
|
v-bind="{ title: 'Chứng từ' }"
|
||||||
|
></Caption>
|
||||||
|
<FileGallery v-bind="{ row: record, api: 'entryfile' }"></FileGallery>
|
||||||
|
<div
|
||||||
|
class="mt-5"
|
||||||
|
id="ignore"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white mr-2"
|
||||||
|
@click="$exportpdf('printable', record.code, 'a4', 'landscape')"
|
||||||
|
>
|
||||||
|
{{ $lang("print") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="record.category === 2"
|
||||||
|
class="button is-light"
|
||||||
|
@click="viewPhieuThuTienMat"
|
||||||
|
>
|
||||||
|
Xem phiếu thu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['row'],
|
props: ["row"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
errors: {},
|
errors: {},
|
||||||
record: undefined
|
record: undefined,
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.record = await this.$getdata("internalentry", { code: this.row.code }, undefined, true);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// Tính tổng số tiền đã phân bổ
|
||||||
|
totalAllocated() {
|
||||||
|
if (!this.record || !this.record.allocation_detail) return 0;
|
||||||
|
return this.record.allocation_detail.reduce((sum, item) => sum + (item.amount || 0), 0);
|
||||||
},
|
},
|
||||||
async created() {
|
// Tính tổng gốc
|
||||||
this.record = await this.$getdata('internalentry', { code: this.row.code }, undefined, true)
|
totalPrincipal() {
|
||||||
|
if (!this.record || !this.record.allocation_detail) return 0;
|
||||||
|
return this.record.allocation_detail.reduce((sum, item) => sum + (item.principal || 0), 0);
|
||||||
},
|
},
|
||||||
computed: {
|
// Tính tổng phạt
|
||||||
|
totalPenalty() {
|
||||||
// Tính tổng số tiền đã phân bổ
|
if (!this.record || !this.record.allocation_detail) return 0;
|
||||||
totalAllocated() {
|
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty || 0), 0);
|
||||||
if (!this.record || !this.record.allocation_detail) return 0
|
|
||||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.amount || 0), 0)
|
|
||||||
},
|
|
||||||
// Tính tổng gốc
|
|
||||||
totalPrincipal() {
|
|
||||||
if (!this.record || !this.record.allocation_detail) return 0
|
|
||||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.principal || 0), 0)
|
|
||||||
},
|
|
||||||
// Tính tổng phạt
|
|
||||||
totalPenalty() {
|
|
||||||
if (!this.record || !this.record.allocation_detail) return 0
|
|
||||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty || 0), 0)
|
|
||||||
},
|
|
||||||
// Tính tổng phạt đã giảm
|
|
||||||
totalPenaltyReduce() {
|
|
||||||
if (!this.record || !this.record.allocation_detail) return 0
|
|
||||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty_reduce || 0), 0)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
// Tính tổng phạt đã giảm
|
||||||
selected(attr, obj) {
|
totalPenaltyReduce() {
|
||||||
this.record[attr] = obj
|
if (!this.record || !this.record.allocation_detail) return 0;
|
||||||
this.record = this.$copy(this.record)
|
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty_reduce || 0), 0);
|
||||||
if (attr === '_type') this.category = obj.category__code
|
},
|
||||||
},
|
},
|
||||||
viewPhieuThuTienMat() {
|
methods: {
|
||||||
const url = `${this.$getpath()}static/contract/PHIEU_THU_TIEN_MAT-${this.record.code}.pdf`;
|
selected(attr, obj) {
|
||||||
window.open(url, '_blank');
|
this.record[attr] = obj;
|
||||||
}
|
this.record = this.$copy(this.record);
|
||||||
}
|
if (attr === "_type") this.category = obj.category__code;
|
||||||
}
|
},
|
||||||
|
viewPhieuThuTienMat() {
|
||||||
|
const url = `${this.$getpath()}static/contract/PHIEU_THU_TIEN_MAT-${this.record.code}.pdf`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.column {
|
.column {
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,104 +1,191 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="columns is-multiline mx-0">
|
<div class="columns is-multiline mx-0">
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{$lang('source-account')}}<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label">{{ $lang("source-account") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SearchBox v-bind="{api:'internalaccount', field:'label', column:['label'], first: true, optionid: row.id}"
|
<SearchBox
|
||||||
@option="selected('_source', $event)" v-if="!record.id"></SearchBox>
|
v-bind="{
|
||||||
<span v-else>{{record.account__code}}</span>
|
api: 'internalaccount',
|
||||||
</div>
|
field: 'label',
|
||||||
<p class="help is-danger" v-if="errors.source">{{ errors.source }}</p>
|
column: ['label'],
|
||||||
</div>
|
first: true,
|
||||||
</div>
|
optionid: row.id,
|
||||||
<div class="column is-12">
|
}"
|
||||||
<div class="field">
|
@option="selected('_source', $event)"
|
||||||
<label class="label">{{$lang('dest-account')}}<b class="ml-1 has-text-danger">*</b></label>
|
v-if="!record.id"
|
||||||
<div class="control">
|
></SearchBox>
|
||||||
<SearchBox v-bind="vbind" @option="selected('_target', $event)" v-if="vbind"></SearchBox>
|
<span v-else>{{ record.account__code }}</span>
|
||||||
<span v-else>{{record.account__code}}</span>
|
</div>
|
||||||
</div>
|
<p
|
||||||
<p class="help is-danger" v-if="errors.target">{{ errors.target }}</p>
|
class="help is-danger"
|
||||||
</div>
|
v-if="errors.source"
|
||||||
</div>
|
>
|
||||||
<div class="column is-12">
|
{{ errors.source }}
|
||||||
<div class="field">
|
</p>
|
||||||
<label class="label">{{ $lang('amount-only') }}<b class="ml-1 has-text-danger">*</b></label>
|
</div>
|
||||||
<div class="control">
|
</div>
|
||||||
<InputNumber v-bind="{record: record, attr: 'amount', placeholder: ''}" @number="selected('amount', $event)"></InputNumber>
|
<div class="column is-12">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $lang("dest-account") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<SearchBox
|
||||||
|
v-bind="vbind"
|
||||||
|
@option="selected('_target', $event)"
|
||||||
|
v-if="vbind"
|
||||||
|
></SearchBox>
|
||||||
|
<span v-else>{{ record.account__code }}</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.target"
|
||||||
|
>
|
||||||
|
{{ errors.target }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-12">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $lang("amount-only") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<InputNumber
|
||||||
|
v-bind="{ record: record, attr: 'amount', placeholder: '' }"
|
||||||
|
@number="selected('amount', $event)"
|
||||||
|
></InputNumber>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.amount"
|
||||||
|
>
|
||||||
|
{{ errors.amount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-12">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ $lang("content") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
rows="2"
|
||||||
|
v-model="record.content"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.content"
|
||||||
|
>
|
||||||
|
{{ errors.content }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.amount">{{errors.amount}}</p>
|
<div class="mt-5">
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="confirm()"
|
||||||
|
>
|
||||||
|
{{ $lang("confirm") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
@confirm="update()"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-12">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{ $lang('content') }}<b class="ml-1 has-text-danger">*</b></label>
|
|
||||||
<div class="control">
|
|
||||||
<textarea class="textarea" rows="2" v-model="record.content"></textarea>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors.content">{{errors.content}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<button class="button is-primary has-text-white" @click="confirm()">{{ $lang('confirm') }}</button>
|
|
||||||
</div>
|
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" @confirm="update()" v-if="showmodal"></Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['pagename', 'row'],
|
props: ["pagename", "row"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
record: {},
|
record: {},
|
||||||
errors: {},
|
errors: {},
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
vbind: undefined
|
vbind: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selected(attr, obj) {
|
selected(attr, obj) {
|
||||||
this.record[attr] = obj
|
this.record[attr] = obj;
|
||||||
this.record = this.$copy(this.record)
|
this.record = this.$copy(this.record);
|
||||||
if(attr==='_source') {
|
if (attr === "_source") {
|
||||||
let currency = obj? obj.currency : undefined
|
let currency = obj ? obj.currency : undefined;
|
||||||
this.vbind = undefined
|
this.vbind = undefined;
|
||||||
setTimeout(()=>this.vbind = {api:'internalaccount', field:'label', column:['label'], first: true, filter: {currency: currency}})
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
(this.vbind = {
|
||||||
|
api: "internalaccount",
|
||||||
|
field: "label",
|
||||||
|
column: ["label"],
|
||||||
|
first: true,
|
||||||
|
filter: { currency: currency },
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkError() {
|
checkError() {
|
||||||
this.errors = {}
|
this.errors = {};
|
||||||
if(this.$empty(this.record._source)) this.errors.source = 'Chưa chọn tài khoản nguồn'
|
if (this.$empty(this.record._source)) this.errors.source = "Chưa chọn tài khoản nguồn";
|
||||||
if(this.$empty(this.record._target)) this.errors.target = 'Chưa chọn tài khoản đích'
|
if (this.$empty(this.record._target)) this.errors.target = "Chưa chọn tài khoản đích";
|
||||||
if(Object.keys(this.errors).length===0) {
|
if (Object.keys(this.errors).length === 0) {
|
||||||
if(this.record._source.id===this.record._target.id) this.errors.target = 'Tài khoản nguồn phải khác tài khoản đích'
|
if (this.record._source.id === this.record._target.id)
|
||||||
|
this.errors.target = "Tài khoản nguồn phải khác tài khoản đích";
|
||||||
}
|
}
|
||||||
if(this.$empty(this.record.amount)) this.errors.amount = 'Chưa nhập số tiền'
|
if (this.$empty(this.record.amount)) this.errors.amount = "Chưa nhập số tiền";
|
||||||
else if(this.$formatNumber(this.record.amount)<=0) this.errors.amount = 'Số tiền phải > 0'
|
else if (this.$formatNumber(this.record.amount) <= 0) this.errors.amount = "Số tiền phải > 0";
|
||||||
else if(this.record._source.balance<this.$formatNumber(this.record.amount)) this.errors.source = 'Tài khoản nguồn không đủ số dư để điều chuyển'
|
else if (this.record._source.balance < this.$formatNumber(this.record.amount))
|
||||||
if(this.$empty(this.record.content)) this.errors.content = 'Chưa nhập nội dung'
|
this.errors.source = "Tài khoản nguồn không đủ số dư để điều chuyển";
|
||||||
return Object.keys(this.errors).length>0
|
if (this.$empty(this.record.content)) this.errors.content = "Chưa nhập nội dung";
|
||||||
|
return Object.keys(this.errors).length > 0;
|
||||||
},
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
if(this.checkError()) return
|
if (this.checkError()) return;
|
||||||
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10},
|
this.showmodal = {
|
||||||
title: this.$lang('confirm'), width: '500px', height: '100px'}
|
component: `dialog/Confirm`,
|
||||||
|
vbind: { content: this.$lang("confirm-action"), duration: 10 },
|
||||||
|
title: this.$lang("confirm"),
|
||||||
|
width: "500px",
|
||||||
|
height: "100px",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
async update() {
|
async update() {
|
||||||
let content = `${this.record.content} (${this.record._source.code} -> ${this.record._target.code})`
|
let content = `${this.record.content} (${this.record._source.code} -> ${this.record._target.code})`;
|
||||||
let obj1 = {code: this.record._source.code, amount: this.record.amount, content: content, type: 'DR', category: 2, user: this.$store.login.id}
|
let obj1 = {
|
||||||
let rs1 = await this.$insertapi('accountentry', obj1, undefined, false)
|
code: this.record._source.code,
|
||||||
if(rs1==='error') return
|
amount: this.record.amount,
|
||||||
let obj2 = {code: this.record._target.code, amount: this.record.amount, content: content, type: 'CR', category: 2, user: this.$store.login.id}
|
content: content,
|
||||||
let rs2 = await this.$insertapi('accountentry', obj2, undefined, false)
|
type: "DR",
|
||||||
if(rs2==='error') return
|
category: 2,
|
||||||
let data = await this.$getdata('internalaccount', {code__in: [this.record._source.code, this.record._target.code]})
|
user: this.$store.login.id,
|
||||||
this.$updatepage(this.pagename, data)
|
};
|
||||||
this.$dialog(`Điều chuyển vốn <b>${this.$numtoString(this.record.amount)}</b> từ <b>${this.record._source.code}</b> tới <b>${this.record._target.code}</b> thành công.`, 'Thành công', 'Success', 10)
|
let rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
|
||||||
this.$emit('close')
|
if (rs1 === "error") return;
|
||||||
}
|
let obj2 = {
|
||||||
}
|
code: this.record._target.code,
|
||||||
}
|
amount: this.record.amount,
|
||||||
</script>
|
content: content,
|
||||||
|
type: "CR",
|
||||||
|
category: 2,
|
||||||
|
user: this.$store.login.id,
|
||||||
|
};
|
||||||
|
let rs2 = await this.$insertapi("accountentry", obj2, undefined, false);
|
||||||
|
if (rs2 === "error") return;
|
||||||
|
let data = await this.$getdata("internalaccount", {
|
||||||
|
code__in: [this.record._source.code, this.record._target.code],
|
||||||
|
});
|
||||||
|
this.$updatepage(this.pagename, data);
|
||||||
|
this.$dialog(
|
||||||
|
`Điều chuyển vốn <b>${this.$numtoString(this.record.amount)}</b> từ <b>${this.record._source.code}</b> tới <b>${this.record._target.code}</b> thành công.`,
|
||||||
|
"Thành công",
|
||||||
|
"Success",
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
this.$emit("close");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-1"></div>
|
<div class="column is-1"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns" v-for="(invoice, index) in invoices">
|
<div
|
||||||
|
class="columns"
|
||||||
|
v-for="(invoice, index) in invoices"
|
||||||
|
>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<input
|
<input
|
||||||
class="input has-text-centered has-text-weight-bold has-text-left"
|
class="input has-text-centered has-text-weight-bold has-text-left"
|
||||||
@@ -32,7 +35,12 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<p v-if="invoice.errorLink" class="help is-danger">Link phải bắt đầu bằng https</p>
|
<p
|
||||||
|
v-if="invoice.errorLink"
|
||||||
|
class="help is-danger"
|
||||||
|
>
|
||||||
|
Link phải bắt đầu bằng https
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<input
|
<input
|
||||||
@@ -49,7 +57,12 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<p v-if="invoice.errorCode" class="help is-danger">Mã tra cứu không được bỏ trống</p>
|
<p
|
||||||
|
v-if="invoice.errorCode"
|
||||||
|
class="help is-danger"
|
||||||
|
>
|
||||||
|
Mã tra cứu không được bỏ trống
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<input
|
<input
|
||||||
@@ -66,7 +79,12 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<p v-if="invoice.errorAmount" class="help is-danger">Số tiền không được bỏ trống</p>
|
<p
|
||||||
|
v-if="invoice.errorAmount"
|
||||||
|
class="help is-danger"
|
||||||
|
>
|
||||||
|
Số tiền không được bỏ trống
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<select
|
<select
|
||||||
@@ -84,22 +102,44 @@
|
|||||||
<option value="principal">Tiền gốc</option>
|
<option value="principal">Tiền gốc</option>
|
||||||
<option value="interest">Tiền lãi</option>
|
<option value="interest">Tiền lãi</option>
|
||||||
</select>
|
</select>
|
||||||
<p v-if="invoice.errorType" class="help is-danger">Loại tiền không được bỏ trống</p>
|
<p
|
||||||
|
v-if="invoice.errorType"
|
||||||
|
class="help is-danger"
|
||||||
|
>
|
||||||
|
Loại tiền không được bỏ trống
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow is-1">
|
<div class="column is-narrow is-1">
|
||||||
<label class="label" v-if="i === 0"> </label>
|
<label
|
||||||
<div class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small" style="height: 40px">
|
class="label"
|
||||||
<button class="button is-dark" @click="handlerRemove(index)">
|
v-if="i === 0"
|
||||||
|
> </label
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small"
|
||||||
|
style="height: 40px"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-dark"
|
||||||
|
@click="handlerRemove(index)"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button is-dark" @click="add()">
|
<button
|
||||||
|
class="button is-dark"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<a class="button is-dark" :href="invoice.link" target="_blank">
|
<a
|
||||||
|
class="button is-dark"
|
||||||
|
:href="invoice.link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
@@ -108,8 +148,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 buttons is-right">
|
<div class="mt-5 buttons is-right">
|
||||||
<button class="button" @click="emit('close')">{{ isVietnamese ? "Hủy" : "Cancel" }}</button>
|
<button
|
||||||
<button class="button is-primary" @click="handlerUpdate">{{ isVietnamese ? "Lưu lại" : "Save" }}</button>
|
class="button"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
{{ isVietnamese ? "Hủy" : "Cancel" }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-primary"
|
||||||
|
@click="handlerUpdate"
|
||||||
|
>
|
||||||
|
{{ isVietnamese ? "Lưu lại" : "Save" }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +207,11 @@ if (resInvoice.length) {
|
|||||||
errorAmount: false,
|
errorAmount: false,
|
||||||
errorType: false,
|
errorType: false,
|
||||||
};
|
};
|
||||||
const formatData = resInvoice.map((invoice) => ({ ...invoice, amount: $formatNumber(invoice.amount), ...error }));
|
const formatData = resInvoice.map((invoice) => ({
|
||||||
|
...invoice,
|
||||||
|
amount: $formatNumber(invoice.amount),
|
||||||
|
...error,
|
||||||
|
}));
|
||||||
|
|
||||||
invoices.value = formatData;
|
invoices.value = formatData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,5 +17,4 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["remove"]);
|
const emit = defineEmits(["remove"]);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="docid">
|
<div :id="docid">
|
||||||
<div :id="docid1">
|
<div :id="docid1">
|
||||||
<Caption v-bind="{ title: isVietnamese? 'Thanh toán' : 'Payment', type: 'has-text-warning' }"></Caption>
|
<Caption
|
||||||
|
v-bind="{
|
||||||
|
title: isVietnamese ? 'Thanh toán' : 'Payment',
|
||||||
|
type: 'has-text-warning',
|
||||||
|
}"
|
||||||
|
></Caption>
|
||||||
<div class="columns is-multiline mx-0">
|
<div class="columns is-multiline mx-0">
|
||||||
<div class="column is-4 pb-1 px-0">
|
<div class="column is-4 pb-1 px-0">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("loan_code")[lang] }}</label>
|
<label class="label">{{ dataLang && findFieldName("loan_code")[lang] }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<span class="hyperlink" @click="$copyToClipboard(record.code)">{{ record?.code || "/" }}</span>
|
<span
|
||||||
|
class="hyperlink"
|
||||||
|
@click="$copyToClipboard(record.code)"
|
||||||
|
>{{ record?.code || "/" }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +63,7 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<span>{{ record?.commission ? $numtoString(record.commission) : "/" }}</span>
|
<span>{{ record?.commission ? $numtoString(record.commission) : "/" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-5 pb-1 px-0">
|
<div class="column is-5 pb-1 px-0">
|
||||||
@@ -76,14 +85,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="mt-2 border-bottom"></div> -->
|
<!-- <div class="mt-2 border-bottom"></div> -->
|
||||||
<div class="buttons mt-5" id="ignore">
|
<div
|
||||||
<button class="button is-primary has-text-white mt-2" @click="handleUpdate()">
|
class="buttons mt-5"
|
||||||
|
id="ignore"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white mt-2"
|
||||||
|
@click="handleUpdate()"
|
||||||
|
>
|
||||||
{{ dataLang && findFieldName("update")[lang] }}
|
{{ dataLang && findFieldName("update")[lang] }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="docid">
|
<div :id="docid">
|
||||||
<!-- Loading state -->
|
<!-- Loading state -->
|
||||||
<div v-if="isLoading" class="has-text-centered mt-5 mb-5" style="min-height: 500px">
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="has-text-centered mt-5 mb-5"
|
||||||
|
style="min-height: 500px"
|
||||||
|
>
|
||||||
<button class="button is-primary is-loading is-large"></button>
|
<button class="button is-primary is-loading is-large"></button>
|
||||||
<p class="mt-4 has-text-primary has-text-weight-semibold">
|
<p class="mt-4 has-text-primary has-text-weight-semibold">
|
||||||
{{ isVietnamese ? 'Đang tải hợp đồng...' : 'Loading contracts...' }}
|
{{ isVietnamese ? "Đang tải hợp đồng..." : "Loading contracts..." }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No contract state -->
|
<!-- No contract state -->
|
||||||
<div v-else-if="!hasContracts" class="has-text-centered mt-5 mb-5" style="min-height: 500px">
|
<div
|
||||||
|
v-else-if="!hasContracts"
|
||||||
|
class="has-text-centered mt-5 mb-5"
|
||||||
|
style="min-height: 500px"
|
||||||
|
>
|
||||||
<article class="message is-primary">
|
<article class="message is-primary">
|
||||||
<div class="message-body" style="font-size: 17px; text-align: left; color: black">
|
<div
|
||||||
|
class="message-body"
|
||||||
|
style="font-size: 17px; text-align: left; color: black"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
isVietnamese
|
isVietnamese
|
||||||
? "Chưa có hợp đồng. Vui lòng tạo giao dịch và hợp đồng trước."
|
? "Chưa có hợp đồng. Vui lòng tạo giao dịch và hợp đồng trước."
|
||||||
@@ -24,10 +35,22 @@
|
|||||||
<!-- Contracts list -->
|
<!-- Contracts list -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Tabs khi có nhiều hợp đồng -->
|
<!-- Tabs khi có nhiều hợp đồng -->
|
||||||
<div class="tabs border-bottom" id="ignore" v-if="contractsList.length > 1">
|
<div
|
||||||
|
class="tabs border-bottom"
|
||||||
|
id="ignore"
|
||||||
|
v-if="contractsList.length > 1"
|
||||||
|
>
|
||||||
<ul class="tabs-list">
|
<ul class="tabs-list">
|
||||||
<li class="tabs-item" style="border: none" v-for="(contract, index) in contractsList" :key="index"
|
<li
|
||||||
:class="{ 'bg-primary has-text-white': activeContractIndex === index }" @click="switchContract(index)">
|
class="tabs-item"
|
||||||
|
style="border: none"
|
||||||
|
v-for="(contract, index) in contractsList"
|
||||||
|
:key="index"
|
||||||
|
:class="{
|
||||||
|
'bg-primary has-text-white': activeContractIndex === index,
|
||||||
|
}"
|
||||||
|
@click="switchContract(index)"
|
||||||
|
>
|
||||||
<a class="tabs-link">
|
<a class="tabs-link">
|
||||||
<span>{{ contract.document[0]?.name || contract.document[0]?.en || `Contract ${index + 1}` }}</span>
|
<span>{{ contract.document[0]?.name || contract.document[0]?.en || `Contract ${index + 1}` }}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -38,23 +61,42 @@
|
|||||||
<!-- Contract content -->
|
<!-- Contract content -->
|
||||||
<div v-if="currentContract && pdfFileUrl && hasValidDocument">
|
<div v-if="currentContract && pdfFileUrl && hasValidDocument">
|
||||||
<div class="contract-content mt-2">
|
<div class="contract-content mt-2">
|
||||||
<iframe :src="`https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfFileUrl}`" width="100%"
|
<iframe
|
||||||
height="90vh" scrolling="no" style="border: none; height: 75vh; top: 0; left: 0; right: 0; bottom: 0">
|
:src="`https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfFileUrl}`"
|
||||||
|
width="100%"
|
||||||
|
height="90vh"
|
||||||
|
scrolling="no"
|
||||||
|
style="border: none; height: 75vh; top: 0; left: 0; right: 0; bottom: 0"
|
||||||
|
>
|
||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Download buttons -->
|
<!-- Download buttons -->
|
||||||
<div class="mt-4" id="ignore">
|
<div
|
||||||
<button v-if="hasValidDocument" class="button is-primary has-text-white mr-4" @click="downloadDocx">
|
class="mt-4"
|
||||||
|
id="ignore"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="hasValidDocument"
|
||||||
|
class="button is-primary has-text-white mr-4"
|
||||||
|
@click="downloadDocx"
|
||||||
|
>
|
||||||
{{ isVietnamese ? "Tải file docx" : "Download contract as docx" }}
|
{{ isVietnamese ? "Tải file docx" : "Download contract as docx" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="hasValidDocument" class="button is-primary has-text-white mr-4" @click="downloadPdf">
|
<button
|
||||||
|
v-if="hasValidDocument"
|
||||||
|
class="button is-primary has-text-white mr-4"
|
||||||
|
@click="downloadPdf"
|
||||||
|
>
|
||||||
{{ isVietnamese ? "Tải file pdf" : "Download contract as pdf" }}
|
{{ isVietnamese ? "Tải file pdf" : "Download contract as pdf" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p v-if="contractError" class="has-text-danger mt-2">
|
<p
|
||||||
|
v-if="contractError"
|
||||||
|
class="has-text-danger mt-2"
|
||||||
|
>
|
||||||
{{ contractError }}
|
{{ contractError }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,16 +115,16 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
contractId: {
|
contractId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
directDocument: {
|
directDocument: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: ["contractCreated", "update", "close", "dataevent"],
|
emits: ["contractCreated", "update", "close", "dataevent"],
|
||||||
data() {
|
data() {
|
||||||
@@ -107,10 +149,12 @@ export default {
|
|||||||
},
|
},
|
||||||
hasValidDocument() {
|
hasValidDocument() {
|
||||||
if (!this.currentContract) return false;
|
if (!this.currentContract) return false;
|
||||||
return this.currentContract.document &&
|
return (
|
||||||
|
this.currentContract.document &&
|
||||||
this.currentContract.document.length > 0 &&
|
this.currentContract.document.length > 0 &&
|
||||||
this.currentContract.document[0]?.pdf;
|
this.currentContract.document[0]?.pdf
|
||||||
}
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
@@ -118,11 +162,9 @@ export default {
|
|||||||
this.contractError = null;
|
this.contractError = null;
|
||||||
|
|
||||||
if (this.directDocument) {
|
if (this.directDocument) {
|
||||||
this.contractsList = [
|
this.contractsList = [{ document: [this.directDocument] }];
|
||||||
{ document: [this.directDocument] }
|
|
||||||
];
|
|
||||||
this.updatePdfUrl(0);
|
this.updatePdfUrl(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let contracts = [];
|
let contracts = [];
|
||||||
@@ -130,31 +172,22 @@ export default {
|
|||||||
|
|
||||||
if (this.contractId) {
|
if (this.contractId) {
|
||||||
fetchParams = { id: this.contractId };
|
fetchParams = { id: this.contractId };
|
||||||
}
|
} else if (this.row?.id) {
|
||||||
else if (this.row?.id) {
|
|
||||||
fetchParams = { transaction: this.row.id };
|
fetchParams = { transaction: this.row.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchParams) {
|
if (!fetchParams) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
this.isVietnamese
|
this.isVietnamese
|
||||||
? 'Không có ID hợp đồng hoặc transaction để tải.'
|
? "Không có ID hợp đồng hoặc transaction để tải."
|
||||||
: 'No contract ID or transaction provided to load.'
|
: "No contract ID or transaction provided to load.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
contracts = await this.$getdata(
|
contracts = await this.$getdata("contract", fetchParams, undefined);
|
||||||
'contract',
|
|
||||||
fetchParams,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!contracts || contracts.length === 0) {
|
if (!contracts || contracts.length === 0) {
|
||||||
throw new Error(
|
throw new Error(this.isVietnamese ? "Không tìm thấy hợp đồng." : "Contract not found.");
|
||||||
this.isVietnamese
|
|
||||||
? 'Không tìm thấy hợp đồng.'
|
|
||||||
: 'Contract not found.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contractsList = contracts;
|
this.contractsList = contracts;
|
||||||
@@ -164,12 +197,9 @@ export default {
|
|||||||
this.updatePdfUrl(this.activeContractIndex);
|
this.updatePdfUrl(this.activeContractIndex);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading contracts:', error);
|
console.error("Error loading contracts:", error);
|
||||||
this.contractError = error.message || (
|
this.contractError =
|
||||||
this.isVietnamese
|
error.message || (this.isVietnamese ? "Lỗi khi tải danh sách hợp đồng." : "Error loading contracts list.");
|
||||||
? 'Lỗi khi tải danh sách hợp đồng.'
|
|
||||||
: 'Error loading contracts list.'
|
|
||||||
);
|
|
||||||
this.contractsList = [];
|
this.contractsList = [];
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -192,10 +222,7 @@ export default {
|
|||||||
|
|
||||||
downloadDocx() {
|
downloadDocx() {
|
||||||
if (!this.hasValidDocument) {
|
if (!this.hasValidDocument) {
|
||||||
this.$snackbar(
|
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
|
||||||
this.isVietnamese ? "Không có file để tải" : "No file to download",
|
|
||||||
{ type: 'is-warning' }
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,10 +233,7 @@ export default {
|
|||||||
|
|
||||||
downloadPdf() {
|
downloadPdf() {
|
||||||
if (!this.hasValidDocument) {
|
if (!this.hasValidDocument) {
|
||||||
this.$snackbar(
|
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
|
||||||
this.isVietnamese ? "Không có file để tải" : "No file to download",
|
|
||||||
{ type: 'is-warning' }
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,4 +283,4 @@ export default {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
<div v-if="isLoading" class="loading-overlay">
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="loading-overlay"
|
||||||
|
>
|
||||||
<div class="loader"></div>
|
<div class="loader"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- View Mode Toggle -->
|
<!-- View Mode Toggle -->
|
||||||
<div class="mb-5 pb-3" style="border-bottom: 2px solid #e8e8e8;">
|
<div
|
||||||
<div class="buttons has-addons ">
|
class="mb-5 pb-3"
|
||||||
<button @click="viewMode = 'list'" :class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']">
|
style="border-bottom: 2px solid #e8e8e8"
|
||||||
|
>
|
||||||
|
<div class="buttons has-addons">
|
||||||
|
<button
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
:class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']"
|
||||||
|
>
|
||||||
Danh sách
|
Danh sách
|
||||||
</button>
|
</button>
|
||||||
<button @click="viewMode = 'gallery'" :class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']">
|
<button
|
||||||
|
@click="viewMode = 'gallery'"
|
||||||
|
:class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']"
|
||||||
|
>
|
||||||
Thư viện
|
Thư viện
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,8 +31,11 @@
|
|||||||
|
|
||||||
<!-- Phase Document Types List -->
|
<!-- Phase Document Types List -->
|
||||||
<div v-if="phasedoctypes && phasedoctypes.length > 0">
|
<div v-if="phasedoctypes && phasedoctypes.length > 0">
|
||||||
<div v-for="doctype in phasedoctypes" :key="doctype.id" class="mb-6">
|
<div
|
||||||
|
v-for="doctype in phasedoctypes"
|
||||||
|
:key="doctype.id"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
<!-- Document Type Header with Upload Button -->
|
<!-- Document Type Header with Upload Button -->
|
||||||
<div class="level is-mobile mb-4">
|
<div class="level is-mobile mb-4">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
@@ -34,9 +49,9 @@
|
|||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<FileUpload
|
<FileUpload
|
||||||
v-if="$getEditRights()"
|
v-if="$getEditRights()"
|
||||||
:type="['file', 'image', 'pdf']"
|
:type="['file', 'image', 'pdf']"
|
||||||
@files="(files) => handleUpload(files, doctype.doctype)"
|
@files="(files) => handleUpload(files, doctype.doctype)"
|
||||||
position="right"
|
position="right"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,112 +60,195 @@
|
|||||||
<!-- List View -->
|
<!-- List View -->
|
||||||
<div v-if="viewMode === 'list'">
|
<div v-if="viewMode === 'list'">
|
||||||
<div v-if="getFilesByDocType(doctype.doctype).length > 0">
|
<div v-if="getFilesByDocType(doctype.doctype).length > 0">
|
||||||
<div v-for="file in getFilesByDocType(doctype.doctype)" :key="file.id"
|
<div
|
||||||
|
v-for="file in getFilesByDocType(doctype.doctype)"
|
||||||
|
:key="file.id"
|
||||||
class="is-flex is-justify-content-space-between is-align-items-center py-3 px-4 has-background-warning has-text-white"
|
class="is-flex is-justify-content-space-between is-align-items-center py-3 px-4 has-background-warning has-text-white"
|
||||||
style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer;"
|
style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer"
|
||||||
@mouseenter="$event.currentTarget.style.opacity = '1'"
|
@mouseenter="$event.currentTarget.style.opacity = '1'"
|
||||||
@mouseleave="$event.currentTarget.style.opacity = '0.95'">
|
@mouseleave="$event.currentTarget.style.opacity = '0.95'"
|
||||||
|
>
|
||||||
<div style="flex: 1; min-width: 0;">
|
<div style="flex: 1; min-width: 0">
|
||||||
<p class="is-size-7 has-text-weight-semibold has-text-white mb-1" style="word-break: break-word;">
|
<p
|
||||||
|
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
||||||
|
style="word-break: break-word"
|
||||||
|
>
|
||||||
{{ file.name || file.file__name }}
|
{{ file.name || file.file__name }}
|
||||||
</p>
|
</p>
|
||||||
<p class="is-size-7 has-text-white-bis">
|
<p class="is-size-7 has-text-white-bis">
|
||||||
{{ $formatFileSize(file.file__size) }} • {{ $dayjs(file.create_time).format("DD/MM/YYYY HH:mm") }}
|
{{ $formatFileSize(file.file__size) }} •
|
||||||
|
{{ $dayjs(file.create_time).format("DD/MM/YYYY HH:mm") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons are-small ml-3">
|
<div class="buttons are-small ml-3">
|
||||||
<button @click="viewFile(file)" class="button has-background-white has-text-primary ">
|
<button
|
||||||
|
@click="viewFile(file)"
|
||||||
|
class="button has-background-white has-text-primary"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{
|
<SvgIcon
|
||||||
name: 'view.svg',
|
v-bind="{
|
||||||
type: 'success',
|
name: 'view.svg',
|
||||||
size: 18,
|
type: 'success',
|
||||||
}"></SvgIcon>
|
size: 18,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="downloadFile(file)" class="button has-background-white has-text-primary ">
|
<button
|
||||||
|
@click="downloadFile(file)"
|
||||||
|
class="button has-background-white has-text-primary"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{
|
<SvgIcon
|
||||||
name: 'download.svg',
|
v-bind="{
|
||||||
type: 'success',
|
name: 'download.svg',
|
||||||
size: 18,
|
type: 'success',
|
||||||
}"></SvgIcon>
|
size: 18,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="deleteFile(file.id)" class="button has-background-white has-text-danger ">
|
<button
|
||||||
|
@click="deleteFile(file.id)"
|
||||||
|
class="button has-background-white has-text-danger"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{
|
<SvgIcon
|
||||||
name: 'bin.svg',
|
v-bind="{
|
||||||
type: 'danger',
|
name: 'bin.svg',
|
||||||
size: 18,
|
type: 'danger',
|
||||||
}"></SvgIcon>
|
size: 18,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="has-text-grey-light is-size-7 has-text-centered py-5">
|
<div
|
||||||
|
v-else
|
||||||
|
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
||||||
|
>
|
||||||
Chưa có file nào
|
Chưa có file nào
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gallery View -->
|
<!-- Gallery View -->
|
||||||
<div v-if="viewMode === 'gallery'">
|
<div v-if="viewMode === 'gallery'">
|
||||||
<div v-if="getFilesByDocType(doctype.doctype).length > 0" class="columns is-multiline is-variable is-2">
|
<div
|
||||||
<div v-for="file in getFilesByDocType(doctype.doctype)" :key="file.id"
|
v-if="getFilesByDocType(doctype.doctype).length > 0"
|
||||||
class="column is-half-tablet is-one-third-desktop">
|
class="columns is-multiline is-variable is-2"
|
||||||
<div class="has-background-warning has-text-white"
|
>
|
||||||
style="border-radius: 6px; overflow: hidden; height: 100%; display: flex; flex-direction: column; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);"
|
<div
|
||||||
@mouseenter="$event.currentTarget.style.transform = 'translateY(-4px)'; $event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)'"
|
v-for="file in getFilesByDocType(doctype.doctype)"
|
||||||
@mouseleave="$event.currentTarget.style.transform = 'translateY(0)'; $event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)'">
|
:key="file.id"
|
||||||
|
class="column is-half-tablet is-one-third-desktop"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="has-background-warning has-text-white"
|
||||||
|
style="
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);
|
||||||
|
"
|
||||||
|
@mouseenter="
|
||||||
|
$event.currentTarget.style.transform = 'translateY(-4px)';
|
||||||
|
$event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)';
|
||||||
|
"
|
||||||
|
@mouseleave="
|
||||||
|
$event.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
$event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)';
|
||||||
|
"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style="flex: 1; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(255, 255, 255, 0.1); min-height: 140px;">
|
style="
|
||||||
<div v-if="isImage(file.file__name)"
|
flex: 1;
|
||||||
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
|
display: flex;
|
||||||
<img :src="`${$getpath()}static/files/${file.file__file}`" :alt="file.file__name"
|
align-items: center;
|
||||||
style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
min-height: 140px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isImage(file.file__name)"
|
||||||
|
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`${$getpath()}static/files/${file.file__file}`"
|
||||||
|
:alt="file.file__name"
|
||||||
|
style="max-width: 100%; max-height: 100%; object-fit: contain"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="has-text-white-ter" style="font-size: 48px; line-height: 1;">
|
<div
|
||||||
|
v-else
|
||||||
|
class="has-text-white-ter"
|
||||||
|
style="font-size: 48px; line-height: 1"
|
||||||
|
>
|
||||||
FILE
|
FILE
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="padding: 12px 16px;">
|
<div style="padding: 12px 16px">
|
||||||
<p class="is-size-7 has-text-weight-semibold has-text-white mb-1" :title="file.file__name"
|
<p
|
||||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
||||||
|
:title="file.file__name"
|
||||||
|
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
|
||||||
|
>
|
||||||
{{ file.file__name }}
|
{{ file.file__name }}
|
||||||
</p>
|
</p>
|
||||||
<p class="is-size-7 has-text-white-bis mb-3">{{ $formatFileSize(file.file__size) }}</p>
|
<p class="is-size-7 has-text-white-bis mb-3">
|
||||||
|
{{ $formatFileSize(file.file__size) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="buttons are-small is-centered">
|
<div class="buttons are-small is-centered">
|
||||||
<button @click="viewFile(file)" class="button has-background-white has-text-primary ">
|
<button
|
||||||
|
@click="viewFile(file)"
|
||||||
|
class="button has-background-white has-text-primary"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{
|
<SvgIcon
|
||||||
name: 'view.svg',
|
v-bind="{
|
||||||
type: 'success',
|
name: 'view.svg',
|
||||||
size: 18,
|
type: 'success',
|
||||||
}"></SvgIcon>
|
size: 18,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="downloadFile(file)" class="button has-background-white has-text-primary ">
|
<button
|
||||||
|
@click="downloadFile(file)"
|
||||||
|
class="button has-background-white has-text-primary"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{
|
<SvgIcon
|
||||||
name: 'download.svg',
|
v-bind="{
|
||||||
type: 'success',
|
name: 'download.svg',
|
||||||
size: 18,
|
type: 'success',
|
||||||
}"></SvgIcon>
|
size: 18,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="deleteFile(file.id)" class="button has-background-white has-text-danger ">
|
<button
|
||||||
|
@click="deleteFile(file.id)"
|
||||||
|
class="button has-background-white has-text-danger"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{
|
<SvgIcon
|
||||||
name: 'bin.svg',
|
v-bind="{
|
||||||
type: 'danger',
|
name: 'bin.svg',
|
||||||
size: 18,
|
type: 'danger',
|
||||||
}"></SvgIcon>
|
size: 18,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,7 +256,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="has-text-grey-light is-size-7 has-text-centered py-5">
|
<div
|
||||||
|
v-else
|
||||||
|
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
||||||
|
>
|
||||||
Chưa có file nào
|
Chưa có file nào
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,11 +267,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- If no phase doctypes -->
|
<!-- If no phase doctypes -->
|
||||||
<div v-else-if="!isLoading" class="has-text-centered py-6">
|
<div
|
||||||
|
v-else-if="!isLoading"
|
||||||
|
class="has-text-centered py-6"
|
||||||
|
>
|
||||||
<p class="has-text-grey-light is-size-7">Chưa có loại tài liệu được định nghĩa cho giai đoạn này.</p>
|
<p class="has-text-grey-light is-size-7">Chưa có loại tài liệu được định nghĩa cho giai đoạn này.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal @close="showmodal = undefined" @modalevent="handleModalEvent" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
@modalevent="handleModalEvent"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -184,7 +293,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
row: {
|
row: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -194,7 +303,7 @@ export default {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
phasedoctypes: [],
|
phasedoctypes: [],
|
||||||
viewMode: 'list',
|
viewMode: "list",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -221,9 +330,14 @@ export default {
|
|||||||
if (!this.transaction?.phase) return;
|
if (!this.transaction?.phase) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const phasedoctypesData = await $getdata('phasedoctype', {
|
const phasedoctypesData = await $getdata(
|
||||||
phase: this.transaction.phase,
|
"phasedoctype",
|
||||||
}, undefined, false);
|
{
|
||||||
|
phase: this.transaction.phase,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
if (phasedoctypesData) {
|
if (phasedoctypesData) {
|
||||||
this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData];
|
this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData];
|
||||||
@@ -240,15 +354,27 @@ export default {
|
|||||||
if (!this.row.id) return;
|
if (!this.row.id) return;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
try {
|
try {
|
||||||
const detail = await $getdata('reservation', {
|
const detail = await $getdata(
|
||||||
id: this.transaction.txncurrent__detail
|
"reservation",
|
||||||
}, undefined, true)
|
{
|
||||||
const filesArray = await $getdata('transactionfile', {
|
id: this.transaction.txncurrent__detail,
|
||||||
txn_detail: detail.id,
|
},
|
||||||
}, undefined, false);
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const filesArray = await $getdata(
|
||||||
|
"transactionfile",
|
||||||
|
{
|
||||||
|
txn_detail: detail.id,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
if (filesArray) {
|
if (filesArray) {
|
||||||
this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort((a, b) => new Date(b.create_time) - new Date(a.create_time));
|
this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort(
|
||||||
|
(a, b) => new Date(b.create_time) - new Date(a.create_time),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.files = [];
|
this.files = [];
|
||||||
}
|
}
|
||||||
@@ -260,18 +386,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getFilesByDocType(docTypeId) {
|
getFilesByDocType(docTypeId) {
|
||||||
return this.files.filter(file => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null));
|
return this.files.filter(
|
||||||
|
(file) => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getFileExtension(fileName) {
|
getFileExtension(fileName) {
|
||||||
return fileName ? fileName.split('.').pop().toLowerCase() : 'file';
|
return fileName ? fileName.split(".").pop().toLowerCase() : "file";
|
||||||
},
|
},
|
||||||
isImage(fileName) {
|
isImage(fileName) {
|
||||||
const ext = this.getFileExtension(fileName);
|
const ext = this.getFileExtension(fileName);
|
||||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext);
|
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
|
||||||
},
|
},
|
||||||
isViewableDocument(fileName) {
|
isViewableDocument(fileName) {
|
||||||
const ext = this.getFileExtension(fileName);
|
const ext = this.getFileExtension(fileName);
|
||||||
return ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext);
|
return ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(ext);
|
||||||
},
|
},
|
||||||
async handleUpload(uploadedFiles, docTypeId) {
|
async handleUpload(uploadedFiles, docTypeId) {
|
||||||
if (!uploadedFiles || uploadedFiles.length === 0) return;
|
if (!uploadedFiles || uploadedFiles.length === 0) return;
|
||||||
@@ -282,15 +410,20 @@ export default {
|
|||||||
try {
|
try {
|
||||||
for (const fileRecord of uploadedFiles) {
|
for (const fileRecord of uploadedFiles) {
|
||||||
if (docTypeId) {
|
if (docTypeId) {
|
||||||
await $patchapi('file', {
|
await $patchapi("file", {
|
||||||
id: fileRecord.id,
|
id: fileRecord.id,
|
||||||
doc_type: docTypeId
|
doc_type: docTypeId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const detail = await $getdata('reservation', {
|
const detail = await $getdata(
|
||||||
id: this.transaction.txncurrent__detail,
|
"reservation",
|
||||||
}, undefined, true)
|
{
|
||||||
|
id: this.transaction.txncurrent__detail,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
txn_detail: detail.id,
|
txn_detail: detail.id,
|
||||||
@@ -305,7 +438,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.fetchFiles();
|
await this.fetchFiles();
|
||||||
this.$emit('upload-completed');
|
this.$emit("upload-completed");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Lỗi khi lưu file:", error);
|
console.error("Lỗi khi lưu file:", error);
|
||||||
alert("Đã xảy ra lỗi khi tải file lên. Vui lòng thử lại.");
|
alert("Đã xảy ra lỗi khi tải file lên. Vui lòng thử lại.");
|
||||||
@@ -318,21 +451,21 @@ export default {
|
|||||||
const filePath = file.file__file || file.file;
|
const filePath = file.file__file || file.file;
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement("a");
|
||||||
link.href = `${$getpath()}static/files/${filePath}`;
|
link.href = `${$getpath()}static/files/${filePath}`;
|
||||||
link.download = file.file__name || 'download';
|
link.download = file.file__name || "download";
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
},
|
},
|
||||||
deleteFile(fileId) {
|
deleteFile(fileId) {
|
||||||
this.showmodal = {
|
this.showmodal = {
|
||||||
component: 'dialog/Confirm',
|
component: "dialog/Confirm",
|
||||||
title: 'Xác nhận xóa',
|
title: "Xác nhận xóa",
|
||||||
height: '10vh',
|
height: "10vh",
|
||||||
width: '40%',
|
width: "40%",
|
||||||
vbind: {
|
vbind: {
|
||||||
content: 'Bạn có chắc chắn muốn xóa file này không?'
|
content: "Bạn có chắc chắn muốn xóa file này không?",
|
||||||
},
|
},
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
@@ -350,17 +483,17 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleModalEvent(event) {
|
handleModalEvent(event) {
|
||||||
if (event.name === 'confirm' && typeof this.showmodal?.onConfirm === 'function') {
|
if (event.name === "confirm" && typeof this.showmodal?.onConfirm === "function") {
|
||||||
this.showmodal.onConfirm();
|
this.showmodal.onConfirm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
viewFile(file) {
|
viewFile(file) {
|
||||||
const { $getpath } = useNuxtApp();
|
const { $getpath } = useNuxtApp();
|
||||||
const fileName = file.file__name || '';
|
const fileName = file.file__name || "";
|
||||||
const filePath = file.file__file || file.file;
|
const filePath = file.file__file || file.file;
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
|
|
||||||
@@ -371,23 +504,23 @@ export default {
|
|||||||
if (isImageFile) {
|
if (isImageFile) {
|
||||||
this.showmodal = {
|
this.showmodal = {
|
||||||
title: fileName,
|
title: fileName,
|
||||||
component: 'media/ChipImage',
|
component: "media/ChipImage",
|
||||||
vbind: {
|
vbind: {
|
||||||
extend: false,
|
extend: false,
|
||||||
file: file,
|
file: file,
|
||||||
image: fileUrl,
|
image: fileUrl,
|
||||||
show: ['download', 'delete']
|
show: ["download", "delete"],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
} else if (isViewable) {
|
} else if (isViewable) {
|
||||||
// Mở Google Viewer trực tiếp trong tab mới
|
// Mở Google Viewer trực tiếp trong tab mới
|
||||||
const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`;
|
const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`;
|
||||||
window.open(viewerUrl, '_blank');
|
window.open(viewerUrl, "_blank");
|
||||||
} else {
|
} else {
|
||||||
this.downloadFile(file);
|
this.downloadFile(file);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -420,4 +553,4 @@ export default {
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- Viewer: display when click tem from another dealer -->
|
<!-- Viewer: display when click tem from another dealer -->
|
||||||
<template>
|
<template>
|
||||||
<p>Rất tiếc, bạn hiện chưa có quyền xem thông tin sản phẩm này.</p>
|
<p>Rất tiếc, bạn hiện chưa có 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
@@ -1,8 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="productData" class="grid px-3">
|
<div
|
||||||
|
v-if="productData"
|
||||||
|
class="grid px-3"
|
||||||
|
>
|
||||||
<div class="cell is-col-span-12">
|
<div class="cell is-col-span-12">
|
||||||
<div id="schedule-content">
|
<div id="schedule-content">
|
||||||
<div v-if="selectedPolicy" id="print-area" :class="{ 'is-loading': isLoading }">
|
<div
|
||||||
|
v-if="selectedPolicy"
|
||||||
|
id="print-area"
|
||||||
|
:class="{ 'is-loading': isLoading }"
|
||||||
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
||||||
<h3 class="title is-4 has-text-primary mb-1">
|
<h3 class="title is-4 has-text-primary mb-1">
|
||||||
@@ -12,101 +19,186 @@
|
|||||||
<span class="button is-white">
|
<span class="button is-white">
|
||||||
<span class="has-text-weight-semibold">Đơn vị: VNĐ</span>
|
<span class="has-text-weight-semibold">Đơn vị: VNĐ</span>
|
||||||
</span>
|
</span>
|
||||||
<button class="button is-light" @click="$emit('print')" id="ignore-print">
|
<button
|
||||||
|
class="button is-light"
|
||||||
|
@click="$emit('print')"
|
||||||
|
id="ignore-print"
|
||||||
|
>
|
||||||
<span class="is-size-6">In</span>
|
<span class="is-size-6">In</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-4" style="background-color: var(--bulma-background)"></hr>
|
<hr
|
||||||
|
class="my-4"
|
||||||
|
style="background-color: var(--bulma-background)"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Summary Information -->
|
<!-- Summary Information -->
|
||||||
<div class="fixed-grid has-4-cols-mobile has-7-cols-desktop">
|
<div class="fixed-grid has-4-cols-mobile has-7-cols-desktop">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||||
<p class="is-size-6 has-text-weight-bold mb-1">Sản phẩm</p>
|
<p class="is-size-6 has-text-weight-bold mb-1">Sản phẩm</p>
|
||||||
<p class="has-text-primary has-text-weight-medium">{{ productData.trade_code || productData.code }} <a
|
<p class="has-text-primary has-text-weight-medium">
|
||||||
class="ml-4" id="ignore" @click="$copyToClipboard(productData.trade_code)">
|
{{ productData.trade_code || productData.code }}
|
||||||
<SvgIcon name="copy.svg" type="primary" :size="18" />
|
<a
|
||||||
|
class="ml-4"
|
||||||
|
id="ignore"
|
||||||
|
@click="$copyToClipboard(productData.trade_code)"
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
name="copy.svg"
|
||||||
|
type="primary"
|
||||||
|
:size="18"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá niêm yết</p>
|
<p class="is-size-6 has-text-weight-bold mb-1">Giá niêm yết</p>
|
||||||
<p class="has-text-primary">{{ $numtoString(calculatorData.originPrice) }}</p>
|
<p class="has-text-primary">
|
||||||
|
{{ $numtoString(calculatorData.originPrice) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||||
<p class="is-size-6 has-text-weight-bold mb-1">Tổng chiết khấu</p>
|
<p class="is-size-6 has-text-weight-bold mb-1">Tổng chiết khấu</p>
|
||||||
<p class="has-text-danger has-text-weight-bold">{{ $numtoString(calculatorData.totalDiscount) }}</p>
|
<p class="has-text-danger has-text-weight-bold">
|
||||||
|
{{ $numtoString(calculatorData.totalDiscount) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá sau chiết khấu</p>
|
<p class="is-size-6 has-text-weight-bold mb-1">Giá sau chiết khấu</p>
|
||||||
<p class="has-text-black has-text-weight-bold">{{ $numtoString(calculatorData.salePrice) }}</p>
|
<p class="has-text-black has-text-weight-bold">
|
||||||
|
{{ $numtoString(calculatorData.salePrice) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedPolicy.contract_allocation_percentage < 100"
|
<div
|
||||||
class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
v-if="selectedPolicy.contract_allocation_percentage < 100"
|
||||||
|
class="cell is-col-span-6-mobile is-col-span-1-desktop"
|
||||||
|
>
|
||||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá trị bảo đảm</p>
|
<p class="is-size-6 has-text-weight-bold mb-1">Giá trị bảo đảm</p>
|
||||||
<p class="has-text-primary">{{ $numtoString(calculatorData.allocatedPrice) }}</p>
|
<p class="has-text-primary">
|
||||||
|
{{ $numtoString(calculatorData.allocatedPrice) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="totalPaid === 0" class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
<div
|
||||||
|
v-if="totalPaid === 0"
|
||||||
|
class="cell is-col-span-6-mobile is-col-span-1-desktop"
|
||||||
|
>
|
||||||
<p class="is-size-6 has-text-weight-bold mb-1">Đặt cọc</p>
|
<p class="is-size-6 has-text-weight-bold mb-1">Đặt cọc</p>
|
||||||
<p class="has-text-primary">{{ $numtoString(selectedPolicy.deposit) }}</p>
|
<p class="has-text-primary">
|
||||||
|
{{ $numtoString(selectedPolicy.deposit) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||||
<p class="is-size-6 has-text-weight-bold mb-1">Khách hàng</p>
|
<p class="is-size-6 has-text-weight-bold mb-1">Khách hàng</p>
|
||||||
<p v-if="selectedCustomer" class="has-text-primary has-text-weight-medium">
|
<p
|
||||||
|
v-if="selectedCustomer"
|
||||||
|
class="has-text-primary has-text-weight-medium"
|
||||||
|
>
|
||||||
{{ selectedCustomer.code }} - {{ selectedCustomer.fullname }}
|
{{ selectedCustomer.code }} - {{ selectedCustomer.fullname }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="has-text-grey is-italic is-size-6">Chưa chọn</p>
|
<p
|
||||||
|
v-else
|
||||||
|
class="has-text-grey is-italic is-size-6"
|
||||||
|
>
|
||||||
|
Chưa chọn
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4" style="background-color: var(--bulma-background)"></hr>
|
<hr
|
||||||
|
class="my-4"
|
||||||
|
style="background-color: var(--bulma-background)"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Detailed Discounts -->
|
<!-- Detailed Discounts -->
|
||||||
<div v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0" class="mt-4 mb-4">
|
<div
|
||||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
|
v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0"
|
||||||
CHI TIẾT CHIẾT KHẤU:
|
class="mt-4 mb-4"
|
||||||
</p>
|
>
|
||||||
|
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">CHI TIẾT CHIẾT KHẤU:</p>
|
||||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;"
|
<th
|
||||||
colspan="2">Diễn giải chiết khấu</th>
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
style="border: none"
|
||||||
style="border: none;" width="15%">Giá trị</th>
|
colspan="2"
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
>
|
||||||
style="border: none;" width="20%">Thành tiền</th>
|
Diễn giải chiết khấu
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
</th>
|
||||||
style="border: none;" width="20%">Còn lại</th>
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
width="15%"
|
||||||
|
>
|
||||||
|
Giá trị
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
width="20%"
|
||||||
|
>
|
||||||
|
Thành tiền
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
width="20%"
|
||||||
|
>
|
||||||
|
Còn lại
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr style="border-bottom: 1px solid #f5f5f5;" class="has-text-grey-light">
|
<tr
|
||||||
<td colspan="4" class="has-text-right pt-1 pb-1">Giá gốc</td>
|
style="border-bottom: 1px solid #f5f5f5"
|
||||||
<td class="has-text-right has-text-weight-bold pt-1 pb-1">{{
|
class="has-text-grey-light"
|
||||||
$numtoString(calculatorData.originPrice) }}</td>
|
>
|
||||||
|
<td
|
||||||
|
colspan="4"
|
||||||
|
class="has-text-right pt-1 pb-1"
|
||||||
|
>
|
||||||
|
Giá gốc
|
||||||
|
</td>
|
||||||
|
<td class="has-text-right has-text-weight-bold pt-1 pb-1">
|
||||||
|
{{ $numtoString(calculatorData.originPrice) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="(item, idx) in calculatorData.detailedDiscounts" :key="`discount-${idx}`"
|
<tr
|
||||||
style="border-bottom: 1px solid #f5f5f5;">
|
v-for="(item, idx) in calculatorData.detailedDiscounts"
|
||||||
<td width="5%" class="has-text-centered">{{ idx + 1 }}</td>
|
:key="`discount-${idx}`"
|
||||||
|
style="border-bottom: 1px solid #f5f5f5"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
width="5%"
|
||||||
|
class="has-text-centered"
|
||||||
|
>
|
||||||
|
{{ idx + 1 }}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="has-text-weight-semibold">{{ item.name }}</span>
|
<span class="has-text-weight-semibold">{{ item.name }}</span>
|
||||||
<span class="tag is-primary has-text-white is-rounded border ml-1">{{ item.code }}</span>
|
<span class="tag is-primary has-text-white is-rounded border ml-1">{{ item.code }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="has-text-right">{{ item.customType === 1 ? item.customValue + '%' :
|
<td class="has-text-right">
|
||||||
$numtoString(item.customValue) }}</td>
|
{{ item.customType === 1 ? item.customValue + "%" : $numtoString(item.customValue) }}
|
||||||
|
</td>
|
||||||
<td class="has-text-right has-text-danger">-{{ $numtoString(item.amount) }}</td>
|
<td class="has-text-right has-text-danger">-{{ $numtoString(item.amount) }}</td>
|
||||||
<td class="has-text-right has-text-primary">{{ $numtoString(item.remaining) }}</td>
|
<td class="has-text-right has-text-primary">
|
||||||
|
{{ $numtoString(item.remaining) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Early Payment Details -->
|
<!-- Early Payment Details -->
|
||||||
<div v-if="isEarlyPaymentActive" class="mt-4 mb-4">
|
<div
|
||||||
|
v-if="isEarlyPaymentActive"
|
||||||
|
class="mt-4 mb-4"
|
||||||
|
>
|
||||||
<!-- Original Schedule -->
|
<!-- Original Schedule -->
|
||||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
|
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
|
||||||
LỊCH THANH TOÁN GỐC (THEO CHÍNH SÁCH)
|
LỊCH THANH TOÁN GỐC (THEO CHÍNH SÁCH)
|
||||||
@@ -115,26 +207,57 @@
|
|||||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Đợt
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Tỷ lệ
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Số tiền (VND)
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Ngày bắt đầu
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Ngày đến hạn
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Số ngày
|
||||||
</th>
|
</th>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
|
||||||
style="border: none;">Tỷ lệ</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
|
||||||
style="border: none;">Số tiền (VND)</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
|
||||||
bắt đầu</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
|
||||||
đến hạn</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
|
||||||
style="border: none;">Số ngày</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(plan, index) in calculatorData.originalPaymentSchedule" :key="`orig-plan-${index}`"
|
<tr
|
||||||
style="border-bottom: 1px solid #f5f5f5;">
|
v-for="(plan, index) in calculatorData.originalPaymentSchedule"
|
||||||
|
:key="`orig-plan-${index}`"
|
||||||
|
style="border-bottom: 1px solid #f5f5f5"
|
||||||
|
>
|
||||||
<td class="has-text-weight-semibold">Đợt {{ plan.cycle }}</td>
|
<td class="has-text-weight-semibold">Đợt {{ plan.cycle }}</td>
|
||||||
<td class="has-text-right">{{ plan.type === 1 ? `${plan.value}%` : '-' }}</td>
|
<td class="has-text-right">
|
||||||
<td class="has-text-right">{{ $numtoString(plan.amount) }}</td>
|
{{ plan.type === 1 ? `${plan.value}%` : "-" }}
|
||||||
|
</td>
|
||||||
|
<td class="has-text-right">
|
||||||
|
{{ $numtoString(plan.amount) }}
|
||||||
|
</td>
|
||||||
<td>{{ formatDate(plan.from_date) }}</td>
|
<td>{{ formatDate(plan.from_date) }}</td>
|
||||||
<td>{{ formatDate(plan.to_date) }}</td>
|
<td>{{ formatDate(plan.to_date) }}</td>
|
||||||
<td class="has-text-right">{{ plan.days }}</td>
|
<td class="has-text-right">{{ plan.days }}</td>
|
||||||
@@ -151,29 +274,62 @@
|
|||||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Đợt
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Hạn TT Gốc
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Ngày TT Thực Tế
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Số tiền gốc
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Số ngày TT sớm
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Tỷ lệ CK (%/ngày)
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Tiền chiết khấu
|
||||||
</th>
|
</th>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Hạn
|
|
||||||
TT Gốc</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
|
||||||
TT Thực Tế</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
|
||||||
style="border: none;">Số tiền gốc</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
|
||||||
style="border: none;">Số ngày TT sớm</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
|
||||||
style="border: none;">Tỷ lệ CK (%/ngày)</th>
|
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
|
||||||
style="border: none;">Tiền chiết khấu</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(item, idx) in calculatorData.earlyDiscountDetails" :key="`early-discount-${idx}`"
|
<tr
|
||||||
style="border-bottom: 1px solid #f5f5f5;">
|
v-for="(item, idx) in calculatorData.earlyDiscountDetails"
|
||||||
|
:key="`early-discount-${idx}`"
|
||||||
|
style="border-bottom: 1px solid #f5f5f5"
|
||||||
|
>
|
||||||
<td>Đợt {{ item.cycle }}</td>
|
<td>Đợt {{ item.cycle }}</td>
|
||||||
<td>{{ formatDate(item.original_payment_date) }}</td>
|
<td>{{ formatDate(item.original_payment_date) }}</td>
|
||||||
<td>{{ formatDate(item.actual_payment_date) }}</td>
|
<td>{{ formatDate(item.actual_payment_date) }}</td>
|
||||||
<td class="has-text-right">{{ $numtoString(item.original_amount) }}</td>
|
<td class="has-text-right">
|
||||||
|
{{ $numtoString(item.original_amount) }}
|
||||||
|
</td>
|
||||||
<td class="has-text-right">{{ item.early_days }}</td>
|
<td class="has-text-right">{{ item.early_days }}</td>
|
||||||
<td class="has-text-right">{{ item.discount_rate }}</td>
|
<td class="has-text-right">{{ item.discount_rate }}</td>
|
||||||
<td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td>
|
<td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td>
|
||||||
@@ -181,9 +337,15 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="has-background-light">
|
<tr class="has-background-light">
|
||||||
<th colspan="6" class="has-text-right has-text-weight-bold">Tổng chiết khấu thanh toán sớm</th>
|
<th
|
||||||
<th class="has-text-right has-text-weight-bold has-text-danger">-{{
|
colspan="6"
|
||||||
$numtoString(totalEarlyDiscount) }}</th>
|
class="has-text-right has-text-weight-bold"
|
||||||
|
>
|
||||||
|
Tổng chiết khấu thanh toán sớm
|
||||||
|
</th>
|
||||||
|
<th class="has-text-right has-text-weight-bold has-text-danger">
|
||||||
|
-{{ $numtoString(totalEarlyDiscount) }}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
@@ -191,7 +353,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Schedule Table -->
|
<!-- Payment Schedule Table -->
|
||||||
<div v-if="displaySchedule.length > 0" class="mt-4">
|
<div
|
||||||
|
v-if="displaySchedule.length > 0"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
<div class="level m-0 mb-2 is-mobile">
|
<div class="level m-0 mb-2 is-mobile">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<p class="has-text-weight-bold is-size-5 has-text-primary is-underlined">
|
<p class="has-text-weight-bold is-size-5 has-text-primary is-underlined">
|
||||||
@@ -199,14 +364,23 @@
|
|||||||
<span v-else>LỊCH THANH TOÁN</span>
|
<span v-else>LỊCH THANH TOÁN</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right" id="ignore-print">
|
<div
|
||||||
|
class="level-right"
|
||||||
|
id="ignore-print"
|
||||||
|
>
|
||||||
<div class="buttons are-small has-addons">
|
<div class="buttons are-small has-addons">
|
||||||
<button class="button" @click="viewMode = 'table'"
|
<button
|
||||||
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'">
|
class="button"
|
||||||
|
@click="viewMode = 'table'"
|
||||||
|
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'"
|
||||||
|
>
|
||||||
<span class="is-size-6">Bảng</span>
|
<span class="is-size-6">Bảng</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button" @click="viewMode = 'list'"
|
<button
|
||||||
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'">
|
class="button"
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'"
|
||||||
|
>
|
||||||
<span class="is-size-6">Thẻ</span>
|
<span class="is-size-6">Thẻ</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,64 +388,136 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table View -->
|
<!-- Table View -->
|
||||||
<div v-if="viewMode === 'table'" class="table-container schedule-container">
|
<div
|
||||||
|
v-if="viewMode === 'table'"
|
||||||
|
class="table-container schedule-container"
|
||||||
|
>
|
||||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
|
<th
|
||||||
thanh toán</th>
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
style="border: none"
|
||||||
style="border: none;">Số tiền (VND)</th>
|
>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
Đợt thanh toán
|
||||||
style="border: none;">Đã thanh toán</th>
|
</th>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
<th
|
||||||
style="border: none;">Còn phải TT</th>
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
style="border: none"
|
||||||
bắt đầu</th>
|
>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
Số tiền (VND)
|
||||||
đến hạn</th>
|
</th>
|
||||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Trạng
|
<th
|
||||||
thái</th>
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Đã thanh toán
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Còn phải TT
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Ngày bắt đầu
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Ngày đến hạn
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="has-background-primary has-text-white has-font-weight-normal"
|
||||||
|
style="border: none"
|
||||||
|
>
|
||||||
|
Trạng thái
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(plan, index) in displaySchedule" :key="`plan-${index}`"
|
<tr
|
||||||
style="border-bottom: 1px solid #f5f5f5;"
|
v-for="(plan, index) in displaySchedule"
|
||||||
:class="plan.is_merged ? 'has-background-warning-light' : ''">
|
:key="`plan-${index}`"
|
||||||
<td class="has-text-weight-semibold" :class="plan.is_merged ? 'has-text-warning' : ''">
|
style="border-bottom: 1px solid #f5f5f5"
|
||||||
|
:class="plan.is_merged ? 'has-background-warning-light' : ''"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="has-text-weight-semibold"
|
||||||
|
:class="plan.is_merged ? 'has-text-warning' : ''"
|
||||||
|
>
|
||||||
Đợt {{ plan.cycle }}
|
Đợt {{ plan.cycle }}
|
||||||
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span>
|
<span
|
||||||
|
v-if="plan.is_merged"
|
||||||
|
class="tag is-warning is-light ml-1 is-size-7"
|
||||||
|
>GỘP SỚM</span
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
<td class="has-text-right">
|
<td class="has-text-right">
|
||||||
<div v-if="plan.is_merged" class="has-text-right">
|
<div
|
||||||
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount) }}
|
v-if="plan.is_merged"
|
||||||
|
class="has-text-right"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="has-text-grey"
|
||||||
|
title="Tổng các đợt gốc"
|
||||||
|
>
|
||||||
|
{{ $numtoString(totalOriginalEarlyAmount) }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="has-text-danger"
|
||||||
|
title="Chiết khấu thanh toán sớm"
|
||||||
|
>
|
||||||
|
- {{ $numtoString(totalEarlyDiscount) }}
|
||||||
|
</p>
|
||||||
|
<hr
|
||||||
|
class="my-1"
|
||||||
|
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%"
|
||||||
|
/>
|
||||||
|
<p class="has-text-weight-bold">
|
||||||
|
{{ $numtoString(plan.amount) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{
|
|
||||||
$numtoString(totalEarlyDiscount) }}</p>
|
|
||||||
<hr class="my-1"
|
|
||||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%;">
|
|
||||||
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="has-text-right has-text-success">{{ $numtoString(plan.paid_amount) }}</td>
|
<td class="has-text-right has-text-success">
|
||||||
<td class="has-text-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</td>
|
{{ $numtoString(plan.paid_amount) }}
|
||||||
|
</td>
|
||||||
|
<td class="has-text-right has-text-danger">
|
||||||
|
{{ $numtoString(plan.remain_amount) }}
|
||||||
|
</td>
|
||||||
<td>{{ formatDate(plan.from_date) }}</td>
|
<td>{{ formatDate(plan.from_date) }}</td>
|
||||||
<td>{{ formatDate(plan.to_date) }}</td>
|
<td>{{ formatDate(plan.to_date) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span>
|
<span
|
||||||
<span v-else class="tag is-warning">Chờ thanh toán</span>
|
v-if="plan.status === 2"
|
||||||
|
class="tag is-success"
|
||||||
|
>Đã thanh toán</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="tag is-warning"
|
||||||
|
>Chờ thanh toán</span
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="has-background-light">
|
<tr class="has-background-light">
|
||||||
<th class="has-text-right has-text-weight-bold">Tổng cộng</th>
|
<th class="has-text-right has-text-weight-bold">Tổng cộng</th>
|
||||||
<th class="has-text-right has-text-weight-bold">{{ $numtoString(totalAmount) }}</th>
|
<th class="has-text-right has-text-weight-bold">
|
||||||
<th class="has-text-right has-text-weight-bold has-text-success">{{ $numtoString(totalPaid) }}
|
{{ $numtoString(totalAmount) }}
|
||||||
|
</th>
|
||||||
|
<th class="has-text-right has-text-weight-bold has-text-success">
|
||||||
|
{{ $numtoString(totalPaid) }}
|
||||||
|
</th>
|
||||||
|
<th class="has-text-right has-text-weight-bold has-text-danger">
|
||||||
|
{{ $numtoString(calculatorData.totalRemaining) }}
|
||||||
</th>
|
</th>
|
||||||
<th class="has-text-right has-text-weight-bold has-text-danger">{{
|
|
||||||
$numtoString(calculatorData.totalRemaining) }}</th>
|
|
||||||
<th colspan="3"></th>
|
<th colspan="3"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@@ -279,28 +525,57 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- List View (Card) -->
|
<!-- List View (Card) -->
|
||||||
<div v-else-if="viewMode === 'list'" class="schedule-container">
|
<div
|
||||||
<div v-for="(plan, index) in displaySchedule" :key="`card-${index}`" class="card mb-4"
|
v-else-if="viewMode === 'list'"
|
||||||
:class="plan.is_merged ? 'has-background-warning-light' : ''">
|
class="schedule-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(plan, index) in displaySchedule"
|
||||||
|
:key="`card-${index}`"
|
||||||
|
class="card mb-4"
|
||||||
|
:class="plan.is_merged ? 'has-background-warning-light' : ''"
|
||||||
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="level is-mobile mb-5">
|
<div class="level is-mobile mb-5">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<span class="tag is-primary" :class="plan.is_merged ? 'is-warning' : ''">Đợt {{ plan.cycle
|
<span
|
||||||
}}</span>
|
class="tag is-primary"
|
||||||
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span>
|
:class="plan.is_merged ? 'is-warning' : ''"
|
||||||
|
>Đợt {{ plan.cycle }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="plan.is_merged"
|
||||||
|
class="tag is-warning is-light ml-1 is-size-7"
|
||||||
|
>GỘP SỚM</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<div class="level-item has-text-weight-bold">
|
<div class="level-item has-text-weight-bold">
|
||||||
<div v-if="plan.is_merged" class="has-text-right">
|
<div
|
||||||
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount)
|
v-if="plan.is_merged"
|
||||||
}}</p>
|
class="has-text-right"
|
||||||
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{
|
>
|
||||||
$numtoString(totalEarlyDiscount) }}</p>
|
<p
|
||||||
<hr class="my-1"
|
class="has-text-grey"
|
||||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto">
|
title="Tổng các đợt gốc"
|
||||||
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
|
>
|
||||||
|
{{ $numtoString(totalOriginalEarlyAmount) }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="has-text-danger"
|
||||||
|
title="Chiết khấu thanh toán sớm"
|
||||||
|
>
|
||||||
|
- {{ $numtoString(totalEarlyDiscount) }}
|
||||||
|
</p>
|
||||||
|
<hr
|
||||||
|
class="my-1"
|
||||||
|
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto"
|
||||||
|
/>
|
||||||
|
<p class="has-text-weight-bold">
|
||||||
|
{{ $numtoString(plan.amount) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,25 +583,41 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="level is-mobile mb-1">
|
<div class="level is-mobile mb-1">
|
||||||
<div class="level-left">Đã thanh toán:</div>
|
<div class="level-left">Đã thanh toán:</div>
|
||||||
<div class="level-right has-text-success">{{ $numtoString(plan.paid_amount) }}</div>
|
<div class="level-right has-text-success">
|
||||||
|
{{ $numtoString(plan.paid_amount) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level is-mobile mb-1">
|
<div class="level is-mobile mb-1">
|
||||||
<div class="level-left">Còn phải TT:</div>
|
<div class="level-left">Còn phải TT:</div>
|
||||||
<div class="level-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</div>
|
<div class="level-right has-text-danger">
|
||||||
|
{{ $numtoString(plan.remain_amount) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level is-mobile mb-1">
|
<div class="level is-mobile mb-1">
|
||||||
<div class="level-left">Từ ngày:</div>
|
<div class="level-left">Từ ngày:</div>
|
||||||
<div class="level-right">{{ formatDate(plan.from_date) }}</div>
|
<div class="level-right">
|
||||||
|
{{ formatDate(plan.from_date) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level is-mobile mb-1">
|
<div class="level is-mobile mb-1">
|
||||||
<div class="level-left">Đến hạn:</div>
|
<div class="level-left">Đến hạn:</div>
|
||||||
<div class="level-right">{{ formatDate(plan.to_date) }}</div>
|
<div class="level-right">
|
||||||
|
{{ formatDate(plan.to_date) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level is-mobile">
|
<div class="level is-mobile">
|
||||||
<div class="level-left">Trạng thái:</div>
|
<div class="level-left">Trạng thái:</div>
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span>
|
<span
|
||||||
<span v-else class="tag is-warning">Chờ thanh toán</span>
|
v-if="plan.status === 2"
|
||||||
|
class="tag is-success"
|
||||||
|
>Đã thanh toán</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="tag is-warning"
|
||||||
|
>Chờ thanh toán</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,7 +626,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Summary Footer -->
|
<!-- Summary Footer -->
|
||||||
<div class="" style="border-top: 1px solid #eee;">
|
<div
|
||||||
|
class=""
|
||||||
|
style="border-top: 1px solid #eee"
|
||||||
|
>
|
||||||
<div class="level is-mobile is-size-6 my-4">
|
<div class="level is-mobile is-size-6 my-4">
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
@@ -354,22 +648,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from "vue";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
// Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN
|
// Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
productData: {
|
productData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
selectedPolicy: {
|
selectedPolicy: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
selectedCustomer: {
|
selectedCustomer: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
calculatorData: {
|
calculatorData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -390,15 +684,15 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(['print']);
|
const emit = defineEmits(["print"]);
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const viewMode = ref('table');
|
const viewMode = ref("table");
|
||||||
|
|
||||||
// Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN
|
// Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN
|
||||||
const displaySchedule = computed(() => {
|
const displaySchedule = computed(() => {
|
||||||
@@ -426,8 +720,8 @@ const totalPaid = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (date) => {
|
const formatDate = (date) => {
|
||||||
if (!date) return '-';
|
if (!date) return "-";
|
||||||
return dayjs(date).format('DD/MM/YYYY');
|
return dayjs(date).format("DD/MM/YYYY");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -501,4 +795,4 @@ th,
|
|||||||
page-break-inside: avoid !important;
|
page-break-inside: avoid !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,3 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
</script>
|
<template>Cash Book</template>
|
||||||
|
|
||||||
<template>
|
|
||||||
Cash Book
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -25,12 +25,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<template v-if="option === 'your'">
|
<template v-if="option === 'your'">
|
||||||
<template v-if="tab === 'message'">
|
<template v-if="tab === 'message'">
|
||||||
<div class="field is-grouped" v-for="(v, i) in message">
|
<div
|
||||||
|
class="field is-grouped"
|
||||||
|
v-for="(v, i) in message"
|
||||||
|
>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<textarea class="textarea" placeholder="" rows="3" v-model="v.text"></textarea>
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
placeholder=""
|
||||||
|
rows="3"
|
||||||
|
v-model="v.text"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a class="mr-3" @click="add()">
|
<a
|
||||||
|
class="mr-3"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 20 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a @click="remove(v, i)">
|
<a @click="remove(v, i)">
|
||||||
@@ -44,22 +55,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button>
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="update()"
|
||||||
|
>
|
||||||
|
Cập nhật
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="tab === 'image'">
|
<template v-else-if="tab === 'image'">
|
||||||
<div class="field is-grouped mb-0">
|
<div class="field is-grouped mb-0">
|
||||||
<div class="control is-expanded"></div>
|
<div class="control is-expanded"></div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<FileUpload v-bind="{ position: 'left' }" @files="getImages"></FileUpload>
|
<FileUpload
|
||||||
|
v-bind="{ position: 'left' }"
|
||||||
|
@files="getImages"
|
||||||
|
></FileUpload>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0">
|
<div
|
||||||
<div class="control mb-2" v-for="(v, i) in image">
|
class="field is-grouped is-grouped-multiline"
|
||||||
|
v-if="image.length > 0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="control mb-2"
|
||||||
|
v-for="(v, i) in image"
|
||||||
|
>
|
||||||
<ChipImage
|
<ChipImage
|
||||||
style="width: 128px"
|
style="width: 128px"
|
||||||
@remove="removeImage(v, i)"
|
@remove="removeImage(v, i)"
|
||||||
v-bind="{ show: ['copy', 'download', 'delete'], file: v, image: `${$getpath()}static/files/${v.file}` }"
|
v-bind="{
|
||||||
|
show: ['copy', 'download', 'delete'],
|
||||||
|
file: v,
|
||||||
|
image: `${$getpath()}static/files/${v.file}`,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
</ChipImage>
|
</ChipImage>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,24 +98,47 @@
|
|||||||
<div class="field is-grouped mb-0">
|
<div class="field is-grouped mb-0">
|
||||||
<div class="control is-expanded"></div>
|
<div class="control is-expanded"></div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<FileUpload v-bind="{ position: 'left', type: 'file' }" @files="getFiles"></FileUpload>
|
<FileUpload
|
||||||
|
v-bind="{ position: 'left', type: 'file' }"
|
||||||
|
@files="getFiles"
|
||||||
|
></FileUpload>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FileShow @remove="removeFile" v-bind="{ files: file, show: { delete: 1 } }"></FileShow>
|
<FileShow
|
||||||
|
@remove="removeFile"
|
||||||
|
v-bind="{ files: file, show: { delete: 1 } }"
|
||||||
|
></FileShow>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="tab === 'link'">
|
<template v-else-if="tab === 'link'">
|
||||||
<div class="field is-grouped" v-for="(v, i) in link">
|
<div
|
||||||
|
class="field is-grouped"
|
||||||
|
v-for="(v, i) in link"
|
||||||
|
>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<input class="input" placeholder="" v-model="v.link" />
|
<input
|
||||||
|
class="input"
|
||||||
|
placeholder=""
|
||||||
|
v-model="v.link"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a class="mr-3" @click="copyContent(v.link)">
|
<a
|
||||||
|
class="mr-3"
|
||||||
|
@click="copyContent(v.link)"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a class="mr-3" :href="v.link" target="_blank">
|
<a
|
||||||
|
class="mr-3"
|
||||||
|
:href="v.link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'open.svg', type: 'primary', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'open.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a class="mr-3" @click="addLink()">
|
<a
|
||||||
|
class="mr-3"
|
||||||
|
@click="addLink()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 18 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 18 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a @click="removeLink(v, i)">
|
<a @click="removeLink(v, i)">
|
||||||
@@ -95,14 +147,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button>
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="update()"
|
||||||
|
>
|
||||||
|
Cập nhật
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="option === 'system'">
|
<template v-else-if="option === 'system'">
|
||||||
<template v-if="tab === 'message'">
|
<template v-if="tab === 'message'">
|
||||||
<div v-if="message">
|
<div v-if="message">
|
||||||
<div class="px-2 py-2 mb-2" style="border: 1px solid #e8e8e8" v-for="(v, i) in message">
|
<div
|
||||||
|
class="px-2 py-2 mb-2"
|
||||||
|
style="border: 1px solid #e8e8e8"
|
||||||
|
v-for="(v, i) in message"
|
||||||
|
>
|
||||||
<span class="mr-3">{{ v.text }}</span>
|
<span class="mr-3">{{ v.text }}</span>
|
||||||
<a @click="copyContent(v.text)">
|
<a @click="copyContent(v.text)">
|
||||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||||
@@ -111,11 +172,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="tab === 'image'">
|
<template v-else-if="tab === 'image'">
|
||||||
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0">
|
<div
|
||||||
<div class="control mb-2" v-for="(v, i) in image">
|
class="field is-grouped is-grouped-multiline"
|
||||||
|
v-if="image.length > 0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="control mb-2"
|
||||||
|
v-for="(v, i) in image"
|
||||||
|
>
|
||||||
<ChipImage
|
<ChipImage
|
||||||
style="width: 128px"
|
style="width: 128px"
|
||||||
v-bind="{ show: ['copy', 'download'], file: v, image: `${$getpath()}static/files/${v.file}` }"
|
v-bind="{
|
||||||
|
show: ['copy', 'download'],
|
||||||
|
file: v,
|
||||||
|
image: `${$getpath()}static/files/${v.file}`,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
</ChipImage>
|
</ChipImage>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,8 +196,17 @@
|
|||||||
<FileShow v-bind="{ files: file }"></FileShow>
|
<FileShow v-bind="{ files: file }"></FileShow>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="tab === 'link'">
|
<template v-else-if="tab === 'link'">
|
||||||
<div class="px-2 py-2 mb-2" style="border: 1px solid #e8e8e8" v-for="(v, i) in link">
|
<div
|
||||||
<a :href="v.link" target="_blank" class="mr-3">{{ v.link }}</a>
|
class="px-2 py-2 mb-2"
|
||||||
|
style="border: 1px solid #e8e8e8"
|
||||||
|
v-for="(v, i) in link"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="v.link"
|
||||||
|
target="_blank"
|
||||||
|
class="mr-3"
|
||||||
|
>{{ v.link }}</a
|
||||||
|
>
|
||||||
<a @click="copyContent(v.link)">
|
<a @click="copyContent(v.link)">
|
||||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -3,20 +3,26 @@ const props = defineProps({
|
|||||||
text: String,
|
text: String,
|
||||||
image: String,
|
image: String,
|
||||||
type: String,
|
type: String,
|
||||||
size: Number
|
size: Number,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
@click="$emit('justclick')"
|
@click="$emit('justclick')"
|
||||||
class="rounded-full mx-0 px-0 size-10 font-bold is-flex is-justify-content-center is-align-items-center"
|
class="rounded-full mx-0 px-0 size-10 font-bold is-flex is-justify-content-center is-align-items-center"
|
||||||
:style="{
|
:style="{
|
||||||
border: image ? 'none' : '1px solid var(--bulma-grey-70)'
|
border: image ? 'none' : '1px solid var(--bulma-grey-70)',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<figure v-if="image" class="image">
|
<figure
|
||||||
<img class="is-rounded" :src="`${$path()}download?name=${image}`">
|
v-if="image"
|
||||||
|
class="image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="is-rounded"
|
||||||
|
:src="`${$path()}download?name=${image}`"
|
||||||
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
<span v-else>{{text}}</span>
|
<span v-else>{{ text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="handleClick()" class="is-clickable">
|
<div
|
||||||
|
@click="handleClick()"
|
||||||
|
class="is-clickable"
|
||||||
|
>
|
||||||
<template v-if="count > 0">
|
<template v-if="count > 0">
|
||||||
<span class="dot-primary">
|
<span class="dot-primary">
|
||||||
{{ count }}
|
{{ count }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="dot-primary">
|
<span class="dot-primary"> + </span>
|
||||||
+
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['row', 'countField', 'modalConfig'],
|
props: ["row", "countField", "modalConfig"],
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
count() {
|
count() {
|
||||||
return this.row[this.countField] || 0;
|
return this.row[this.countField] || 0;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleClick() {
|
handleClick() {
|
||||||
if (!this.modalConfig) return;
|
if (!this.modalConfig) return;
|
||||||
|
|
||||||
let config = this.$copy(this.modalConfig);
|
let config = this.$copy(this.modalConfig);
|
||||||
|
|
||||||
this.$emit('open', {
|
this.$emit("open", {
|
||||||
name: 'dataevent',
|
name: "dataevent",
|
||||||
data: {
|
data: {
|
||||||
modal: config
|
modal: config,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,135 +1,141 @@
|
|||||||
<!-- CountdownTimer.vue -->
|
<!-- CountdownTimer.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="countdown-wrapper">
|
<div class="countdown-wrapper">
|
||||||
<span v-if="isExpired" class="tag is-danger">
|
<span
|
||||||
{{ isVietnamese ? 'Hết giờ' : 'Expired' }}
|
v-if="isExpired"
|
||||||
|
class="tag is-danger"
|
||||||
|
>
|
||||||
|
{{ isVietnamese ? "Hết giờ" : "Expired" }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="tag" :class="tagClass">
|
<span
|
||||||
|
v-else
|
||||||
|
class="tag"
|
||||||
|
:class="tagClass"
|
||||||
|
>
|
||||||
<span class="countdown-text">{{ formattedTime }}</span>
|
<span class="countdown-text">{{ formattedTime }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { useStore } from '@/stores/index'
|
import { useStore } from "@/stores/index";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dateValue: {
|
dateValue: {
|
||||||
type: [String, Date],
|
type: [String, Date],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
format: {
|
format: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'HH:mm:ss'
|
default: "HH:mm:ss",
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
const { $dayjs } = useNuxtApp()
|
const { $dayjs } = useNuxtApp();
|
||||||
|
|
||||||
const timeRemaining = ref({
|
const timeRemaining = ref({
|
||||||
days: 0,
|
days: 0,
|
||||||
hours: 0,
|
hours: 0,
|
||||||
minutes: 0,
|
minutes: 0,
|
||||||
seconds: 0
|
seconds: 0,
|
||||||
})
|
});
|
||||||
|
|
||||||
const isExpired = ref(false)
|
const isExpired = ref(false);
|
||||||
let intervalId = null
|
let intervalId = null;
|
||||||
|
|
||||||
const isVietnamese = computed(() => store.lang === 'vi')
|
const isVietnamese = computed(() => store.lang === "vi");
|
||||||
|
|
||||||
const tagClass = computed(() => {
|
const tagClass = computed(() => {
|
||||||
const totalSeconds = timeRemaining.value.days * 86400 +
|
const totalSeconds =
|
||||||
timeRemaining.value.hours * 3600 +
|
timeRemaining.value.days * 86400 +
|
||||||
timeRemaining.value.minutes * 60 +
|
timeRemaining.value.hours * 3600 +
|
||||||
timeRemaining.value.seconds
|
timeRemaining.value.minutes * 60 +
|
||||||
|
timeRemaining.value.seconds;
|
||||||
|
|
||||||
if (totalSeconds <= 0) return 'is-danger'
|
if (totalSeconds <= 0) return "is-danger";
|
||||||
if (totalSeconds <= 3600) return 'is-warning' // <= 1 hour
|
if (totalSeconds <= 3600) return "is-warning"; // <= 1 hour
|
||||||
if (totalSeconds <= 86400) return 'is-info' // <= 1 day
|
if (totalSeconds <= 86400) return "is-info"; // <= 1 day
|
||||||
return 'is-primary' // > 1 day
|
return "is-primary"; // > 1 day
|
||||||
})
|
});
|
||||||
|
|
||||||
const formattedTime = computed(() => {
|
const formattedTime = computed(() => {
|
||||||
const { days, hours, minutes, seconds } = timeRemaining.value
|
const { days, hours, minutes, seconds } = timeRemaining.value;
|
||||||
|
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
return isVietnamese
|
return isVietnamese ? `${days}d ${hours}h ${minutes}m` : `${days}d ${hours}h ${minutes}m`;
|
||||||
? `${days}d ${hours}h ${minutes}m`
|
|
||||||
: `${days}d ${hours}h ${minutes}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return isVietnamese
|
|
||||||
? `${hours}h ${minutes}m ${seconds}s`
|
|
||||||
: `${hours}h ${minutes}m ${seconds}s`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isVietnamese
|
if (hours > 0) {
|
||||||
? `${minutes}m ${seconds}s`
|
return isVietnamese ? `${hours}h ${minutes}m ${seconds}s` : `${hours}h ${minutes}m ${seconds}s`;
|
||||||
: `${minutes}m ${seconds}s`
|
}
|
||||||
})
|
|
||||||
|
return isVietnamese ? `${minutes}m ${seconds}s` : `${minutes}m ${seconds}s`;
|
||||||
|
});
|
||||||
|
|
||||||
const calculateTimeRemaining = () => {
|
const calculateTimeRemaining = () => {
|
||||||
try {
|
try {
|
||||||
const targetDate = $dayjs(props.dateValue)
|
const targetDate = $dayjs(props.dateValue);
|
||||||
const now = $dayjs()
|
const now = $dayjs();
|
||||||
|
|
||||||
if (now.isAfter(targetDate)) {
|
if (now.isAfter(targetDate)) {
|
||||||
isExpired.value = true
|
isExpired.value = true;
|
||||||
timeRemaining.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
|
timeRemaining.value = { days: 0, hours: 0, minutes: 0, seconds: 0 };
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isExpired.value = false
|
isExpired.value = false;
|
||||||
const diff = targetDate.diff(now, 'second')
|
const diff = targetDate.diff(now, "second");
|
||||||
|
|
||||||
const days = Math.floor(diff / 86400)
|
const days = Math.floor(diff / 86400);
|
||||||
const hours = Math.floor((diff % 86400) / 3600)
|
const hours = Math.floor((diff % 86400) / 3600);
|
||||||
const minutes = Math.floor((diff % 3600) / 60)
|
const minutes = Math.floor((diff % 3600) / 60);
|
||||||
const seconds = diff % 60
|
const seconds = diff % 60;
|
||||||
|
|
||||||
timeRemaining.value = { days, hours, minutes, seconds }
|
timeRemaining.value = { days, hours, minutes, seconds };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error calculating countdown:', error)
|
console.error("Error calculating countdown:", error);
|
||||||
isExpired.value = true
|
isExpired.value = true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const startCountdown = () => {
|
const startCountdown = () => {
|
||||||
calculateTimeRemaining()
|
calculateTimeRemaining();
|
||||||
|
|
||||||
if (intervalId) clearInterval(intervalId)
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
|
||||||
intervalId = setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
calculateTimeRemaining()
|
calculateTimeRemaining();
|
||||||
|
|
||||||
if (isExpired.value && intervalId) {
|
if (isExpired.value && intervalId) {
|
||||||
clearInterval(intervalId)
|
clearInterval(intervalId);
|
||||||
intervalId = null
|
intervalId = null;
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000);
|
||||||
}
|
};
|
||||||
|
|
||||||
watch(() => props.dateValue, () => {
|
watch(
|
||||||
startCountdown()
|
() => props.dateValue,
|
||||||
}, { deep: true })
|
() => {
|
||||||
|
startCountdown();
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startCountdown()
|
startCountdown();
|
||||||
})
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
clearInterval(intervalId)
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.countdown-wrapper {
|
.countdown-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const toolbarOptions = [
|
|||||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||||
|
|
||||||
// ✍️ Định dạng cơ bản
|
// ✍️ Định dạng cơ bản
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
["bold", "italic", "underline", "strike"],
|
||||||
|
|
||||||
// 🎨 Màu chữ & nền
|
// 🎨 Màu chữ & nền
|
||||||
[{ color: [] }, { background: [] }],
|
[{ color: [] }, { background: [] }],
|
||||||
@@ -46,14 +46,13 @@ const toolbarOptions = [
|
|||||||
[{ align: [] }],
|
[{ align: [] }],
|
||||||
|
|
||||||
// 📋 Danh sách
|
// 📋 Danh sách
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
[{ list: "ordered" }, { list: "bullet" }],
|
||||||
|
|
||||||
// 🔗 Media
|
// 🔗 Media
|
||||||
['link', 'image', 'video'],
|
["link", "image", "video"],
|
||||||
|
|
||||||
['clean'], // Xóa định dạng
|
|
||||||
]
|
|
||||||
|
|
||||||
|
["clean"], // Xóa định dạng
|
||||||
|
];
|
||||||
|
|
||||||
var content = props.text;
|
var content = props.text;
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input :class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-black' : ''}`" type="text"
|
<input
|
||||||
:placeholder="placeholder || ''" v-model="value" @keyup="doCheck" :disabled="disabled || false">
|
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-black' : ''}`"
|
||||||
<span class="icon is-left">
|
type="text"
|
||||||
<SvgIcon v-bind="{name: 'email.svg', type: 'gray', size: 21}"></SvgIcon>
|
:placeholder="placeholder || ''"
|
||||||
</span>
|
v-model="value"
|
||||||
</div>
|
@keyup="doCheck"
|
||||||
|
:disabled="disabled || false"
|
||||||
|
/>
|
||||||
|
<span class="icon is-left">
|
||||||
|
<SvgIcon v-bind="{ name: 'email.svg', type: 'gray', size: 21 }"></SvgIcon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['record', 'attr', 'placeholder', 'disabled'],
|
props: ["record", "attr", "placeholder", "disabled"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined,
|
value: this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined,
|
||||||
error: undefined
|
error: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
record: function(newVal) {
|
record: function (newVal) {
|
||||||
this.value = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined
|
this.value = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doCheck() {
|
doCheck() {
|
||||||
if(this.$empty(this.value)) {
|
if (this.$empty(this.value)) {
|
||||||
this.value = undefined
|
this.value = undefined;
|
||||||
this.error = false
|
this.error = false;
|
||||||
return this.$emit('email', null)
|
return this.$emit("email", null);
|
||||||
}
|
}
|
||||||
let check = this.$errEmail(this.value)
|
let check = this.$errEmail(this.value);
|
||||||
this.error = check? true : false
|
this.error = check ? true : false;
|
||||||
this.$emit('email', this.value)
|
this.$emit("email", this.value);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
/>
|
/>
|
||||||
<span class="icon is-left">
|
<span class="icon is-left">
|
||||||
<SvgIcon v-bind="{name: 'phone.png', type: 'gray', size: 20}"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'phone.png', type: 'gray', size: 20 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea v-model="record.note" class="textarea" name="note" placeholder="" rows="8"></textarea>
|
<textarea
|
||||||
|
v-model="record.note"
|
||||||
|
class="textarea"
|
||||||
|
name="note"
|
||||||
|
placeholder=""
|
||||||
|
rows="8"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button class="button is-primary has-text-white" @click="save()">
|
<button
|
||||||
{{ $store.lang==='vi'? 'Lưu lại' : 'Save' }}
|
class="button is-primary has-text-white"
|
||||||
|
@click="save()"
|
||||||
|
>
|
||||||
|
{{ $store.lang === "vi" ? "Lưu lại" : "Save" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(["close"])
|
const emit = defineEmits(["close"]);
|
||||||
const { $store, $getdata, $updateapi, $updatepage } = useNuxtApp();
|
const { $store, $getdata, $updateapi, $updatepage } = useNuxtApp();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
row: Object,
|
row: Object,
|
||||||
pagename: String
|
pagename: String,
|
||||||
})
|
});
|
||||||
var record = await $getdata('application', {id: props.row.id}, undefined, true)
|
var record = await $getdata("application", { id: props.row.id }, undefined, true);
|
||||||
async function save() {
|
async function save() {
|
||||||
await $updateapi('application', record)
|
await $updateapi("application", record);
|
||||||
record = await $getdata('application', {id: props.row.id}, undefined, true)
|
record = await $getdata("application", { id: props.row.id }, undefined, true);
|
||||||
$updatepage(props.pagename, record)
|
$updatepage(props.pagename, record);
|
||||||
emit('close')
|
emit("close");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<template v-if="data">
|
<template v-if="data">
|
||||||
<article class="message is-findata" v-if="data.length===0">
|
<article
|
||||||
<div class="message-body py-2 fs-16">
|
class="message is-findata"
|
||||||
Chưa có <b>ghi chú</b> nào được lưu
|
v-if="data.length === 0"
|
||||||
</div>
|
>
|
||||||
|
<div class="message-body py-2 fs-16">Chưa có <b>ghi chú</b> nào được lưu</div>
|
||||||
</article>
|
</article>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<article class="media mt-0 mb-0" v-for="(v,i) in data">
|
<article
|
||||||
|
class="media mt-0 mb-0"
|
||||||
|
v-for="(v, i) in data"
|
||||||
|
>
|
||||||
<figure class="media-left">
|
<figure class="media-left">
|
||||||
<Avatarbox v-bind="{
|
<Avatarbox
|
||||||
text: v.user__fullname[0].toUpperCase(),
|
v-bind="{
|
||||||
size: 'two',
|
text: v.user__fullname[0].toUpperCase(),
|
||||||
type: 'primary'
|
size: 'two',
|
||||||
}" />
|
type: 'primary',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div>
|
<div>
|
||||||
@@ -22,14 +28,20 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="mt-1 fs-14 has-text-grey">
|
<p class="mt-1 fs-14 has-text-grey">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span>{{v.user__fullname}}</span>
|
<span>{{ v.user__fullname }}</span>
|
||||||
<span class="ml-3">{{ $dayjs(v['create_time']).fromNow(true) }}</span>
|
<span class="ml-3">{{ $dayjs(v["create_time"]).fromNow(true) }}</span>
|
||||||
<template v-if="login.id===v.user">
|
<template v-if="login.id === v.user">
|
||||||
<a class="ml-3" @click="edit(v)">
|
<a
|
||||||
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 20}"></SvgIcon>
|
class="ml-3"
|
||||||
|
@click="edit(v)"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'gray', size: 20 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a class="ml-3" @click="askConfirm(v, i)">
|
<a
|
||||||
<SvgIcon v-bind="{name: 'bin.svg', type: 'gray', size: 20}"></SvgIcon>
|
class="ml-3"
|
||||||
|
@click="askConfirm(v, i)"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'bin.svg', type: 'gray', size: 20 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
@@ -39,87 +51,126 @@
|
|||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="$getEditRights()" class="field is-grouped mt-3">
|
<div
|
||||||
|
v-if="$getEditRights()"
|
||||||
|
class="field is-grouped mt-3"
|
||||||
|
>
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
<textarea class="textarea" rows="2" placeholder="Viết ghi chú tại đây" v-model="detail"></textarea>
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Viết ghi chú tại đây"
|
||||||
|
v-model="detail"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-primary has-text-white" @click="save()">Lưu</button>
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="save()"
|
||||||
|
>
|
||||||
|
Lưu
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" @confirm="confirm()"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
@confirm="confirm()"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['row', 'api', 'pagename'],
|
props: ["row", "api", "pagename"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
detail: undefined,
|
detail: undefined,
|
||||||
vbind2: {image: undefined, text: 'ABC', size: 'two', type: 'findata'},
|
vbind2: { image: undefined, text: "ABC", size: "two", type: "findata" },
|
||||||
current: undefined,
|
current: undefined,
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
obj: undefined
|
obj: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
if(!this.row) return
|
if (!this.row) return;
|
||||||
this.data = await this.$getdata(this.api, {ref: this.row.id})
|
this.data = await this.$getdata(this.api, { ref: this.row.id });
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
login: {
|
login: {
|
||||||
get: function() {return this.$store.login},
|
get: function () {
|
||||||
set: function(val) {this.$store.commit("updateLogin", {login: val})}
|
return this.$store.login;
|
||||||
}
|
},
|
||||||
|
set: function (val) {
|
||||||
|
this.$store.commit("updateLogin", { login: val });
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async save() {
|
async save() {
|
||||||
if(this.$empty(this.detail)) return this.$snackbar('Chưa nhập nội dung ghi chú')
|
if (this.$empty(this.detail)) return this.$snackbar("Chưa nhập nội dung ghi chú");
|
||||||
let data = {user: this.$store.login.id, detail: this.detail, ref: this.row.id}
|
let data = {
|
||||||
if(this.current) {
|
user: this.$store.login.id,
|
||||||
data = this.$copy(this.current)
|
detail: this.detail,
|
||||||
data.detail = this.detail
|
ref: this.row.id,
|
||||||
|
};
|
||||||
|
if (this.current) {
|
||||||
|
data = this.$copy(this.current);
|
||||||
|
data.detail = this.detail;
|
||||||
}
|
}
|
||||||
let rs = data.id? await this.$updateapi(this.api, data) : await this.$insertapi(this.api, data)
|
let rs = data.id ? await this.$updateapi(this.api, data) : await this.$insertapi(this.api, data);
|
||||||
if(!rs) return
|
if (!rs) return;
|
||||||
this.detail = undefined
|
this.detail = undefined;
|
||||||
if(this.current) {
|
if (this.current) {
|
||||||
this.current = undefined
|
this.current = undefined;
|
||||||
let idx = this.$findIndex(this.data, {id: rs.id})
|
let idx = this.$findIndex(this.data, { id: rs.id });
|
||||||
this.$set(this.data, idx, rs)
|
this.$set(this.data, idx, rs);
|
||||||
} else {
|
} else {
|
||||||
this.data.push(rs)
|
this.data.push(rs);
|
||||||
let rows = this.$copy(this.$store[this.pagename].data)
|
let rows = this.$copy(this.$store[this.pagename].data);
|
||||||
let idx = this.$findIndex(rows, {id: this.row.id})
|
let idx = this.$findIndex(rows, { id: this.row.id });
|
||||||
let copy = this.$copy(this.row)
|
let copy = this.$copy(this.row);
|
||||||
copy.count_note += 1
|
copy.count_note += 1;
|
||||||
rows[idx] = copy
|
rows[idx] = copy;
|
||||||
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}})
|
this.$store.commit("updateState", {
|
||||||
|
name: this.pagename,
|
||||||
|
key: "update",
|
||||||
|
data: { data: rows },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
edit(v) {
|
edit(v) {
|
||||||
this.current = this.$copy(v)
|
this.current = this.$copy(v);
|
||||||
this.detail = v.detail
|
this.detail = v.detail;
|
||||||
},
|
},
|
||||||
askConfirm(v, i) {
|
askConfirm(v, i) {
|
||||||
this.obj = {v: v, i: i}
|
this.obj = { v: v, i: i };
|
||||||
this.showmodal = {component: `dialog/Confirm`,vbind: {content: 'Bạn có muốn xóa ghi chú này không?', duration: 10},
|
this.showmodal = {
|
||||||
title: 'Xóa ghi chú', width: '500px', height: '100px'}
|
component: `dialog/Confirm`,
|
||||||
|
vbind: { content: "Bạn có muốn xóa ghi chú này không?", duration: 10 },
|
||||||
|
title: "Xóa ghi chú",
|
||||||
|
width: "500px",
|
||||||
|
height: "100px",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
async confirm() {
|
async confirm() {
|
||||||
let v = this.obj.v
|
let v = this.obj.v;
|
||||||
let i = this.obj.i
|
let i = this.obj.i;
|
||||||
let rs = await this.$deleteapi(this.api, v.id)
|
let rs = await this.$deleteapi(this.api, v.id);
|
||||||
if(rs==='error') return
|
if (rs === "error") return;
|
||||||
this.$delete(this.data, i)
|
this.$delete(this.data, i);
|
||||||
let rows = this.$copy(this.$store[this.pagename].data)
|
let rows = this.$copy(this.$store[this.pagename].data);
|
||||||
let idx = this.$findIndex(rows, {id: this.row.id})
|
let idx = this.$findIndex(rows, { id: this.row.id });
|
||||||
let copy = this.$copy(this.row)
|
let copy = this.$copy(this.row);
|
||||||
copy.count_note -= 1
|
copy.count_note -= 1;
|
||||||
rows[idx] = copy
|
rows[idx] = copy;
|
||||||
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}})
|
this.$store.commit("updateState", {
|
||||||
}
|
name: this.pagename,
|
||||||
}
|
key: "update",
|
||||||
}
|
data: { data: rows },
|
||||||
</script>
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
v-if="row.count_note || $getEditRights()"
|
v-if="row.count_note || $getEditRights()"
|
||||||
class="dot-primary"
|
class="dot-primary"
|
||||||
@click="doClick()"
|
@click="doClick()"
|
||||||
>{{ row.count_note || '+' }}</span>
|
>{{ row.count_note || "+" }}</span
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['row', 'api', 'pagename'],
|
props: ["row", "api", "pagename"],
|
||||||
methods: {
|
methods: {
|
||||||
doClick() {
|
doClick() {
|
||||||
let obj = {component: 'common/NoteInfo', title: 'Ghi chú', width: '50%', vbind: {row: this.row, api: this.api, pagename: this.pagename}}
|
let obj = {
|
||||||
this.$emit('open', {name: 'dataevent', data: {modal: obj}})
|
component: "common/NoteInfo",
|
||||||
}
|
title: "Ghi chú",
|
||||||
}
|
width: "50%",
|
||||||
}
|
vbind: { row: this.row, api: this.api, pagename: this.pagename },
|
||||||
</script>
|
};
|
||||||
|
this.$emit("open", { name: "dataevent", data: { modal: obj } });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,54 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<p class="fsb-30">{{ text }}
|
<p class="fsb-30">
|
||||||
<a class="ml-3" @click="copy()">
|
{{ text }}
|
||||||
<SvgIcon v-bind="{name: 'copy.svg', type: 'primary', size: 24}"></SvgIcon>
|
<a
|
||||||
|
class="ml-3"
|
||||||
|
@click="copy()"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 24 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="buttons mt-4">
|
<p class="buttons mt-4">
|
||||||
<button class="button is-primary" @click="call()">Call</button>
|
<button
|
||||||
<button class="button is-primary" @click="sms()">SMS</button>
|
class="button is-primary"
|
||||||
<button class="button is-primary" @click="openZalo()">Zalo</button>
|
@click="call()"
|
||||||
|
>
|
||||||
|
Call
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-primary"
|
||||||
|
@click="sms()"
|
||||||
|
>
|
||||||
|
SMS
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-primary"
|
||||||
|
@click="openZalo()"
|
||||||
|
>
|
||||||
|
Zalo
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['row', 'pagename'],
|
props: ["row", "pagename"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
text: undefined,
|
text: undefined,
|
||||||
phone: this.row.customer__phone || this.row.party__phone || this.row.phone,
|
phone: this.row.customer__phone || this.row.party__phone || this.row.phone,
|
||||||
showmodal: undefined
|
showmodal: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
var format = function(s) {
|
var format = function (s) {
|
||||||
return `${s.slice(0,3)} ${s.slice(3,6)} ${s.slice(6, 20)}`
|
return `${s.slice(0, 3)} ${s.slice(3, 6)} ${s.slice(6, 20)}`;
|
||||||
}
|
};
|
||||||
this.text = format(this.phone)
|
this.text = format(this.phone);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
call() {
|
call() {
|
||||||
window.open(`tel:${this.phone}`)
|
window.open(`tel:${this.phone}`);
|
||||||
},
|
},
|
||||||
sms() {
|
sms() {
|
||||||
window.open(`sms:${this.phone}`)
|
window.open(`sms:${this.phone}`);
|
||||||
},
|
},
|
||||||
sendSms() {
|
sendSms() {
|
||||||
let api = this.row.code.indexOf('CN')>=0? 'customersms' : undefined
|
let api = this.row.code.indexOf("CN") >= 0 ? "customersms" : undefined;
|
||||||
if(this.row.code.indexOf('LN')>=0) api = 'loansms'
|
if (this.row.code.indexOf("LN") >= 0) api = "loansms";
|
||||||
else if(this.row.code.indexOf('TS')>=0) api = 'collateralsms'
|
else if (this.row.code.indexOf("TS") >= 0) api = "collateralsms";
|
||||||
this.showmodal = {component: 'user/Sms', title: 'Nhắn tin SMS', width: '50%', height: '400px',
|
this.showmodal = {
|
||||||
vbind: {row: this.row, pagename: this.pagename, api: api}}
|
component: "user/Sms",
|
||||||
|
title: "Nhắn tin SMS",
|
||||||
|
width: "50%",
|
||||||
|
height: "400px",
|
||||||
|
vbind: { row: this.row, pagename: this.pagename, api: api },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
copy() {
|
copy() {
|
||||||
this.$copyToClipboard(this.phone)
|
this.$copyToClipboard(this.phone);
|
||||||
},
|
},
|
||||||
openZalo() {
|
openZalo() {
|
||||||
window.open(`https://zalo.me/${this.phone}`, '_blank')
|
window.open(`https://zalo.me/${this.phone}`, "_blank");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="dot-primary" @click="onClick()">{{ row.count_product }}</span>
|
<span
|
||||||
|
class="dot-primary"
|
||||||
|
@click="onClick()"
|
||||||
|
>{{ row.count_product }}</span
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
// use in Khách hàng -> Giao dịch (<DataView :setting='customer-all-transaction'/>)
|
// use in Khách hàng -> Giao dịch (<DataView :setting='customer-all-transaction'/>)
|
||||||
export default {
|
export default {
|
||||||
props: ['row', 'api', 'pagename'],
|
props: ["row", "api", "pagename"],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
const obj = {
|
const obj = {
|
||||||
component: 'common/ProductInfo',
|
component: "common/ProductInfo",
|
||||||
title: 'Sản phẩm',
|
title: "Sản phẩm",
|
||||||
width: '60%',
|
width: "60%",
|
||||||
height: '400px',
|
height: "400px",
|
||||||
vbind: {
|
vbind: {
|
||||||
row: this.row,
|
row: this.row,
|
||||||
pagename: this.pagename
|
pagename: this.pagename,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
this.$emit('open', {name: 'dataevent', data: { modal: obj }})
|
this.$emit("open", { name: "dataevent", data: { modal: obj } });
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
row: Object,
|
row: Object,
|
||||||
pagename: String
|
pagename: String,
|
||||||
});
|
});
|
||||||
const { $id } = useNuxtApp();
|
const { $id } = useNuxtApp();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<DataView v-bind="{
|
<DataView
|
||||||
setting: 'product-info',
|
v-bind="{
|
||||||
pagename: $id(),
|
setting: 'product-info',
|
||||||
api: 'product',
|
pagename: $id(),
|
||||||
params: {
|
api: 'product',
|
||||||
filter: { prdbk__transaction__customer: props.row.id },
|
params: {
|
||||||
// copied from 02-connection.js
|
filter: { prdbk__transaction__customer: props.row.id },
|
||||||
values: 'price_excluding_vat,prdbk__transaction__txncurrent__detail__status__name,locked_until,note,cart,cart__name,cart__code,cart__dealer,cart__dealer__code,cart__dealer__name,direction,type,zone_type,dealer,link,type__name,dealer__code,dealer__name,prdbk,prdbk__transaction__customer,prdbk__transaction,prdbk__transaction__policy__code,prdbk__transaction__sale_price,prdbk__transaction__discount_amount,prdbk__transaction__code,prdbk__transaction__customer__code,prdbk__transaction__customer__phone,prdbk__transaction__customer__fullname,prdbk__transaction__customer__legal_code,id,code,trade_code,land_lot_code,zone_code,zone_type__name,lot_area,building_area,total_built_area,number_of_floors,land_lot_size,origin_price,direction__name,villa_model,product_type,template_name,project,project__name,status,status__code,status__name,status__color,status__sale_status,status__sale_status__color,create_time,prdbk__transaction__amount_received,prdbk__transaction__amount_remain',
|
// copied from 02-connection.js
|
||||||
}
|
values:
|
||||||
}" />
|
'price_excluding_vat,prdbk__transaction__txncurrent__detail__status__name,locked_until,note,cart,cart__name,cart__code,cart__dealer,cart__dealer__code,cart__dealer__name,direction,type,zone_type,dealer,link,type__name,dealer__code,dealer__name,prdbk,prdbk__transaction__customer,prdbk__transaction,prdbk__transaction__policy__code,prdbk__transaction__sale_price,prdbk__transaction__discount_amount,prdbk__transaction__code,prdbk__transaction__customer__code,prdbk__transaction__customer__phone,prdbk__transaction__customer__fullname,prdbk__transaction__customer__legal_code,id,code,trade_code,land_lot_code,zone_code,zone_type__name,lot_area,building_area,total_built_area,number_of_floors,land_lot_size,origin_price,direction__name,villa_model,product_type,template_name,project,project__name,status,status__code,status__name,status__color,status__sale_status,status__sale_status__color,create_time,prdbk__transaction__amount_received,prdbk__transaction__amount_remain',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p v-if="row && row.fullname"><b>{{row.fullname}}</b></p>
|
<p v-if="row && row.fullname">
|
||||||
|
<b>{{ row.fullname }}</b>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 px-5 is-flex is-justify-content-center">
|
<div class="mt-2 px-5 is-flex is-justify-content-center">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<Qrcode
|
<Qrcode
|
||||||
v-if="finalLink"
|
v-if="finalLink"
|
||||||
:key="finalLink"
|
:key="finalLink"
|
||||||
id="qrcode"
|
id="qrcode"
|
||||||
:value="finalLink"
|
:value="finalLink"
|
||||||
:size="300"
|
:size="300"
|
||||||
/>
|
/>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div v-if="!finalLink" style="width: 300px; height: 300px; border: 1px dashed #ccc; display: flex; align-items: center; justify-content: center; color: #888;">
|
<div
|
||||||
|
v-if="!finalLink"
|
||||||
|
style="
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #888;
|
||||||
|
"
|
||||||
|
>
|
||||||
Không có dữ liệu để tạo QR Code
|
Không có dữ liệu để tạo QR Code
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 is-flex is-justify-content-center is-gap-1">
|
<div class="mt-2 is-flex is-justify-content-center is-gap-1">
|
||||||
<a
|
<a
|
||||||
@click="openLink()"
|
@click="openLink()"
|
||||||
class="button is-light is-link is-rounded"
|
class="button is-light is-link is-rounded"
|
||||||
:title="isVietnamese ? 'Mở đường dẫn liên kết' : 'Open external link'"
|
:title="isVietnamese ? 'Mở đường dẫn liên kết' : 'Open external link'"
|
||||||
v-if="finalLink"
|
v-if="finalLink"
|
||||||
@@ -31,8 +44,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@click="download()"
|
@click="download()"
|
||||||
class="button is-light is-link is-rounded"
|
class="button is-light is-link is-rounded"
|
||||||
:title="isVietnamese ? 'Tải Xuống QR Code' : 'Download QR Code'"
|
:title="isVietnamese ? 'Tải Xuống QR Code' : 'Download QR Code'"
|
||||||
v-if="finalLink"
|
v-if="finalLink"
|
||||||
@@ -45,83 +58,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from "vue";
|
||||||
import { useStore } from "@/stores/index";
|
import { useStore } from "@/stores/index";
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
const isVietnamese = computed(() => store.lang === "vi")
|
|
||||||
|
|
||||||
const { $getpath, $snackbar } = useNuxtApp()
|
const store = useStore();
|
||||||
const props = defineProps({
|
const isVietnamese = computed(() => store.lang === "vi");
|
||||||
row: Object,
|
|
||||||
link: String
|
|
||||||
})
|
|
||||||
|
|
||||||
const finalLink = computed(() => {
|
const { $getpath, $snackbar } = useNuxtApp();
|
||||||
if (props.link) {
|
const props = defineProps({
|
||||||
return props.link
|
row: Object,
|
||||||
}
|
link: String,
|
||||||
if (props.row && props.row.code) {
|
});
|
||||||
const path = $getpath()
|
|
||||||
const baseUrl = path ? path.replace('api.', '') : '';
|
|
||||||
return `${baseUrl}loan/${props.row.code}`
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
function openLink() {
|
const finalLink = computed(() => {
|
||||||
if (finalLink.value) {
|
if (props.link) {
|
||||||
window.open(finalLink.value, "_blank")
|
return props.link;
|
||||||
}
|
}
|
||||||
|
if (props.row && props.row.code) {
|
||||||
|
const path = $getpath();
|
||||||
|
const baseUrl = path ? path.replace("api.", "") : "";
|
||||||
|
return `${baseUrl}loan/${props.row.code}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function openLink() {
|
||||||
|
if (finalLink.value) {
|
||||||
|
window.open(finalLink.value, "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
if (!finalLink.value) return;
|
||||||
|
|
||||||
|
let svg = document.getElementById("qrcode");
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
while (!svg && attempts < 5) {
|
||||||
|
await sleep(100);
|
||||||
|
svg = document.getElementById("qrcode");
|
||||||
|
attempts++;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
if (!svg) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
console.error("QR Code SVG element not found after waiting.");
|
||||||
|
$snackbar(isVietnamese.value ? "Không tìm thấy mã QR để tải xuống." : "QR Code not found for download.", {
|
||||||
|
type: "is-danger",
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function download() {
|
const serializer = new XMLSerializer();
|
||||||
if (!finalLink.value) return;
|
const svgData = serializer.serializeToString(svg);
|
||||||
|
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
||||||
|
const url = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
let svg = document.getElementById('qrcode');
|
const image = new Image();
|
||||||
let attempts = 0;
|
image.onload = () => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
while (!svg && attempts < 5) {
|
canvas.width = 300;
|
||||||
await sleep(100);
|
canvas.height = 300;
|
||||||
svg = document.getElementById('qrcode');
|
const ctx = canvas.getContext("2d");
|
||||||
attempts++;
|
ctx.drawImage(image, 0, 0, 300, 300);
|
||||||
}
|
|
||||||
|
|
||||||
if (!svg) {
|
const pngUrl = canvas.toDataURL("image/png");
|
||||||
console.error("QR Code SVG element not found after waiting.");
|
|
||||||
$snackbar(isVietnamese.value ? 'Không tìm thấy mã QR để tải xuống.' : 'QR Code not found for download.', { type: 'is-danger' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializer = new XMLSerializer()
|
const linkElement = document.createElement("a");
|
||||||
const svgData = serializer.serializeToString(svg)
|
linkElement.href = pngUrl;
|
||||||
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
|
|
||||||
const url = URL.createObjectURL(svgBlob)
|
|
||||||
|
|
||||||
const image = new Image()
|
const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : "qrcode.png";
|
||||||
image.onload = () => {
|
linkElement.download = filename;
|
||||||
const canvas = document.createElement('canvas')
|
linkElement.click();
|
||||||
canvas.width = 300
|
|
||||||
canvas.height = 300
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
ctx.drawImage(image, 0, 0, 300, 300)
|
|
||||||
|
|
||||||
const pngUrl = canvas.toDataURL('image/png')
|
URL.revokeObjectURL(url);
|
||||||
|
$snackbar(isVietnamese.value ? "Đã tải xuống mã QR!" : "QR Code downloaded!", { type: "is-success" });
|
||||||
const linkElement = document.createElement('a')
|
};
|
||||||
linkElement.href = pngUrl
|
image.src = url;
|
||||||
|
}
|
||||||
const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : 'qrcode.png'
|
</script>
|
||||||
linkElement.download = filename
|
|
||||||
linkElement.click()
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
$snackbar(isVietnamese.value ? 'Đã tải xuống mã QR!' : 'QR Code downloaded!', { type: 'is-success' });
|
|
||||||
}
|
|
||||||
image.src = url
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<DataView v-if="vbind" v-bind="vbind"></DataView>
|
<DataView
|
||||||
|
v-if="vbind"
|
||||||
|
v-bind="vbind"
|
||||||
|
></DataView>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['vbind']
|
props: ["vbind"],
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,175 +1,279 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="record">
|
<div v-if="record">
|
||||||
<div class="columns is-multiline mx-0">
|
<div class="columns is-multiline mx-0">
|
||||||
<div :class="`column is-3 ${viewport===1? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{findLang('code')}}<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label">{{ findLang("code") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input has-text-black" disabled type="text" placeholder="" v-model="record.code">
|
<input
|
||||||
</div>
|
class="input has-text-black"
|
||||||
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p>
|
disabled
|
||||||
</div>
|
type="text"
|
||||||
</div>
|
placeholder=""
|
||||||
|
v-model="record.code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.code"
|
||||||
|
>
|
||||||
|
{{ errors.code }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div :class="`column is-6 ${viewport===1? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" placeholder="" v-model="record.fullname">
|
<input
|
||||||
</div>
|
class="input"
|
||||||
<p class="help is-danger" v-if="errors.fullname">{{errors.fullname}}</p>
|
type="text"
|
||||||
</div>
|
placeholder=""
|
||||||
|
v-model="record.fullname"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.fullname"
|
||||||
|
>
|
||||||
|
{{ errors.fullname }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ findLang("shortname") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="record.shortname"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.shortname"
|
||||||
|
>
|
||||||
|
{{ errors.shortname }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ findLang("taxcode") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="record.legal_code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.legal_code"
|
||||||
|
>
|
||||||
|
{{ errors.legal_code }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Điện thoại</label>
|
||||||
|
<div class="control">
|
||||||
|
<InputPhone
|
||||||
|
v-bind="{ record: record, attr: 'phone', placeholder: '' }"
|
||||||
|
@phone="selected('phone', $event)"
|
||||||
|
></InputPhone>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.phone"
|
||||||
|
>
|
||||||
|
{{ errors.phone }}
|
||||||
|
<a
|
||||||
|
v-if="existedCustomer"
|
||||||
|
@click="showCustomer()"
|
||||||
|
>Chi tiết</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Email</label>
|
||||||
|
<div class="control">
|
||||||
|
<InputEmail
|
||||||
|
v-bind="{ record: record, attr: 'email', placeholder: '' }"
|
||||||
|
@email="selected('email', $event)"
|
||||||
|
></InputEmail>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.email"
|
||||||
|
>
|
||||||
|
{{ errors.email }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Website</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="record.website"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ findLang("country") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
vdata: store.country,
|
||||||
|
field: 'name',
|
||||||
|
column: ['name'],
|
||||||
|
first: true,
|
||||||
|
optionid: record.country,
|
||||||
|
position: 'is-top-left',
|
||||||
|
}"
|
||||||
|
@option="selected('_country', $event)"
|
||||||
|
></SearchBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ findLang("province") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="record.province"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="`column is-12 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ findLang("address") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="record.address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.address"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-3 ${viewport===1? 'px-0 pb-1' : ''}`">
|
<div class="mt-2">
|
||||||
<div class="field">
|
<button
|
||||||
<label class="label">{{findLang('shortname')}}</label>
|
class="button is-primary has-text-white"
|
||||||
<div class="control">
|
@click="update()"
|
||||||
<input class="input" type="text" placeholder="" v-model="record.shortname">
|
>
|
||||||
</div>
|
{{ findLang("save") }}
|
||||||
<p class="help is-danger" v-if="errors.shortname">{{errors.shortname}}</p>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
<Modal
|
||||||
<div class="field">
|
@close="showmodal = undefined"
|
||||||
<label class="label">{{findLang('taxcode')}}</label>
|
v-bind="showmodal"
|
||||||
<div class="control">
|
v-if="showmodal"
|
||||||
<input class="input" type="text" placeholder="" v-model="record.legal_code">
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.legal_code">{{errors.legal_code}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Điện thoại</label>
|
|
||||||
<div class="control">
|
|
||||||
<InputPhone v-bind="{record: record, attr: 'phone', placeholder: ''}" @phone="selected('phone', $event)"></InputPhone>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors.phone">{{ errors.phone }}
|
|
||||||
<a v-if="existedCustomer" @click="showCustomer()">Chi tiết</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Email</label>
|
|
||||||
<div class="control">
|
|
||||||
<InputEmail v-bind="{record: record, attr: 'email', placeholder: ''}" @email="selected('email', $event)"></InputEmail>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Website</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="text" placeholder="" v-model="record.website">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{findLang('country')}}</label>
|
|
||||||
<div class="control">
|
|
||||||
<SearchBox v-bind="{vdata: store.country, field:'name', column:['name'], first:true, optionid: record.country, position: 'is-top-left'}"
|
|
||||||
@option="selected('_country', $event)"></SearchBox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{findLang('province')}}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="text" placeholder="" v-model="record.province">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="`column is-12 ${viewport===1? 'px-0 pb-1' : ''}`">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">{{findLang('address')}}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="text" placeholder="" v-model="record.address">
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="errors.address"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<button class="button is-primary has-text-white" @click="update()">{{ findLang('save') }}</button>
|
|
||||||
</div>
|
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import InputPhone from '~/components/common/InputPhone'
|
import InputPhone from "~/components/common/InputPhone";
|
||||||
import InputEmail from '~/components/common/InputEmail'
|
import InputEmail from "~/components/common/InputEmail";
|
||||||
import SearchBox from '~/components/SearchBox'
|
import SearchBox from "~/components/SearchBox";
|
||||||
import { useStore } from '@/stores/index'
|
import { useStore } from "@/stores/index";
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
pagename: String,
|
pagename: String,
|
||||||
row: Object
|
row: Object,
|
||||||
})
|
});
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } = useNuxtApp()
|
const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } =
|
||||||
const emit = defineEmits(['update', 'dataevent'])
|
useNuxtApp();
|
||||||
var viewport = store.viewport
|
const emit = defineEmits(["update", "dataevent"]);
|
||||||
var errors = ref({})
|
var viewport = store.viewport;
|
||||||
var record = ref()
|
var errors = ref({});
|
||||||
var showmodal = undefined
|
var record = ref();
|
||||||
var existedCustomer = undefined
|
var showmodal = undefined;
|
||||||
async function initData() {
|
var existedCustomer = undefined;
|
||||||
if(props.row) {
|
async function initData() {
|
||||||
let conn = $findapi('company')
|
if (props.row) {
|
||||||
conn.params.filter = {id: props.row.company || props.row.customer__company || props.row.id}
|
let conn = $findapi("company");
|
||||||
let rs = await $getapi([conn])
|
conn.params.filter = {
|
||||||
let found = $find(rs, {name: 'company'})
|
id: props.row.company || props.row.customer__company || props.row.id,
|
||||||
if(found.data.rows.length>0) record.value = found.data.rows[0]
|
};
|
||||||
} else {
|
let rs = await $getapi([conn]);
|
||||||
record.value = {}
|
let found = $find(rs, { name: "company" });
|
||||||
|
if (found.data.rows.length > 0) record.value = found.data.rows[0];
|
||||||
|
} else {
|
||||||
|
record.value = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function findLang(code) {
|
||||||
|
let found = $find(store.common, { code: code });
|
||||||
|
return found ? found[store.lang] : "";
|
||||||
|
}
|
||||||
|
function showCustomer() {
|
||||||
|
showmodal.value = {
|
||||||
|
component: "customer/CustomerView",
|
||||||
|
width: "60%",
|
||||||
|
height: "600px",
|
||||||
|
title: "Khách hàng",
|
||||||
|
vbind: { row: existedCustomer },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function selected(attr, obj) {
|
||||||
|
record.value[attr] = obj;
|
||||||
|
}
|
||||||
|
function checkError() {
|
||||||
|
existedCustomer = undefined;
|
||||||
|
errors.value = {};
|
||||||
|
if ($empty(record.value.fullname)) errors.value.fullname = "Họ tên không được bỏ trống";
|
||||||
|
if (record.value.phone) {
|
||||||
|
let text = $errPhone(record.value.phone);
|
||||||
|
if (text) errors.value.phone = text;
|
||||||
|
}
|
||||||
|
return Object.keys(errors.value).length > 0;
|
||||||
|
}
|
||||||
|
async function update() {
|
||||||
|
if (checkError()) return;
|
||||||
|
if (!record.value.id) {
|
||||||
|
if (record.value.phone) record.value.phone = record.value.phone.trim();
|
||||||
|
let obj = await $getdata("company", { phone: record.value.phone }, undefined, true);
|
||||||
|
if (obj) {
|
||||||
|
existedCustomer = obj;
|
||||||
|
errors.phone = "Số điện thoại đã tồn tại trong hệ thống.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function findLang(code) {
|
record.value = $resetNull(record.value);
|
||||||
let found = $find(store.common, {code: code})
|
if (record.value._country) record.value.country = record.value._country.id;
|
||||||
return found? found[store.lang] : ''
|
if (!record.value.creator) record.value.creator = store.login.id;
|
||||||
}
|
record.value.updater = store.login.id;
|
||||||
function showCustomer() {
|
record.update_time = new Date();
|
||||||
showmodal.value = {component: 'customer/CustomerView', width: '60%', height: '600px', title: 'Khách hàng', vbind: {row: existedCustomer}}
|
let rs = record.value.id ? await $updateapi("company", record.value) : await $insertapi("company", record.value);
|
||||||
}
|
if (rs === "error") return;
|
||||||
function selected(attr, obj) {
|
if (!record.value.id) $snackbar(`Khách hàng đã được khởi tạo với mã <b>${rs.code}</b>`, "Thành công", "Success");
|
||||||
record.value[attr] = obj
|
record.value.id = rs.id;
|
||||||
}
|
let ele = await $getdata("company", { id: rs.id }, null, true);
|
||||||
function checkError() {
|
emit("update", ele);
|
||||||
existedCustomer = undefined
|
emit("modalevent", { name: "dataevent", data: ele });
|
||||||
errors.value = {}
|
}
|
||||||
if($empty(record.value.fullname)) errors.value.fullname = 'Họ tên không được bỏ trống'
|
initData();
|
||||||
if(record.value.phone) {
|
</script>
|
||||||
let text = $errPhone(record.value.phone)
|
|
||||||
if(text) errors.value.phone = text
|
|
||||||
}
|
|
||||||
return Object.keys(errors.value).length>0
|
|
||||||
}
|
|
||||||
async function update() {
|
|
||||||
if(checkError()) return
|
|
||||||
if(!record.value.id) {
|
|
||||||
if(record.value.phone) record.value.phone = record.value.phone.trim()
|
|
||||||
let obj = await $getdata('company', {phone: record.value.phone}, undefined, true)
|
|
||||||
if(obj) {
|
|
||||||
existedCustomer = obj
|
|
||||||
errors.phone = 'Số điện thoại đã tồn tại trong hệ thống.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
record.value = $resetNull(record.value)
|
|
||||||
if(record.value._country) record.value.country = record.value._country.id
|
|
||||||
if(!record.value.creator) record.value.creator = store.login.id
|
|
||||||
record.value.updater = store.login.id
|
|
||||||
record.update_time = new Date()
|
|
||||||
let rs = record.value.id? await $updateapi('company', record.value)
|
|
||||||
: await $insertapi('company', record.value)
|
|
||||||
if(rs==='error') return
|
|
||||||
if(!record.value.id) $snackbar(`Khách hàng đã được khởi tạo với mã <b>${rs.code}</b>`, 'Thành công', 'Success')
|
|
||||||
record.value.id = rs.id
|
|
||||||
let ele = await $getdata('company', {id:rs.id}, null, true)
|
|
||||||
emit('update', ele)
|
|
||||||
emit('modalevent', {name: 'dataevent', data: ele})
|
|
||||||
}
|
|
||||||
initData()
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -10,14 +10,21 @@
|
|||||||
v-for="(v, i) in tabs"
|
v-for="(v, i) in tabs"
|
||||||
:key="i"
|
:key="i"
|
||||||
:class="['is-clickable p-3', i !== 0 && 'mt-2', getStyle(v)]"
|
:class="['is-clickable p-3', i !== 0 && 'mt-2', getStyle(v)]"
|
||||||
style="width: 130px; border-radius: 4px;"
|
style="width: 130px; border-radius: 4px"
|
||||||
@click="changeTab(v)"
|
@click="changeTab(v)"
|
||||||
>
|
>
|
||||||
{{ isVietnamese ? v.name : v.en }}
|
{{ isVietnamese ? v.name : v.en }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="field is-grouped is-grouped-multiline">
|
<div
|
||||||
<div class="control" v-for="(v, i) in tabs" @click="changeTab(v)">
|
v-else
|
||||||
|
class="field is-grouped is-grouped-multiline"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="control"
|
||||||
|
v-for="(v, i) in tabs"
|
||||||
|
@click="changeTab(v)"
|
||||||
|
>
|
||||||
<div style="width: 130px">
|
<div style="width: 130px">
|
||||||
<div :class="`py-3 px-3 ${getStyle(v)}`">
|
<div :class="`py-3 px-3 ${getStyle(v)}`">
|
||||||
{{ isVietnamese ? v.name : v.en }}
|
{{ isVietnamese ? v.name : v.en }}
|
||||||
@@ -54,7 +61,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal @close="handleModalClose" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="handleModalClose"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
@@ -99,11 +110,7 @@ function getStyle(v) {
|
|||||||
|
|
||||||
function changeTab(v) {
|
function changeTab(v) {
|
||||||
if (tab.value === v.code) return;
|
if (tab.value === v.code) return;
|
||||||
if (!record)
|
if (!record) return $dialog("Vui lòng <b>lưu dữ liệu</b> trước khi chuyển sang mục tiếp theo", "Thông báo");
|
||||||
return $dialog(
|
|
||||||
"Vui lòng <b>lưu dữ liệu</b> trước khi chuyển sang mục tiếp theo",
|
|
||||||
"Thông báo"
|
|
||||||
);
|
|
||||||
tab.value = v.code;
|
tab.value = v.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +122,7 @@ function update(v) {
|
|||||||
record = {
|
record = {
|
||||||
...v,
|
...v,
|
||||||
label: `${v.code} / ${v.fullname} / ${v.phone || ""}`,
|
label: `${v.code} / ${v.fullname} / ${v.phone || ""}`,
|
||||||
}
|
};
|
||||||
emit("modalevent", { name: "dataevent", data: record });
|
emit("modalevent", { name: "dataevent", data: record });
|
||||||
if (!props.isEditMode) emit("close");
|
if (!props.isEditMode) emit("close");
|
||||||
}
|
}
|
||||||
@@ -123,8 +130,7 @@ function update(v) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.title {
|
.title {
|
||||||
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial,
|
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
|
||||||
sans-serif !important;
|
|
||||||
}
|
}
|
||||||
.button.is-large.is-fullwidth:hover {
|
.button.is-large.is-fullwidth:hover {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("custcode")[lang]
|
>{{ dataLang && findFieldName("custcode")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
@@ -15,19 +14,30 @@
|
|||||||
placeholder=""
|
placeholder=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.code"
|
||||||
|
>
|
||||||
|
{{ errors.code }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("name")[lang]
|
>{{ dataLang && findFieldName("name")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" placeholder="" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.fullname">
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.fullname"
|
||||||
|
>
|
||||||
{{ errors.fullname }}
|
{{ errors.fullname }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,8 +45,7 @@
|
|||||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("phone_number")[lang]
|
>{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<InputPhone
|
<InputPhone
|
||||||
@@ -45,7 +54,10 @@
|
|||||||
>
|
>
|
||||||
</InputPhone>
|
</InputPhone>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.phone">
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.phone"
|
||||||
|
>
|
||||||
{{ errors.phone }}
|
{{ errors.phone }}
|
||||||
<a
|
<a
|
||||||
class="has-text-primary"
|
class="has-text-primary"
|
||||||
@@ -66,14 +78,18 @@
|
|||||||
>
|
>
|
||||||
</InputEmail>
|
</InputEmail>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.email"
|
||||||
|
>
|
||||||
|
{{ errors.email }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("gender")[lang]
|
>{{ dataLang && findFieldName("gender")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
@@ -93,8 +109,7 @@
|
|||||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("birth_date")[lang]
|
>{{ dataLang && findFieldName("birth_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<Datepicker
|
<Datepicker
|
||||||
@@ -103,14 +118,18 @@
|
|||||||
>
|
>
|
||||||
</Datepicker>
|
</Datepicker>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.dob">{{ errors.dob }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.dob"
|
||||||
|
>
|
||||||
|
{{ errors.dob }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("country")[lang]
|
>{{ dataLang && findFieldName("country")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
@@ -130,8 +149,7 @@
|
|||||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("province")[lang]
|
>{{ dataLang && findFieldName("province")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
@@ -146,8 +164,7 @@
|
|||||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("district")[lang]
|
>{{ dataLang && findFieldName("district")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
@@ -162,8 +179,7 @@
|
|||||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("address")[lang]
|
>{{ dataLang && findFieldName("address")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
@@ -173,14 +189,15 @@
|
|||||||
v-model="record.address"
|
v-model="record.address"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.address"></p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.address"
|
||||||
|
></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-5 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-5 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{
|
<label class="label">{{ dataLang && findFieldName("company")[lang] }}</label>
|
||||||
dataLang && findFieldName("company")[lang]
|
|
||||||
}}</label>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
v-bind="{
|
v-bind="{
|
||||||
@@ -201,8 +218,7 @@
|
|||||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("personal_id")[lang]
|
>{{ dataLang && findFieldName("personal_id")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
@@ -222,8 +238,7 @@
|
|||||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("idnum")[lang] }}
|
>{{ dataLang && findFieldName("idnum")[lang] }} <b class="ml-1 has-text-danger">*</b></label
|
||||||
<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
@@ -233,7 +248,10 @@
|
|||||||
v-model="record.legal_code"
|
v-model="record.legal_code"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.legal_code">
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.legal_code"
|
||||||
|
>
|
||||||
{{ errors.legal_code }}
|
{{ errors.legal_code }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,8 +259,7 @@
|
|||||||
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("issued_date")[lang] }}
|
>{{ dataLang && findFieldName("issued_date")[lang] }} <b class="ml-1 has-text-danger">*</b></label
|
||||||
<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<Datepicker
|
<Datepicker
|
||||||
@@ -255,7 +272,10 @@
|
|||||||
@date="selected('issued_date', $event)"
|
@date="selected('issued_date', $event)"
|
||||||
></Datepicker>
|
></Datepicker>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.issued_date">
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_date"
|
||||||
|
>
|
||||||
{{ errors.issued_date }}
|
{{ errors.issued_date }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,8 +283,7 @@
|
|||||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"
|
<label class="label"
|
||||||
>{{ dataLang && findFieldName("issued_place")[lang] }}
|
>{{ dataLang && findFieldName("issued_place")[lang] }} <b class="ml-1 has-text-danger">*</b></label
|
||||||
<b class="ml-1 has-text-danger">*</b></label
|
|
||||||
>
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
@@ -274,7 +293,10 @@
|
|||||||
v-model="record.issued_place"
|
v-model="record.issued_place"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.issued_place"></p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_place"
|
||||||
|
></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
@@ -306,9 +328,7 @@
|
|||||||
|
|
||||||
<div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{
|
<label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
|
||||||
dataLang && findFieldName("note")[lang]
|
|
||||||
}}</label>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea"
|
class="textarea"
|
||||||
@@ -317,7 +337,12 @@
|
|||||||
rows="1"
|
rows="1"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.note">{{ errors.note }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.note"
|
||||||
|
>
|
||||||
|
{{ errors.note }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -330,11 +355,15 @@
|
|||||||
}"
|
}"
|
||||||
></Caption>
|
></Caption>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns mb-0 mx-0" v-for="(v, i) in people">
|
<div
|
||||||
|
class="columns mb-0 mx-0"
|
||||||
|
v-for="(v, i) in people"
|
||||||
|
>
|
||||||
<div :class="`column is-7 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-7 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<label class="label" v-if="i === 0"
|
<label
|
||||||
>{{ findFieldName("select")[lang]
|
class="label"
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
v-if="i === 0"
|
||||||
|
>{{ findFieldName("select")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
>
|
>
|
||||||
<SearchBox
|
<SearchBox
|
||||||
v-bind="{
|
v-bind="{
|
||||||
@@ -351,12 +380,18 @@
|
|||||||
@option="selectPeople($event, v, i)"
|
@option="selectPeople($event, v, i)"
|
||||||
>
|
>
|
||||||
</SearchBox>
|
</SearchBox>
|
||||||
<p class="help is-danger" v-if="v.error">{{ v.error }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="v.error"
|
||||||
|
>
|
||||||
|
{{ v.error }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<label class="label" v-if="i === 0"
|
<label
|
||||||
>{{ findFieldName("relationship")[lang]
|
class="label"
|
||||||
}}<b class="ml-1 has-text-danger">*</b></label
|
v-if="i === 0"
|
||||||
|
>{{ findFieldName("relationship")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
>
|
>
|
||||||
<SearchBox
|
<SearchBox
|
||||||
v-bind="{
|
v-bind="{
|
||||||
@@ -374,23 +409,30 @@
|
|||||||
</SearchBox>
|
</SearchBox>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`column ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
<div :class="`column ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||||
<label class="label" v-if="i === 0"
|
<label
|
||||||
|
class="label"
|
||||||
|
v-if="i === 0"
|
||||||
>{{ findFieldName("addmore")[lang] }}...</label
|
>{{ findFieldName("addmore")[lang] }}...</label
|
||||||
>
|
>
|
||||||
<button class="button px-2 is-dark mr-3" @click="add()">
|
<button
|
||||||
<SvgIcon
|
class="button px-2 is-dark mr-3"
|
||||||
v-bind="{ name: 'add1.png', type: 'white', size: 20 }"
|
@click="add()"
|
||||||
></SvgIcon>
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 20 }"></SvgIcon>
|
||||||
</button>
|
</button>
|
||||||
<button class="button px-2 is-warning" @click="remove(v, i)">
|
<button
|
||||||
<SvgIcon
|
class="button px-2 is-warning"
|
||||||
v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"
|
@click="remove(v, i)"
|
||||||
></SvgIcon>
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<button class="button is-primary has-text-white" @click="update()">
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="update()"
|
||||||
|
>
|
||||||
{{ store.lang === "en" ? "Save" : "Lưu lại" }}
|
{{ store.lang === "en" ? "Save" : "Lưu lại" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -410,17 +452,7 @@ import Datepicker from "~/components/datepicker/Datepicker";
|
|||||||
import { useStore } from "@/stores/index";
|
import { useStore } from "@/stores/index";
|
||||||
const emit = defineEmits(["close", "update"]);
|
const emit = defineEmits(["close", "update"]);
|
||||||
const nuxtApp = useNuxtApp();
|
const nuxtApp = useNuxtApp();
|
||||||
const {
|
const { $getdata, $updateapi, $insertapi, $empty, $errPhone, $resetNull, $snackbar, $copy, $dayjs } = nuxtApp;
|
||||||
$getdata,
|
|
||||||
$updateapi,
|
|
||||||
$insertapi,
|
|
||||||
$empty,
|
|
||||||
$errPhone,
|
|
||||||
$resetNull,
|
|
||||||
$snackbar,
|
|
||||||
$copy,
|
|
||||||
$dayjs,
|
|
||||||
} = nuxtApp;
|
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
pagename: String,
|
pagename: String,
|
||||||
row: Object,
|
row: Object,
|
||||||
@@ -540,10 +572,7 @@ function showCustomer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selected = (fieldName, value) => {
|
const selected = (fieldName, value) => {
|
||||||
const finalValue =
|
const finalValue = value !== null && typeof value === "object" ? value.id || value.index : value;
|
||||||
value !== null && typeof value === "object"
|
|
||||||
? value.id || value.index
|
|
||||||
: value;
|
|
||||||
record.value[fieldName] = finalValue;
|
record.value[fieldName] = finalValue;
|
||||||
|
|
||||||
if (errors.value[fieldName]) {
|
if (errors.value[fieldName]) {
|
||||||
@@ -566,9 +595,7 @@ function checkError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($empty(record.value.dob)) {
|
if ($empty(record.value.dob)) {
|
||||||
errors.value.dob = isVietnamese.value
|
errors.value.dob = isVietnamese.value ? "Ngày sinh không được bỏ trống" : "Date of birth cannot be empty";
|
||||||
? "Ngày sinh không được bỏ trống"
|
|
||||||
: "Date of birth cannot be empty";
|
|
||||||
} else {
|
} else {
|
||||||
if (record.value.dob > new Date()) {
|
if (record.value.dob > new Date()) {
|
||||||
errors.value.dob = isVietnamese.value
|
errors.value.dob = isVietnamese.value
|
||||||
@@ -583,21 +610,15 @@ function checkError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($empty(record.value.legal_code)) {
|
if ($empty(record.value.legal_code)) {
|
||||||
errors.value.legal_code = isVietnamese.value
|
errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code cannot be empty";
|
||||||
? "Mã số không được bỏ trống"
|
|
||||||
: "Legal code cannot be empty";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($empty(record.value.issued_date)) {
|
if ($empty(record.value.issued_date)) {
|
||||||
errors.value.issued_date = isVietnamese.value
|
errors.value.issued_date = isVietnamese.value ? "Ngày cấp không được bỏ trống" : "Date of issue cannot be empty";
|
||||||
? "Ngày cấp không được bỏ trống"
|
|
||||||
: "Date of issue cannot be empty";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($empty(record.value.issued_place)) {
|
if ($empty(record.value.issued_place)) {
|
||||||
errors.value.issued_place = isVietnamese.value
|
errors.value.issued_place = isVietnamese.value ? "Nơi cấp không được bỏ trống" : "Place of issue cannot be empty";
|
||||||
? "Nơi cấp không được bỏ trống"
|
|
||||||
: "Place of issue cannot be empty";
|
|
||||||
}
|
}
|
||||||
return Object.keys(errors.value).length > 0;
|
return Object.keys(errors.value).length > 0;
|
||||||
}
|
}
|
||||||
@@ -637,45 +658,26 @@ async function update() {
|
|||||||
try {
|
try {
|
||||||
if (record.value.phone) {
|
if (record.value.phone) {
|
||||||
record.value.phone = record.value.phone.trim();
|
record.value.phone = record.value.phone.trim();
|
||||||
let phoneCheck = await $getdata(
|
let phoneCheck = await $getdata("customer", { phone: record.value.phone }, undefined, true);
|
||||||
"customer",
|
|
||||||
{ phone: record.value.phone },
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (phoneCheck) {
|
if (phoneCheck) {
|
||||||
existedCustomer = phoneCheck;
|
existedCustomer = phoneCheck;
|
||||||
errors.value.phone = isVietnamese.value
|
errors.value.phone = isVietnamese.value ? "Số điện thoại đã tồn tại." : "Phone number already exists.";
|
||||||
? "Số điện thoại đã tồn tại."
|
|
||||||
: "Phone number already exists.";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kiểm tra email
|
// Kiểm tra email
|
||||||
if (record.value.email && record.value.email.trim() !== "") {
|
if (record.value.email && record.value.email.trim() !== "") {
|
||||||
let emailCheck = await $getdata(
|
let emailCheck = await $getdata("customer", { email: record.value.email.trim() }, undefined, true);
|
||||||
"customer",
|
|
||||||
{ email: record.value.email.trim() },
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (emailCheck) {
|
if (emailCheck) {
|
||||||
errors.value.email = isVietnamese.value
|
errors.value.email = isVietnamese.value ? "Email đã tồn tại." : "Email already exists.";
|
||||||
? "Email đã tồn tại."
|
|
||||||
: "Email already exists.";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kiểm tra legal_code
|
// Kiểm tra legal_code
|
||||||
if (record.value.legal_code) {
|
if (record.value.legal_code) {
|
||||||
let legalCheck = await $getdata(
|
let legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true);
|
||||||
"customer",
|
|
||||||
{ legal_code: record.value.legal_code },
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (legalCheck) {
|
if (legalCheck) {
|
||||||
errors.value.legal_code = isVietnamese.value
|
errors.value.legal_code = isVietnamese.value
|
||||||
? "Số CMND/CCCD đã tồn tại."
|
? "Số CMND/CCCD đã tồn tại."
|
||||||
@@ -714,34 +716,25 @@ async function update() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
record.value.id = rs.id;
|
record.value.id = rs.id;
|
||||||
const customerData = await $getdata(
|
const customerData = await $getdata("customer", { id: rs.id }, undefined, true);
|
||||||
"customer",
|
|
||||||
{ id: rs.id },
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isNewCustomer) {
|
if (isNewCustomer) {
|
||||||
// link user=customer
|
// link user=customer
|
||||||
await $getdata("linkusercustomer", undefined, { phone: rs.phone });
|
await $getdata("linkusercustomer", undefined, { phone: rs.phone });
|
||||||
$snackbar(
|
$snackbar(
|
||||||
`${
|
`${
|
||||||
isVietnamese.value
|
isVietnamese.value ? "Khách hàng đã được khởi tạo với mã" : "Customer has been created with code"
|
||||||
? "Khách hàng đã được khởi tạo với mã"
|
|
||||||
: "Customer has been created with code"
|
|
||||||
} <b>${rs.code}</b>`,
|
} <b>${rs.code}</b>`,
|
||||||
"Thành công",
|
"Thành công",
|
||||||
"Success"
|
"Success",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$snackbar(
|
$snackbar(
|
||||||
`${
|
`${
|
||||||
isVietnamese.value
|
isVietnamese.value ? "Khách hàng đã được cập nhật với mã" : "Customer has been updated with code"
|
||||||
? "Khách hàng đã được cập nhật với mã"
|
|
||||||
: "Customer has been updated with code"
|
|
||||||
} <b>${rs.code}</b>`,
|
} <b>${rs.code}</b>`,
|
||||||
"Thành công",
|
"Thành công",
|
||||||
"Success"
|
"Success",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,9 +753,7 @@ async function update() {
|
|||||||
//Xử lý file uploads nếu có
|
//Xử lý file uploads nếu có
|
||||||
if (record.value.image && record.value.image.length > 0) {
|
if (record.value.image && record.value.image.length > 0) {
|
||||||
let arr = [];
|
let arr = [];
|
||||||
record.value.image.map((v) =>
|
record.value.image.map((v) => arr.push({ ref: record.value.id, file: v }));
|
||||||
arr.push({ ref: record.value.id, file: v })
|
|
||||||
);
|
|
||||||
await $insertapi("customerfile", arr, undefined, false);
|
await $insertapi("customerfile", arr, undefined, false);
|
||||||
}
|
}
|
||||||
emit("update", customerData);
|
emit("update", customerData);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!selectedCustomerType && isNewCustomer && !props.customerType" class="p-5">
|
<div
|
||||||
|
v-if="!selectedCustomerType && isNewCustomer && !props.customerType"
|
||||||
|
class="p-5"
|
||||||
|
>
|
||||||
<h3 class="title is-4 mb-5 has-text-centered">
|
<h3 class="title is-4 mb-5 has-text-centered">
|
||||||
{{ isVietnamese ? "Chọn loại khách hàng" : "Select Customer Type" }}
|
{{ isVietnamese ? "Chọn loại khách hàng" : "Select Customer Type" }}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -7,8 +10,8 @@
|
|||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<button
|
<button
|
||||||
:disabled="!$getEditRights('edit', { code: 'individual', category: 'submenu' })"
|
:disabled="!$getEditRights('edit', { code: 'individual', category: 'submenu' })"
|
||||||
class="button is-large is-fullwidth"
|
class="button is-large is-fullwidth"
|
||||||
style="height: 120px"
|
style="height: 120px"
|
||||||
@click="selectCustomerType(1)"
|
@click="selectCustomerType(1)"
|
||||||
>
|
>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
@@ -22,10 +25,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<button
|
<button
|
||||||
:disabled="!$getEditRights('edit', { code: 'org', category: 'submenu' })"
|
:disabled="!$getEditRights('edit', { code: 'org', category: 'submenu' })"
|
||||||
class="button is-large is-fullwidth"
|
class="button is-large is-fullwidth"
|
||||||
style="height: 120px"
|
style="height: 120px"
|
||||||
@click="selectCustomerType(2)"
|
@click="selectCustomerType(2)"
|
||||||
>
|
>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
@@ -46,28 +49,60 @@
|
|||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ isIndividual ? "Họ và tên" : "Tên tổ chức" }}<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label"
|
||||||
|
>{{ isIndividual ? "Họ và tên" : "Tên tổ chức" }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
|
>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" v-model="record.fullname" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="record.fullname"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.fullname">{{ errors.fullname }}</p>
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.fullname"
|
||||||
|
>
|
||||||
|
{{ errors.fullname }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label"
|
||||||
<InputPhone v-bind="{ record: record, attr: 'phone' }" @phone="selected('phone', $event)"></InputPhone>
|
>{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
<p class="help is-danger" v-if="errors.phone">
|
>
|
||||||
|
<InputPhone
|
||||||
|
v-bind="{ record: record, attr: 'phone' }"
|
||||||
|
@phone="selected('phone', $event)"
|
||||||
|
></InputPhone>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.phone"
|
||||||
|
>
|
||||||
{{ errors.phone }}
|
{{ errors.phone }}
|
||||||
<a class="has-text-primary" v-if="existedCustomer" @click="showCustomer()">Chi tiết</a>
|
<a
|
||||||
|
class="has-text-primary"
|
||||||
|
v-if="existedCustomer"
|
||||||
|
@click="showCustomer()"
|
||||||
|
>Chi tiết</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Email</label>
|
<label class="label">Email</label>
|
||||||
<InputEmail v-bind="{ record: record, attr: 'email' }" @email="selected('email', $event)"></InputEmail>
|
<InputEmail
|
||||||
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
|
v-bind="{ record: record, attr: 'email' }"
|
||||||
|
@email="selected('email', $event)"
|
||||||
|
></InputEmail>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.email"
|
||||||
|
>
|
||||||
|
{{ errors.email }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,28 +110,47 @@
|
|||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Địa chỉ liên hệ</label>
|
<label class="label">Địa chỉ liên hệ</label>
|
||||||
<input class="input" type="text" v-model="record.contact_address" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="record.contact_address"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ isIndividual ? 'Địa chỉ thường trú' : 'Địa chỉ đăng ký' }}</label>
|
<label class="label">{{ isIndividual ? "Địa chỉ thường trú" : "Địa chỉ đăng ký" }}</label>
|
||||||
<input class="input" type="text" v-model="record.address" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="record.address"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isOrganization" class="columns is-multiline">
|
<div
|
||||||
|
v-if="isOrganization"
|
||||||
|
class="columns is-multiline"
|
||||||
|
>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Tài khoản ngân hàng</label>
|
<label class="label">Tài khoản ngân hàng</label>
|
||||||
<input class="input" type="text" v-model="organizationData.bank_account" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="organizationData.bank_account"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Tên ngân hàng</label>
|
<label class="label">Tên ngân hàng</label>
|
||||||
<input class="input" type="text" v-model="organizationData.bank_name" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="organizationData.bank_name"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,46 +158,117 @@
|
|||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ isIndividual ? 'Giấy tờ tùy thân' : 'Giấy tờ' }}<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label"
|
||||||
<SearchBox v-bind="{ vdata: filteredLegalTypes, api: 'legaltype', field: isVietnamese ? 'name' : 'en', column: ['name', 'en'], first: true, optionid: record.legal_type }" @option="selected('legal_type', $event)"></SearchBox>
|
>{{ isIndividual ? "Giấy tờ tùy thân" : "Giấy tờ" }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
|
>
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
vdata: filteredLegalTypes,
|
||||||
|
api: 'legaltype',
|
||||||
|
field: isVietnamese ? 'name' : 'en',
|
||||||
|
column: ['name', 'en'],
|
||||||
|
first: true,
|
||||||
|
optionid: record.legal_type,
|
||||||
|
}"
|
||||||
|
@option="selected('legal_type', $event)"
|
||||||
|
></SearchBox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label"
|
||||||
<input class="input" type="text" v-model="record.legal_code" />
|
>{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
<p class="help is-danger" v-if="errors.legal_code">{{ errors.legal_code }}</p>
|
>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="record.legal_code"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.legal_code"
|
||||||
|
>
|
||||||
|
{{ errors.legal_code }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
|
<label class="label"
|
||||||
<Datepicker v-bind="{ record: record, attr: 'issued_date', maxdate: new Date() }" @date="selected('issued_date', $event)"></Datepicker>
|
>{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
>
|
||||||
|
<Datepicker
|
||||||
|
v-bind="{
|
||||||
|
record: record,
|
||||||
|
attr: 'issued_date',
|
||||||
|
maxdate: new Date(),
|
||||||
|
}"
|
||||||
|
@date="selected('issued_date', $event)"
|
||||||
|
></Datepicker>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.issued_date"
|
||||||
|
>
|
||||||
|
{{ errors.issued_date }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("issued_place")[lang] }}</label>
|
<label class="label">{{ dataLang && findFieldName("issued_place")[lang] }}</label>
|
||||||
<SearchBox
|
<SearchBox
|
||||||
v-bind="{ api: 'issuedplace', field: 'name', column: ['name'], first: true, position: 'is-bottom-right', optionid: record.issued_place,filter: {id__in: isIndividual ? [2, 3] : [4, 5] } }"
|
v-bind="{
|
||||||
@option="selected('issued_place', $event)"></SearchBox>
|
api: 'issuedplace',
|
||||||
|
field: 'name',
|
||||||
|
column: ['name'],
|
||||||
|
first: true,
|
||||||
|
position: 'is-bottom-right',
|
||||||
|
optionid: record.issued_place,
|
||||||
|
filter: { id__in: isIndividual ? [2, 3] : [4, 5] },
|
||||||
|
}"
|
||||||
|
@option="selected('issued_place', $event)"
|
||||||
|
></SearchBox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns is-multiline" v-if="isIndividual">
|
<div
|
||||||
|
class="columns is-multiline"
|
||||||
|
v-if="isIndividual"
|
||||||
|
>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("gender")[lang] }}</label>
|
<label class="label">{{ dataLang && findFieldName("gender")[lang] }}</label>
|
||||||
<SearchBox v-bind="{ vdata: store.sex, api: 'sex', field: isVietnamese ? 'name' : 'en', column: ['name', 'en'], first: true, optionid: individualData.sex }" @option="selectedIndividual('sex', $event)"></SearchBox>
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
vdata: store.sex,
|
||||||
|
api: 'sex',
|
||||||
|
field: isVietnamese ? 'name' : 'en',
|
||||||
|
column: ['name', 'en'],
|
||||||
|
first: true,
|
||||||
|
optionid: individualData.sex,
|
||||||
|
}"
|
||||||
|
@option="selectedIndividual('sex', $event)"
|
||||||
|
></SearchBox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("birth_date")[lang] }}</label>
|
<label class="label">{{ dataLang && findFieldName("birth_date")[lang] }}</label>
|
||||||
<Datepicker v-bind="{ record: individualData, attr: 'dob', maxdate: new Date() }" @date="selectedIndividual('dob', $event)"></Datepicker>
|
<Datepicker
|
||||||
<p class="help is-danger" v-if="errors.dob">{{ errors.dob }}</p>
|
v-bind="{
|
||||||
|
record: individualData,
|
||||||
|
attr: 'dob',
|
||||||
|
maxdate: new Date(),
|
||||||
|
}"
|
||||||
|
@date="selectedIndividual('dob', $event)"
|
||||||
|
></Datepicker>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.dob"
|
||||||
|
>
|
||||||
|
{{ errors.dob }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,19 +277,33 @@
|
|||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
|
<label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
|
||||||
<textarea class="textarea" v-model="record.note" rows="2"></textarea>
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
v-model="record.note"
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 mb-4">
|
<div class="mt-5 mb-4">
|
||||||
<h4 class="title is-6 has-text-warning">{{ isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật' }}</h4>
|
<h4 class="title is-6 has-text-warning">
|
||||||
|
{{ isIndividual ? "Người liên quan" : "Người đại diện pháp luật" }}
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns is-multiline mb-0 is-2" v-for="(v, i) in localPeople" :key="i">
|
<div
|
||||||
|
class="columns is-multiline mb-0 is-2"
|
||||||
|
v-for="(v, i) in localPeople"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<label class="label" v-if="i === 0">{{ findFieldName("select")[lang] }}</label>
|
<label
|
||||||
<SearchBox
|
class="label"
|
||||||
v-bind="{
|
v-if="i === 0"
|
||||||
|
>{{ findFieldName("select")[lang] }}</label
|
||||||
|
>
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
api: 'people',
|
api: 'people',
|
||||||
field: 'label',
|
field: 'label',
|
||||||
column: ['code', 'fullname', 'phone'],
|
column: ['code', 'fullname', 'phone'],
|
||||||
@@ -172,34 +311,54 @@
|
|||||||
optionid: v.people,
|
optionid: v.people,
|
||||||
position: 'is-top-left',
|
position: 'is-top-left',
|
||||||
addon: peopleAddon,
|
addon: peopleAddon,
|
||||||
viewaddon: peopleviewAddon
|
viewaddon: peopleviewAddon,
|
||||||
}"
|
}"
|
||||||
@option="selectPeople($event, v, i)"></SearchBox>
|
@option="selectPeople($event, v, i)"
|
||||||
|
></SearchBox>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<label class="label" v-if="i === 0">{{ isIndividual ? 'Quan hệ' : 'Chức vụ' }}</label>
|
<label
|
||||||
<SearchBox
|
class="label"
|
||||||
v-bind="{
|
v-if="i === 0"
|
||||||
|
>{{ isIndividual ? "Quan hệ" : "Chức vụ" }}</label
|
||||||
|
>
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
api: 'relation',
|
api: 'relation',
|
||||||
field: store.lang === 'en' ? 'en' : 'name',
|
field: store.lang === 'en' ? 'en' : 'name',
|
||||||
column: ['code', 'name', 'en'],
|
column: ['code', 'name', 'en'],
|
||||||
first: true,
|
first: true,
|
||||||
optionid: v.relation,
|
optionid: v.relation,
|
||||||
position: 'is-top-left',
|
position: 'is-top-left',
|
||||||
filter:{ id__in: isIndividual ? [1,2,3,4,5,6,7,8] : [9,10,11,12] }
|
filter: {
|
||||||
}"
|
id__in: isIndividual ? [1, 2, 3, 4, 5, 6, 7, 8] : [9, 10, 11, 12],
|
||||||
|
},
|
||||||
|
}"
|
||||||
@option="selectRelation($event, v, i)"
|
@option="selectRelation($event, v, i)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<label class="label" v-if="i === 0"> </label>
|
<label
|
||||||
<div class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small" style="height: 40px">
|
class="label"
|
||||||
<button class="button is-dark" @click="add()">
|
v-if="i === 0"
|
||||||
|
> </label
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small"
|
||||||
|
style="height: 40px"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-dark"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button is-dark" @click="remove(v, i)">
|
<button
|
||||||
|
class="button is-dark"
|
||||||
|
@click="remove(v, i)"
|
||||||
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
@@ -209,11 +368,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 buttons is-right">
|
<div class="mt-5 buttons is-right">
|
||||||
<button class="button" @click="emit('close')">{{ isVietnamese ? 'Hủy' : 'Cancel' }}</button>
|
<button
|
||||||
<button class="button is-primary" @click="update()">{{ isVietnamese ? 'Lưu lại' : 'Save' }}</button>
|
class="button"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
{{ isVietnamese ? "Hủy" : "Cancel" }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-primary"
|
||||||
|
@click="update()"
|
||||||
|
>
|
||||||
|
{{ isVietnamese ? "Lưu lại" : "Save" }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -226,7 +399,7 @@ import InputEmail from "~/components/common/InputEmail";
|
|||||||
import SearchBox from "~/components/SearchBox";
|
import SearchBox from "~/components/SearchBox";
|
||||||
import Datepicker from "~/components/datepicker/Datepicker";
|
import Datepicker from "~/components/datepicker/Datepicker";
|
||||||
import { useStore } from "~/stores/index";
|
import { useStore } from "~/stores/index";
|
||||||
import { isEqual, pick } from 'es-toolkit';
|
import { isEqual, pick } from "es-toolkit";
|
||||||
|
|
||||||
const emit = defineEmits(["close", "update", "modalevent"]);
|
const emit = defineEmits(["close", "update", "modalevent"]);
|
||||||
const { $getdata, $patchapi, $insertapi, $deleteapi, $empty, $errPhone, $resetNull, $snackbar, $copy } = useNuxtApp();
|
const { $getdata, $patchapi, $insertapi, $deleteapi, $empty, $errPhone, $resetNull, $snackbar, $copy } = useNuxtApp();
|
||||||
@@ -260,21 +433,32 @@ const isOrganization = computed(() => selectedCustomerType.value === 2);
|
|||||||
|
|
||||||
const filteredLegalTypes = computed(() => {
|
const filteredLegalTypes = computed(() => {
|
||||||
if (!store.legaltype) return [];
|
if (!store.legaltype) return [];
|
||||||
return isOrganization.value ? store.legaltype.filter(lt => lt.id === 4) : store.legaltype.filter(lt => lt.id !== 4);
|
return isOrganization.value
|
||||||
|
? store.legaltype.filter((lt) => lt.id === 4)
|
||||||
|
: store.legaltype.filter((lt) => lt.id !== 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
const peopleAddon = {
|
const peopleAddon = {
|
||||||
component: "people/People",
|
component: "people/People",
|
||||||
width: "65%",
|
width: "65%",
|
||||||
height: "500px",
|
height: "500px",
|
||||||
title: store.lang === "en" ? "Related person" : "Người liên quan"
|
title: store.lang === "en" ? "Related person" : "Người liên quan",
|
||||||
};
|
};
|
||||||
const peopleviewAddon = { ...peopleAddon };
|
const peopleviewAddon = { ...peopleAddon };
|
||||||
|
|
||||||
function selectCustomerType(type) {
|
function selectCustomerType(type) {
|
||||||
const typeNum = Number(type);
|
const typeNum = Number(type);
|
||||||
selectedCustomerType.value = typeNum;
|
selectedCustomerType.value = typeNum;
|
||||||
record.value = { fullname: "", phone: "", email: "", country: 1, type: typeNum, legal_type: typeNum === 2 ? 4 : null, creator: store.login.id, updater: store.login.id };
|
record.value = {
|
||||||
|
fullname: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
country: 1,
|
||||||
|
type: typeNum,
|
||||||
|
legal_type: typeNum === 2 ? 4 : null,
|
||||||
|
creator: store.login.id,
|
||||||
|
updater: store.login.id,
|
||||||
|
};
|
||||||
if (typeNum === 1) {
|
if (typeNum === 1) {
|
||||||
individualData.value = { dob: null, sex: 1 };
|
individualData.value = { dob: null, sex: 1 };
|
||||||
organizationData.value = {};
|
organizationData.value = {};
|
||||||
@@ -287,40 +471,61 @@ function selectCustomerType(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findFieldName(code) {
|
function findFieldName(code) {
|
||||||
return dataLang.value.find(item => item.code === code) || { vi: code, en: code };
|
return dataLang.value.find((item) => item.code === code) || { vi: code, en: code };
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCustomer() {
|
function showCustomer() {
|
||||||
showmodal.value = { component: "customer/CustomerView", width: "60%", height: "600px", title: "Khách hàng", vbind: { row: existedCustomer.value } };
|
showmodal.value = {
|
||||||
|
component: "customer/CustomerView",
|
||||||
|
width: "60%",
|
||||||
|
height: "600px",
|
||||||
|
title: "Khách hàng",
|
||||||
|
vbind: { row: existedCustomer.value },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = (f, v) => { record.value[f] = (v && typeof v === 'object') ? v.id : v; if (errors.value[f]) delete errors.value[f]; };
|
const selected = (f, v) => {
|
||||||
const selectedIndividual = (f, v) => { individualData.value[f] = (v && typeof v === 'object') ? v.id : v; };
|
record.value[f] = v && typeof v === "object" ? v.id : v;
|
||||||
const selectedOrg = (f, v) => { organizationData.value[f] = (v && typeof v === 'object') ? v.id : v; };
|
if (errors.value[f]) delete errors.value[f];
|
||||||
const selectPeople = (opt, _v, i) => {
|
};
|
||||||
localPeople.value[i].people = opt.id;
|
const selectedIndividual = (f, v) => {
|
||||||
|
individualData.value[f] = v && typeof v === "object" ? v.id : v;
|
||||||
|
};
|
||||||
|
const selectedOrg = (f, v) => {
|
||||||
|
organizationData.value[f] = v && typeof v === "object" ? v.id : v;
|
||||||
|
};
|
||||||
|
const selectPeople = (opt, _v, i) => {
|
||||||
|
localPeople.value[i].people = opt.id;
|
||||||
|
};
|
||||||
|
const selectRelation = (opt, _v, i) => {
|
||||||
|
localPeople.value[i].relation = opt ? opt.id : null;
|
||||||
};
|
};
|
||||||
const selectRelation = (opt, _v, i) => { localPeople.value[i].relation = opt ? opt.id : null; };
|
|
||||||
const add = () => localPeople.value.push({});
|
const add = () => localPeople.value.push({});
|
||||||
const remove = (_v, i) => {
|
const remove = (_v, i) => {
|
||||||
localPeople.value.splice(i, 1);
|
localPeople.value.splice(i, 1);
|
||||||
if (localPeople.value.length === 0) localPeople.value = [{}];
|
if (localPeople.value.length === 0) localPeople.value = [{}];
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(people, (val) => {
|
watch(
|
||||||
localPeople.value = val.map(cp => pick(cp, ['id', 'people', 'relation']));
|
people,
|
||||||
}, { deep: true })
|
(val) => {
|
||||||
|
localPeople.value = val.map((cp) => pick(cp, ["id", "people", "relation"]));
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
function checkError() {
|
function checkError() {
|
||||||
errors.value = {};
|
errors.value = {};
|
||||||
if ($empty(record.value.fullname)) errors.value.fullname = isVietnamese.value ? "Họ tên không được bỏ trống" : "Full name is required";
|
if ($empty(record.value.fullname))
|
||||||
|
errors.value.fullname = isVietnamese.value ? "Họ tên không được bỏ trống" : "Full name is required";
|
||||||
if ($empty(record.value.phone)) {
|
if ($empty(record.value.phone)) {
|
||||||
errors.value.phone = isVietnamese.value ? "Số điện thoại không được bỏ trống" : "Phone is required";
|
errors.value.phone = isVietnamese.value ? "Số điện thoại không được bỏ trống" : "Phone is required";
|
||||||
} else {
|
} else {
|
||||||
const text = $errPhone(record.value.phone);
|
const text = $errPhone(record.value.phone);
|
||||||
if (text) errors.value.phone = text;
|
if (text) errors.value.phone = text;
|
||||||
}
|
}
|
||||||
if ($empty(record.value.legal_code)) errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code is required";
|
if ($empty(record.value.legal_code))
|
||||||
|
errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code is required";
|
||||||
if ($empty(record.value.issued_date)) errors.value.issued_date = "Ngày cấp không được bỏ trống";
|
if ($empty(record.value.issued_date)) errors.value.issued_date = "Ngày cấp không được bỏ trống";
|
||||||
|
|
||||||
return Object.keys(errors.value).length > 0;
|
return Object.keys(errors.value).length > 0;
|
||||||
@@ -349,7 +554,10 @@ async function update() {
|
|||||||
}
|
}
|
||||||
if (record.value.legal_code) {
|
if (record.value.legal_code) {
|
||||||
const legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true);
|
const legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true);
|
||||||
if (legalCheck) { errors.value.legal_code = "Số CMND/CCCD đã tồn tại."; return; }
|
if (legalCheck) {
|
||||||
|
errors.value.legal_code = "Số CMND/CCCD đã tồn tại.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +566,9 @@ async function update() {
|
|||||||
customerData.updater = store.login.id;
|
customerData.updater = store.login.id;
|
||||||
customerData.update_time = new Date();
|
customerData.update_time = new Date();
|
||||||
|
|
||||||
let res = isNewCustomer.value ? await $insertapi("customer", customerData, undefined, false) : await $patchapi("customer", customerData, undefined, false);
|
let res = isNewCustomer.value
|
||||||
|
? await $insertapi("customer", customerData, undefined, false)
|
||||||
|
: await $patchapi("customer", customerData, undefined, false);
|
||||||
if (!res || res === "error") return;
|
if (!res || res === "error") return;
|
||||||
|
|
||||||
const customerId = res.id;
|
const customerId = res.id;
|
||||||
@@ -367,7 +577,8 @@ async function update() {
|
|||||||
if (isIndividual.value) {
|
if (isIndividual.value) {
|
||||||
let indPayload = $resetNull({ ...individualData.value });
|
let indPayload = $resetNull({ ...individualData.value });
|
||||||
indPayload.customer = customerId;
|
indPayload.customer = customerId;
|
||||||
if (individualData.value.id) await $patchapi("individual", { ...indPayload, id: individualData.value.id }, undefined, false);
|
if (individualData.value.id)
|
||||||
|
await $patchapi("individual", { ...indPayload, id: individualData.value.id }, undefined, false);
|
||||||
else await $insertapi("individual", indPayload, undefined, false);
|
else await $insertapi("individual", indPayload, undefined, false);
|
||||||
} else if (isOrganization.value) {
|
} else if (isOrganization.value) {
|
||||||
let orgPayload = $resetNull({ ...organizationData.value });
|
let orgPayload = $resetNull({ ...organizationData.value });
|
||||||
@@ -393,22 +604,22 @@ async function update() {
|
|||||||
commonPayload = { organization: organizationId };
|
commonPayload = { organization: organizationId };
|
||||||
}
|
}
|
||||||
|
|
||||||
const validLocalPeople = localPeople.value.filter(lp => lp.people && lp.relation).map(lp => toRaw(lp));
|
const validLocalPeople = localPeople.value.filter((lp) => lp.people && lp.relation).map((lp) => toRaw(lp));
|
||||||
const peopleKeys = people.value.map(p => pick(p, ['id', 'people', 'relation']));
|
const peopleKeys = people.value.map((p) => pick(p, ["id", "people", "relation"]));
|
||||||
|
|
||||||
// 1. check existing ids, if people or relation changes -> patch
|
// 1. check existing ids, if people or relation changes -> patch
|
||||||
const existingLocalPeople = validLocalPeople.filter(cp => Boolean(cp.id));
|
const existingLocalPeople = validLocalPeople.filter((cp) => Boolean(cp.id));
|
||||||
existingLocalPeople.forEach(lp => {
|
existingLocalPeople.forEach((lp) => {
|
||||||
const match = peopleKeys.find(p => isEqual(p, lp));
|
const match = peopleKeys.find((p) => isEqual(p, lp));
|
||||||
const payload = { ...lp, ...commonPayload }
|
const payload = { ...lp, ...commonPayload };
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
$patchapi(apiName, payload);
|
$patchapi(apiName, payload);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. if localPeople has and people doesn't -> insert
|
// 2. if localPeople has and people doesn't -> insert
|
||||||
validLocalPeople.forEach(lp => {
|
validLocalPeople.forEach((lp) => {
|
||||||
if (!lp.id) {
|
if (!lp.id) {
|
||||||
const payload = { ...lp, ...commonPayload };
|
const payload = { ...lp, ...commonPayload };
|
||||||
$insertapi(apiName, payload);
|
$insertapi(apiName, payload);
|
||||||
@@ -417,8 +628,8 @@ async function update() {
|
|||||||
|
|
||||||
// 3. if people has and localPeople doesn't -> delete
|
// 3. if people has and localPeople doesn't -> delete
|
||||||
if (peopleKeys.length !== 0 && validLocalPeople.length !== 0) {
|
if (peopleKeys.length !== 0 && validLocalPeople.length !== 0) {
|
||||||
peopleKeys.forEach(cp => {
|
peopleKeys.forEach((cp) => {
|
||||||
const match = validLocalPeople.find(lp => cp.id === lp.id);
|
const match = validLocalPeople.find((lp) => cp.id === lp.id);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
$deleteapi(apiName, cp.id);
|
$deleteapi(apiName, cp.id);
|
||||||
}
|
}
|
||||||
@@ -427,17 +638,24 @@ async function update() {
|
|||||||
|
|
||||||
// Ảnh
|
// Ảnh
|
||||||
if (record.value.image && record.value.image.length > 0) {
|
if (record.value.image && record.value.image.length > 0) {
|
||||||
await $insertapi("customerfile", record.value.image.map(v => ({ ref: customerId, file: v })), undefined, false);
|
await $insertapi(
|
||||||
|
"customerfile",
|
||||||
|
record.value.image.map((v) => ({ ref: customerId, file: v })),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const completeData = await $getdata("customer", { id: customerId }, undefined, true);
|
const completeData = await $getdata("customer", { id: customerId }, undefined, true);
|
||||||
|
|
||||||
$snackbar(`Khách hàng đã được ${isNewCustomer.value ? "khởi tạo" : "cập nhật"} thành công`, "Thành công");
|
$snackbar(`Khách hàng đã được ${isNewCustomer.value ? "khởi tạo" : "cập nhật"} thành công`, "Thành công");
|
||||||
|
|
||||||
emit("modalevent", { name: "dataevent", data: completeData });
|
emit("modalevent", { name: "dataevent", data: completeData });
|
||||||
emit("update", completeData);
|
emit("update", completeData);
|
||||||
setTimeout(() => emit("close"), 100);
|
setTimeout(() => emit("close"), 100);
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initData() {
|
async function initData() {
|
||||||
@@ -465,7 +683,12 @@ async function initData() {
|
|||||||
const copyData = $copy(props.application);
|
const copyData = $copy(props.application);
|
||||||
const type = props.customerType || copyData.type || 1;
|
const type = props.customerType || copyData.type || 1;
|
||||||
selectCustomerType(type);
|
selectCustomerType(type);
|
||||||
record.value = { ...record.value, ...copyData, id: undefined, code: undefined };
|
record.value = {
|
||||||
|
...record.value,
|
||||||
|
...copyData,
|
||||||
|
id: undefined,
|
||||||
|
code: undefined,
|
||||||
|
};
|
||||||
individualData.value = { ...individualData.value, ...copyData };
|
individualData.value = { ...individualData.value, ...copyData };
|
||||||
} else if (props.customerType) {
|
} else if (props.customerType) {
|
||||||
selectCustomerType(props.customerType);
|
selectCustomerType(props.customerType);
|
||||||
@@ -473,4 +696,4 @@ async function initData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => initData());
|
onMounted(() => initData());
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -28,9 +28,7 @@
|
|||||||
>
|
>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
<div class="">
|
<div class="">
|
||||||
<SvgIcon
|
<SvgIcon v-bind="{ name: 'building.svg', type: 'black', size: 40 }"></SvgIcon>
|
||||||
v-bind="{ name: 'building.svg', type: 'black', size: 40 }"
|
|
||||||
></SvgIcon>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="title is-6 mb-0">
|
<div class="title is-6 mb-0">
|
||||||
{{ isVietnamese ? "Doanh nghiệp" : "Company" }}
|
{{ isVietnamese ? "Doanh nghiệp" : "Company" }}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="docid" v-if="record">
|
<div
|
||||||
|
:id="docid"
|
||||||
|
v-if="record"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Caption v-bind="{ title: this.data && findFieldName('info')[this.lang] }"></Caption>
|
<Caption v-bind="{ title: this.data && findFieldName('info')[this.lang] }"></Caption>
|
||||||
<div class="columns is-multiline mx-0">
|
<div class="columns is-multiline mx-0">
|
||||||
@@ -7,7 +10,11 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Mã khách hàng</label>
|
<label class="label">Mã khách hàng</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<span class="hyperlink" @click="$copyToClipboard(record.code)">{{ record.code }}</span>
|
<span
|
||||||
|
class="hyperlink"
|
||||||
|
@click="$copyToClipboard(record.code)"
|
||||||
|
>{{ record.code }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,14 +30,21 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Điện thoại</label>
|
<label class="label">Điện thoại</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<span class="hyperlink" @click="openPhone()">{{ record.phone }}</span>
|
<span
|
||||||
|
class="hyperlink"
|
||||||
|
@click="openPhone()"
|
||||||
|
>{{ record.phone }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3 pb-1 px-0">
|
<div class="column is-3 pb-1 px-0">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Email</label>
|
<label class="label">Email</label>
|
||||||
<div class="control" style="word-break: break-all">
|
<div
|
||||||
|
class="control"
|
||||||
|
style="word-break: break-all"
|
||||||
|
>
|
||||||
{{ record.email || "/" }}
|
{{ record.email || "/" }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +117,11 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Người tạo</label>
|
<label class="label">Người tạo</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<span class="hyperlink" @click="openUser(record.creator)">{{ record.creator__fullname || "/" }}</span>
|
<span
|
||||||
|
class="hyperlink"
|
||||||
|
@click="openUser(record.creator)"
|
||||||
|
>{{ record.creator__fullname || "/" }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +140,11 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Người cập nhật</label>
|
<label class="label">Người cập nhật</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<span class="hyperlink" @click="openUser(record.updater)">{{ record.updater__fullname || "/" }}</span>
|
<span
|
||||||
|
class="hyperlink"
|
||||||
|
@click="openUser(record.updater)"
|
||||||
|
>{{ record.updater__fullname || "/" }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +153,7 @@
|
|||||||
<label class="label">Thời gian cập nhật</label>
|
<label class="label">Thời gian cập nhật</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<span>
|
<span>
|
||||||
{{ record.update_time ? $dayjs(record.update_time).format("DD/MM/YYYY HH:mm") : '/' }}
|
{{ record.update_time ? $dayjs(record.update_time).format("DD/MM/YYYY HH:mm") : "/" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,58 +165,137 @@
|
|||||||
<ImageGallery v-bind="{ row: record, api: 'customerfile', hideopt: true }"></ImageGallery>
|
<ImageGallery v-bind="{ row: record, api: 'customerfile', hideopt: true }"></ImageGallery>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<Caption v-bind="{ title: this.isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật' }"></Caption>
|
<Caption
|
||||||
|
v-bind="{
|
||||||
|
title: this.isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật',
|
||||||
|
}"
|
||||||
|
></Caption>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div
|
<div
|
||||||
v-if="this.relatedPeople && this.relatedPeople.length > 0"
|
v-if="this.relatedPeople && this.relatedPeople.length > 0"
|
||||||
v-for="relatedPerson in this.relatedPeople"
|
v-for="relatedPerson in this.relatedPeople"
|
||||||
class="columns is-0 mb-2"
|
class="columns is-0 mb-2"
|
||||||
>
|
>
|
||||||
<span class="column is-2">{{ relatedPerson.people__code }}</span>
|
<span class="column is-2">{{ relatedPerson.people__code }}</span>
|
||||||
<span class="column is-4 ">
|
<span class="column is-4">
|
||||||
<span class="has-text-primary hyperlink"
|
<span
|
||||||
|
class="has-text-primary hyperlink"
|
||||||
@click="openRelatedPerson(relatedPerson.people)"
|
@click="openRelatedPerson(relatedPerson.people)"
|
||||||
>{{ relatedPerson.people__fullname }}</span>
|
>{{ relatedPerson.people__fullname }}</span
|
||||||
|
>
|
||||||
<span> ({{ relatedPerson.relation__name }})</span>
|
<span> ({{ relatedPerson.relation__name }})</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="column is-4">{{ relatedPerson.people__phone }}</span>
|
<span class="column is-4">{{ relatedPerson.people__phone }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="has-text-grey">
|
<div
|
||||||
Chưa có {{ this.isIndividual ? 'người liên quan' : 'người đại diện pháp luật' }}
|
v-else
|
||||||
|
class="has-text-grey"
|
||||||
|
>
|
||||||
|
Chưa có
|
||||||
|
{{ this.isIndividual ? "người liên quan" : "người đại diện pháp luật" }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="record.count_product >0" class="mt-3">
|
<div
|
||||||
<Caption class="mb-2" v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"></Caption>
|
v-if="record.count_product > 0"
|
||||||
<DataView v-bind="{
|
class="mt-3"
|
||||||
setting: 'customer-all-transaction',
|
>
|
||||||
pagename: this.$id(),
|
<Caption
|
||||||
api: 'customer',
|
class="mb-2"
|
||||||
params: {
|
v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"
|
||||||
filter: { id: this.row.customer || this.row.id },
|
></Caption>
|
||||||
/* copied from 02-connection.js */
|
<DataView
|
||||||
values:
|
v-bind="{
|
||||||
'id,update_time,creator,creator__fullname,country,country__name,country__en,issued_date,issued_place,issued_place__name,code,email,fullname,legal_code,phone,legal_type,legal_type__name,address,contact_address,note,type,type__name,updater,updater__fullname,create_time,update_time',
|
setting: 'customer-all-transaction',
|
||||||
distinct_values: {
|
pagename: this.$id(),
|
||||||
label: { type: 'Concat', field: ['code', 'fullname', 'phone', 'legal_code'] },
|
api: 'customer',
|
||||||
order: { type: 'RowNumber' },
|
params: {
|
||||||
image_count: { type: 'Count', field: 'id', subquery: { model: 'Customer_File', column: 'ref' } },
|
filter: { id: this.row.customer || this.row.id },
|
||||||
count_note: { type: 'Count', field: 'id', subquery: { model: 'Customer_Note', column: 'ref' } },
|
/* copied from 02-connection.js */
|
||||||
count_product: { type: 'Count', field: 'id', subquery: { model: 'Product_Booked', column: 'transaction__customer' } },
|
values:
|
||||||
sum_product: { type: 'Sum', field: 'transaction__sale_price', subquery: { model: 'Product_Booked', column: 'transaction__customer' } },
|
'id,update_time,creator,creator__fullname,country,country__name,country__en,issued_date,issued_place,issued_place__name,code,email,fullname,legal_code,phone,legal_type,legal_type__name,address,contact_address,note,type,type__name,updater,updater__fullname,create_time,update_time',
|
||||||
sum_receiver: { type: 'Sum', field: 'transaction__amount_received', subquery: { model: 'Product_Booked', column: 'transaction__customer' } },
|
distinct_values: {
|
||||||
sum_remain: { type: 'Sum', field: 'transaction__amount_remain', subquery: { model: 'Product_Booked', column: 'transaction__customer' } }
|
label: {
|
||||||
|
type: 'Concat',
|
||||||
|
field: ['code', 'fullname', 'phone', 'legal_code'],
|
||||||
|
},
|
||||||
|
order: { type: 'RowNumber' },
|
||||||
|
image_count: {
|
||||||
|
type: 'Count',
|
||||||
|
field: 'id',
|
||||||
|
subquery: { model: 'Customer_File', column: 'ref' },
|
||||||
|
},
|
||||||
|
count_note: {
|
||||||
|
type: 'Count',
|
||||||
|
field: 'id',
|
||||||
|
subquery: { model: 'Customer_Note', column: 'ref' },
|
||||||
|
},
|
||||||
|
count_product: {
|
||||||
|
type: 'Count',
|
||||||
|
field: 'id',
|
||||||
|
subquery: {
|
||||||
|
model: 'Product_Booked',
|
||||||
|
column: 'transaction__customer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sum_product: {
|
||||||
|
type: 'Sum',
|
||||||
|
field: 'transaction__sale_price',
|
||||||
|
subquery: {
|
||||||
|
model: 'Product_Booked',
|
||||||
|
column: 'transaction__customer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sum_receiver: {
|
||||||
|
type: 'Sum',
|
||||||
|
field: 'transaction__amount_received',
|
||||||
|
subquery: {
|
||||||
|
model: 'Product_Booked',
|
||||||
|
column: 'transaction__customer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sum_remain: {
|
||||||
|
type: 'Sum',
|
||||||
|
field: 'transaction__amount_remain',
|
||||||
|
subquery: {
|
||||||
|
model: 'Product_Booked',
|
||||||
|
column: 'transaction__customer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: 'annotate',
|
||||||
},
|
},
|
||||||
summary: 'annotate',
|
}"
|
||||||
},
|
/>
|
||||||
}" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 border-bottom" id="ignore"></div>
|
<div
|
||||||
<div class="buttons mt-2 is-flex is-gap-1" id="ignore">
|
class="mt-4 border-bottom"
|
||||||
<button v-if="$getEditRights('edit', { code: 'customer', category: 'topmenu' })" class="button is-primary" @click="edit()">Chỉnh sửa</button>
|
id="ignore"
|
||||||
<button class="button is-light" @click="$exportpdf(docid, record.code)">In thông tin</button>
|
></div>
|
||||||
|
<div
|
||||||
|
class="buttons mt-2 is-flex is-gap-1"
|
||||||
|
id="ignore"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="$getEditRights('edit', { code: 'customer', category: 'topmenu' })"
|
||||||
|
class="button is-primary"
|
||||||
|
@click="edit()"
|
||||||
|
>
|
||||||
|
Chỉnh sửa
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-light"
|
||||||
|
@click="$exportpdf(docid, record.code)"
|
||||||
|
>
|
||||||
|
In thông tin
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Modal @close="showmodal = undefined" v-bind="showmodal" @dataevent="changeInfo" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
@dataevent="changeInfo"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -225,15 +326,19 @@ export default {
|
|||||||
},
|
},
|
||||||
isIndividual() {
|
isIndividual() {
|
||||||
return this.record.type === 1;
|
return this.record.type === 1;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.record = await this.$getdata("customer", { id: this.row.customer || this.row.id }, undefined, true);
|
this.record = await this.$getdata("customer", { id: this.row.customer || this.row.id }, undefined, true);
|
||||||
if (this.isIndividual) {
|
if (this.isIndividual) {
|
||||||
this.relatedPeople = await this.$getdata("customerpeople", { customer: this.row.customer || this.row.id });
|
this.relatedPeople = await this.$getdata("customerpeople", {
|
||||||
|
customer: this.row.customer || this.row.id,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true);
|
const org = await this.$getdata("organization", { customer: this.row.customer || this.row.id }, undefined, true);
|
||||||
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id });
|
this.relatedPeople = await this.$getdata("legalrep", {
|
||||||
|
organization: org.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -270,10 +375,19 @@ export default {
|
|||||||
this.record = this.$copy(v);
|
this.record = this.$copy(v);
|
||||||
// refetch relatedPeople
|
// refetch relatedPeople
|
||||||
if (this.isIndividual) {
|
if (this.isIndividual) {
|
||||||
this.relatedPeople = await this.$getdata("customerpeople", { customer: this.row.customer || this.row.id });
|
this.relatedPeople = await this.$getdata("customerpeople", {
|
||||||
|
customer: this.row.customer || this.row.id,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true);
|
const org = await this.$getdata(
|
||||||
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id });
|
"organization",
|
||||||
|
{ customer: this.row.customer || this.row.id },
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.relatedPeople = await this.$getdata("legalrep", {
|
||||||
|
organization: org.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openUser(userId) {
|
openUser(userId) {
|
||||||
@@ -287,15 +401,15 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async openRelatedPerson(peopleId) {
|
async openRelatedPerson(peopleId) {
|
||||||
const peopleRow = await this.$getdata('people', { id: peopleId }, undefined, true);
|
const peopleRow = await this.$getdata("people", { id: peopleId }, undefined, true);
|
||||||
this.showmodal = {
|
this.showmodal = {
|
||||||
component: "people/PeopleView",
|
component: "people/PeopleView",
|
||||||
vbind: { row: peopleRow },
|
vbind: { row: peopleRow },
|
||||||
title: 'Người liên quan',
|
title: "Người liên quan",
|
||||||
width: '65%',
|
width: "65%",
|
||||||
height: '400px',
|
height: "400px",
|
||||||
}
|
};
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="fs-13 font-semibold mx-0 px-0 is-flex is-justify-content-center is-align-items-center is-flex-shrink-0 rounded-full size-10"
|
class="fs-13 font-semibold mx-0 px-0 is-flex is-justify-content-center is-align-items-center is-flex-shrink-0 rounded-full size-10"
|
||||||
style="border: 1px solid var(--bulma-grey-80)"
|
style="border: 1px solid var(--bulma-grey-80)"
|
||||||
:style="image && 'border: none'">
|
:style="image && 'border: none'"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<span>{{text}}</span>
|
<span>{{ text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['text', 'image', 'type', 'size']
|
props: ["text", "image", "type", "size"],
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.cbox {
|
.cbox {
|
||||||
|
|||||||
@@ -1,72 +1,72 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import DashboardHighlightCard from '@/components/dashboard/DashboardHighlightCard.vue';
|
import DashboardHighlightCard from "@/components/dashboard/DashboardHighlightCard.vue";
|
||||||
import Delivery from '@/components/dashboard/Delivery.vue';
|
import Delivery from "@/components/dashboard/Delivery.vue";
|
||||||
import OrderStatus from '@/components/dashboard/OrderStatus.vue';
|
import OrderStatus from "@/components/dashboard/OrderStatus.vue";
|
||||||
import RevenueChart from '@/components/dashboard/RevenueChart.vue';
|
import RevenueChart from "@/components/dashboard/RevenueChart.vue";
|
||||||
import TopCustomers from '@/components/dashboard/TopCustomers.vue';
|
import TopCustomers from "@/components/dashboard/TopCustomers.vue";
|
||||||
import TopProducts from '@/components/dashboard/TopProducts.vue';
|
import TopProducts from "@/components/dashboard/TopProducts.vue";
|
||||||
import Warnings from '@/components/dashboard/Warnings.vue';
|
import Warnings from "@/components/dashboard/Warnings.vue";
|
||||||
|
|
||||||
const highlights = [
|
const highlights = [
|
||||||
{
|
{
|
||||||
name: 'Doanh thu hôm nay',
|
name: "Doanh thu hôm nay",
|
||||||
value: '72.5M',
|
value: "72.5M",
|
||||||
color: 'blue',
|
color: "blue",
|
||||||
icon: 'material-symbols:attach-money-rounded',
|
icon: "material-symbols:attach-money-rounded",
|
||||||
subheader: {
|
subheader: {
|
||||||
value: '+12.5%',
|
value: "+12.5%",
|
||||||
fluctuation: 'up',
|
fluctuation: "up",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Số đơn hàng',
|
name: "Số đơn hàng",
|
||||||
value: '73',
|
value: "73",
|
||||||
color: 'purple',
|
color: "purple",
|
||||||
icon: 'material-symbols:shopping-cart-outline-rounded',
|
icon: "material-symbols:shopping-cart-outline-rounded",
|
||||||
subheader: {
|
subheader: {
|
||||||
value: '+8 đơn',
|
value: "+8 đơn",
|
||||||
fluctuation: 'up',
|
fluctuation: "up",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Đơn đang giao',
|
name: "Đơn đang giao",
|
||||||
value: '8',
|
value: "8",
|
||||||
color: 'orange',
|
color: "orange",
|
||||||
icon: 'material-symbols:delivery-truck-speed-outline-rounded',
|
icon: "material-symbols:delivery-truck-speed-outline-rounded",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Đơn hoàn thành',
|
name: "Đơn hoàn thành",
|
||||||
value: '38',
|
value: "38",
|
||||||
color: 'green',
|
color: "green",
|
||||||
icon: 'material-symbols:check-circle-outline-rounded',
|
icon: "material-symbols:check-circle-outline-rounded",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Công nợ phải thu',
|
name: "Công nợ phải thu",
|
||||||
value: '245M',
|
value: "245M",
|
||||||
color: 'red',
|
color: "red",
|
||||||
icon: 'material-symbols:universal-currency-alt-outline-rounded',
|
icon: "material-symbols:universal-currency-alt-outline-rounded",
|
||||||
subheader: {
|
subheader: {
|
||||||
value: '+15M',
|
value: "+15M",
|
||||||
fluctuation: 'down',
|
fluctuation: "down",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tồn kho',
|
name: "Tồn kho",
|
||||||
value: '1,250',
|
value: "1,250",
|
||||||
color: 'cyan',
|
color: "cyan",
|
||||||
icon: 'material-symbols:box-outline-rounded',
|
icon: "material-symbols:box-outline-rounded",
|
||||||
subheader: {
|
subheader: {
|
||||||
value: '-45 SP',
|
value: "-45 SP",
|
||||||
fluctuation: 'down',
|
fluctuation: "down",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="fixed-grid has-2-cols has-3-cols-tablet has-6-cols-desktop">
|
<div class="fixed-grid has-2-cols has-3-cols-tablet has-6-cols-desktop">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<DashboardHighlightCard
|
<DashboardHighlightCard
|
||||||
v-for="highlight in highlights"
|
v-for="highlight in highlights"
|
||||||
:key="highlight.name"
|
:key="highlight.name"
|
||||||
v-bind="highlight"
|
v-bind="highlight"
|
||||||
@@ -88,4 +88,4 @@ const highlights = [
|
|||||||
<Delivery />
|
<Delivery />
|
||||||
<Warnings />
|
<Warnings />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ const props = defineProps({
|
|||||||
value: String,
|
value: String,
|
||||||
color: String,
|
color: String,
|
||||||
icon: String,
|
icon: String,
|
||||||
subheader: Object
|
subheader: Object,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
@@ -14,14 +14,19 @@ const props = defineProps({
|
|||||||
<div>
|
<div>
|
||||||
<p class="fs-14 has-text-grey mb-1">{{ name }}</p>
|
<p class="fs-14 has-text-grey mb-1">{{ name }}</p>
|
||||||
<p class="fsb-26 mb-1 has-text-black">{{ value }}</p>
|
<p class="fsb-26 mb-1 has-text-black">{{ value }}</p>
|
||||||
<div v-if="subheader"
|
<div
|
||||||
|
v-if="subheader"
|
||||||
:class="[
|
:class="[
|
||||||
'is-flex is-gap-0.5 is-align-items-center',
|
'is-flex is-gap-0.5 is-align-items-center',
|
||||||
subheader.fluctuation === 'up' ? 'has-text-green-40' : 'has-text-red-40'
|
subheader.fluctuation === 'up' ? 'has-text-green-40' : 'has-text-red-40',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
:name="subheader.fluctuation === 'up' ? 'material-symbols:arrow-upward-rounded' :'material-symbols:arrow-downward-rounded'"
|
:name="
|
||||||
|
subheader.fluctuation === 'up'
|
||||||
|
? 'material-symbols:arrow-upward-rounded'
|
||||||
|
: 'material-symbols:arrow-downward-rounded'
|
||||||
|
"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
/>
|
/>
|
||||||
<p class="fs-14">{{ subheader.value }}</p>
|
<p class="fs-14">{{ subheader.value }}</p>
|
||||||
@@ -30,12 +35,15 @@ const props = defineProps({
|
|||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'rounded-lg size-12 is-flex-shrink-0 is-flex is-justify-content-center is-align-items-center',
|
'rounded-lg size-12 is-flex-shrink-0 is-flex is-justify-content-center is-align-items-center',
|
||||||
`has-background-${color}-soft has-text-${color}-40`
|
`has-background-${color}-soft has-text-${color}-40`,
|
||||||
]"
|
]"
|
||||||
">
|
>
|
||||||
<Icon :name="icon" :size="28" />
|
<Icon
|
||||||
|
:name="icon"
|
||||||
|
:size="28"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import DeliveryInteractive from '@/components/dashboard/DeliveryInteractive.vue';
|
import DeliveryInteractive from "@/components/dashboard/DeliveryInteractive.vue";
|
||||||
import Driver from '@/components/dashboard/Driver.vue';
|
import Driver from "@/components/dashboard/Driver.vue";
|
||||||
|
|
||||||
const drivers = [
|
const drivers = [
|
||||||
{
|
{
|
||||||
name: 'Nguyễn Văn A',
|
name: "Nguyễn Văn A",
|
||||||
status: 'Đang giao',
|
status: "Đang giao",
|
||||||
deliveries: 3,
|
deliveries: 3,
|
||||||
deliveries_completed: 2,
|
deliveries_completed: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Trần Văn B',
|
name: "Trần Văn B",
|
||||||
status: 'Đang giao',
|
status: "Đang giao",
|
||||||
deliveries: 2,
|
deliveries: 2,
|
||||||
deliveries_completed: 1,
|
deliveries_completed: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Lê Thị C',
|
name: "Lê Thị C",
|
||||||
status: 'Hoàn thành',
|
status: "Hoàn thành",
|
||||||
deliveries: 4,
|
deliveries: 4,
|
||||||
deliveries_completed: 4,
|
deliveries_completed: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Phạm Văn D',
|
name: "Phạm Văn D",
|
||||||
status: 'Đang giao',
|
status: "Đang giao",
|
||||||
deliveries: 1,
|
deliveries: 1,
|
||||||
deliveries_completed: 0,
|
deliveries_completed: 0,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -51,4 +51,4 @@ const drivers = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,47 @@
|
|||||||
class="relative w-full has-background-blue-95 rounded-lg is-clipped"
|
class="relative w-full has-background-blue-95 rounded-lg is-clipped"
|
||||||
style="height: 360px"
|
style="height: 360px"
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 w-full h-full opacity-20"><div class="absolute w-full border-t border-gray-300" style="top: 20%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 40%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 60%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 80%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 100%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 20%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 40%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 60%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 80%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 100%;"></div>
|
<div class="absolute inset-0 w-full h-full opacity-20">
|
||||||
|
<div
|
||||||
|
class="absolute w-full border-t border-gray-300"
|
||||||
|
style="top: 20%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute w-full border-t border-gray-300"
|
||||||
|
style="top: 40%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute w-full border-t border-gray-300"
|
||||||
|
style="top: 60%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute w-full border-t border-gray-300"
|
||||||
|
style="top: 80%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute w-full border-t border-gray-300"
|
||||||
|
style="top: 100%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute h-full border-l border-gray-300"
|
||||||
|
style="left: 20%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute h-full border-l border-gray-300"
|
||||||
|
style="left: 40%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute h-full border-l border-gray-300"
|
||||||
|
style="left: 60%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute h-full border-l border-gray-300"
|
||||||
|
style="left: 80%"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute h-full border-l border-gray-300"
|
||||||
|
style="left: 100%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -25,7 +65,11 @@
|
|||||||
<path
|
<path
|
||||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||||
></path>
|
></path>
|
||||||
<circle cx="12" cy="10" r="3"></circle>
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="10"
|
||||||
|
r="3"
|
||||||
|
></circle>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -47,7 +91,11 @@
|
|||||||
<path
|
<path
|
||||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||||
></path>
|
></path>
|
||||||
<circle cx="12" cy="10" r="3"></circle>
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="10"
|
||||||
|
r="3"
|
||||||
|
></circle>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -69,7 +117,11 @@
|
|||||||
<path
|
<path
|
||||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||||
></path>
|
></path>
|
||||||
<circle cx="12" cy="10" r="3"></circle>
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="10"
|
||||||
|
r="3"
|
||||||
|
></circle>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -91,7 +143,11 @@
|
|||||||
<path
|
<path
|
||||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||||
></path>
|
></path>
|
||||||
<circle cx="12" cy="10" r="3"></circle>
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="10"
|
||||||
|
r="3"
|
||||||
|
></circle>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -113,7 +169,11 @@
|
|||||||
<path
|
<path
|
||||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||||
></path>
|
></path>
|
||||||
<circle cx="12" cy="10" r="3"></circle>
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="10"
|
||||||
|
r="3"
|
||||||
|
></circle>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -139,9 +199,7 @@
|
|||||||
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
|
||||||
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -167,9 +225,7 @@
|
|||||||
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
|
||||||
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -220,9 +276,7 @@
|
|||||||
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
|
||||||
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -244,7 +298,12 @@
|
|||||||
<path
|
<path
|
||||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||||
></path>
|
></path>
|
||||||
<circle cx="12" cy="10" r="3"></circle></svg>Hà Nội
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="10"
|
||||||
|
r="3"
|
||||||
|
></circle></svg
|
||||||
|
>Hà Nội
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +373,7 @@
|
|||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||||
}
|
}
|
||||||
.animate-ping {
|
.animate-ping {
|
||||||
animation: ping 1s cubic-bezier(0,0,0.2,1) infinite;
|
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||||
}
|
}
|
||||||
@keyframes ping {
|
@keyframes ping {
|
||||||
0% {
|
0% {
|
||||||
@@ -339,67 +398,67 @@
|
|||||||
|
|
||||||
@keyframes move-random-1 {
|
@keyframes move-random-1 {
|
||||||
0% {
|
0% {
|
||||||
transform: translateX(0) translateY(0)
|
transform: translateX(0) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
33% {
|
33% {
|
||||||
transform: translateX(30px) translateY(24px)
|
transform: translateX(30px) translateY(24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
66% {
|
66% {
|
||||||
transform: translateX(60px) translateY(12px)
|
transform: translateX(60px) translateY(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(0) translateY(0)
|
transform: translateX(0) translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes move-random-2 {
|
@keyframes move-random-2 {
|
||||||
0% {
|
0% {
|
||||||
transform: translateX(0) translateY(0)
|
transform: translateX(0) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
23% {
|
23% {
|
||||||
transform: translateX(-20px) translateY(-36px)
|
transform: translateX(-20px) translateY(-36px);
|
||||||
}
|
}
|
||||||
|
|
||||||
46% {
|
46% {
|
||||||
transform: translateX(0) translateY(22px)
|
transform: translateX(0) translateY(22px);
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
transform: translateX(30px) translateY(-12px)
|
transform: translateX(30px) translateY(-12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(0) translateY(0)
|
transform: translateX(0) translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes move-random-3 {
|
@keyframes move-random-3 {
|
||||||
0% {
|
0% {
|
||||||
transform: translateX(0) translateY(0)
|
transform: translateX(0) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
10% {
|
10% {
|
||||||
transform: translateX(-30px) translateY(-15px)
|
transform: translateX(-30px) translateY(-15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
30% {
|
30% {
|
||||||
transform: translateX(-10px) translateY(26px)
|
transform: translateX(-10px) translateY(26px);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: translateX(48px) translateY(-28px)
|
transform: translateX(48px) translateY(-28px);
|
||||||
}
|
}
|
||||||
|
|
||||||
80% {
|
80% {
|
||||||
transform: translateX(17px) translateY(10px)
|
transform: translateX(17px) translateY(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(0) translateY(0)
|
transform: translateX(0) translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.border-2 {
|
.border-2 {
|
||||||
@@ -442,13 +501,20 @@
|
|||||||
opacity: 40%;
|
opacity: 40%;
|
||||||
}
|
}
|
||||||
.shadow {
|
.shadow {
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow:
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
|
box-shadow:
|
||||||
|
var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
|
||||||
|
var(--tw-shadow);
|
||||||
}
|
}
|
||||||
.backdrop-blur {
|
.backdrop-blur {
|
||||||
--tw-backdrop-blur: blur(8px);
|
--tw-backdrop-blur: blur(8px);
|
||||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,)
|
||||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,)
|
||||||
|
var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
|
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,)
|
||||||
|
var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,)
|
||||||
|
var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
}
|
}
|
||||||
.transition-all {
|
.transition-all {
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
@@ -474,4 +540,4 @@
|
|||||||
.border-gray-300 {
|
.border-gray-300 {
|
||||||
border-color: var(--bulma-grey-85);
|
border-color: var(--bulma-grey-85);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import AvatarBox from '@/components/dashboard/AvatarBox.vue';
|
import AvatarBox from "@/components/dashboard/AvatarBox.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: String,
|
name: String,
|
||||||
status: String,
|
status: String,
|
||||||
deliveries: Number,
|
deliveries: Number,
|
||||||
deliveries_completed: Number,
|
deliveries_completed: Number,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="is-flex is-gap-2 fs-14 p-3 rounded-lg"
|
class="is-flex is-gap-2 fs-14 p-3 rounded-lg"
|
||||||
:style="{
|
:style="{
|
||||||
border: '1px solid var(--bulma-grey-80)',
|
border: '1px solid var(--bulma-grey-80)',
|
||||||
@@ -22,14 +22,13 @@ const props = defineProps({
|
|||||||
<span :class="['tag', status === 'Đang giao' ? 'is-warning' : 'is-success']">{{ status }}</span>
|
<span :class="['tag', status === 'Đang giao' ? 'is-warning' : 'is-success']">{{ status }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="fs-13 has-text-grey">Đơn: {{ deliveries_completed }}/{{ deliveries }}</p>
|
<p class="fs-13 has-text-grey">Đơn: {{ deliveries_completed }}/{{ deliveries }}</p>
|
||||||
<progress
|
<progress
|
||||||
v-if="deliveries !== deliveries_completed"
|
v-if="deliveries !== deliveries_completed"
|
||||||
class="progress is-small is-primary mt-2"
|
class="progress is-small is-primary mt-2"
|
||||||
style="--bulma-size-small: 0.4rem;"
|
style="--bulma-size-small: 0.4rem"
|
||||||
:value="deliveries_completed"
|
:value="deliveries_completed"
|
||||||
:max="deliveries"
|
:max="deliveries"
|
||||||
>
|
></progress>
|
||||||
</progress>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import OrderStatusCard from '@/components/dashboard/OrderStatusCard.vue';
|
import OrderStatusCard from "@/components/dashboard/OrderStatusCard.vue";
|
||||||
|
|
||||||
const statuses = [
|
const statuses = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
code: 'pending',
|
code: "pending",
|
||||||
name: 'Chờ xử lý',
|
name: "Chờ xử lý",
|
||||||
value: 12,
|
value: 12,
|
||||||
color: 'orange',
|
color: "orange",
|
||||||
icon: 'material-symbols:clock-loader-40'
|
icon: "material-symbols:clock-loader-40",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
code: 'delivering',
|
code: "delivering",
|
||||||
name: 'Đang giao',
|
name: "Đang giao",
|
||||||
value: 8,
|
value: 8,
|
||||||
color: 'blue',
|
color: "blue",
|
||||||
icon: 'material-symbols:delivery-truck-speed-outline-rounded'
|
icon: "material-symbols:delivery-truck-speed-outline-rounded",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
code: 'delivered',
|
code: "delivered",
|
||||||
name: 'Đã giao',
|
name: "Đã giao",
|
||||||
value: 15,
|
value: 15,
|
||||||
color: 'purple',
|
color: "purple",
|
||||||
icon: 'material-symbols:bucket-check-outline-rounded'
|
icon: "material-symbols:bucket-check-outline-rounded",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
code: 'completed',
|
code: "completed",
|
||||||
name: 'Hoàn thành',
|
name: "Hoàn thành",
|
||||||
value: 38,
|
value: 38,
|
||||||
color: 'green',
|
color: "green",
|
||||||
icon: 'material-symbols:check-circle-outline-rounded'
|
icon: "material-symbols:check-circle-outline-rounded",
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="card h-full">
|
<div class="card h-full">
|
||||||
@@ -51,4 +51,4 @@ const statuses = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,18 +6,16 @@ const props = defineProps({
|
|||||||
value: Number,
|
value: Number,
|
||||||
icon: String,
|
icon: String,
|
||||||
color: String,
|
color: String,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<div class="card" :style="{ border: `1px solid var(--bulma-${color}-70)` }">
|
<div
|
||||||
|
class="card"
|
||||||
|
:style="{ border: `1px solid var(--bulma-${color}-70)` }"
|
||||||
|
>
|
||||||
<div class="card-content is-flex is-flex-direction-column is-align-items-center is-gap-1">
|
<div class="card-content is-flex is-flex-direction-column is-align-items-center is-gap-1">
|
||||||
<div
|
<div :class="['p-3 is-flex rounded-full', `has-background-${color}-90`, `has-text-${color}-40`]">
|
||||||
:class="[
|
|
||||||
'p-3 is-flex rounded-full',
|
|
||||||
`has-background-${color}-90`,
|
|
||||||
`has-text-${color}-40`,
|
|
||||||
]" >
|
|
||||||
<Icon
|
<Icon
|
||||||
:name="icon"
|
:name="icon"
|
||||||
:size="24"
|
:size="24"
|
||||||
@@ -30,4 +28,4 @@ const props = defineProps({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
const { $shortenCurrency } = useNuxtApp();
|
const { $shortenCurrency } = useNuxtApp();
|
||||||
const revenueChartOptions = {
|
const revenueChartOptions = {
|
||||||
chart: {
|
chart: {
|
||||||
type: 'spline'
|
type: "spline",
|
||||||
},
|
},
|
||||||
credits: {
|
credits: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -11,38 +11,38 @@ const revenueChartOptions = {
|
|||||||
text: null,
|
text: null,
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
categories: [
|
categories: ["10/4", "11/4", "12/4", "13/4", "14/4", "15/4", "16/4", "17/4", "18/4"],
|
||||||
'10/4', '11/4', '12/4', '13/4', '14/4', '15/4', '16/4', '17/4', '18/4'
|
|
||||||
],
|
|
||||||
accessibility: {
|
accessibility: {
|
||||||
description: 'Dates'
|
description: "Dates",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
title: {
|
title: {
|
||||||
text: 'Doanh thu'
|
text: "Doanh thu",
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
format: '{value}'
|
format: "{value}",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
crosshairs: true,
|
crosshairs: true,
|
||||||
shared: true,
|
shared: true,
|
||||||
valueSuffix: ' VNĐ',
|
valueSuffix: " VNĐ",
|
||||||
},
|
},
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
spline: {
|
spline: {
|
||||||
marker: {
|
marker: {
|
||||||
enabled: false
|
enabled: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
series: [{
|
series: [
|
||||||
name: 'Doanh thu',
|
{
|
||||||
data: [45000000, 52000000, 48000000, 51000000, 58000000, 61000000, 67500000, 72000000, 69000000],
|
name: "Doanh thu",
|
||||||
showInLegend: false,
|
data: [45000000, 52000000, 48000000, 51000000, 58000000, 61000000, 67500000, 72000000, 69000000],
|
||||||
}]
|
showInLegend: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@@ -58,4 +58,4 @@ const revenueChartOptions = {
|
|||||||
<highcharts :options="revenueChartOptions" />
|
<highcharts :options="revenueChartOptions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import AvatarBox from '@/components/dashboard/AvatarBox.vue';
|
import AvatarBox from "@/components/dashboard/AvatarBox.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: String,
|
name: String,
|
||||||
order_count: Number,
|
order_count: Number,
|
||||||
paid: Number,
|
paid: Number,
|
||||||
})
|
});
|
||||||
|
|
||||||
const { $shortenCurrency } = useNuxtApp();
|
const { $shortenCurrency } = useNuxtApp();
|
||||||
</script>
|
</script>
|
||||||
@@ -20,4 +20,4 @@ const { $shortenCurrency } = useNuxtApp();
|
|||||||
</div>
|
</div>
|
||||||
<p class="font-semibold">{{ $shortenCurrency(paid) }}</p>
|
<p class="font-semibold">{{ $shortenCurrency(paid) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,35 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TopCustomer from '@/components/dashboard/TopCustomer.vue';
|
import TopCustomer from "@/components/dashboard/TopCustomer.vue";
|
||||||
|
|
||||||
|
|
||||||
const customers = [
|
const customers = [
|
||||||
{
|
{
|
||||||
name: 'Công ty TNHH ABC',
|
name: "Công ty TNHH ABC",
|
||||||
order_count: 45,
|
order_count: 45,
|
||||||
paid: 125000000,
|
paid: 125000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Siêu thị XYZ',
|
name: "Siêu thị XYZ",
|
||||||
order_count: 38,
|
order_count: 38,
|
||||||
paid: 98000000,
|
paid: 98000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nhà hàng Đông Dương',
|
name: "Nhà hàng Đông Dương",
|
||||||
order_count: 32,
|
order_count: 32,
|
||||||
paid: 87000000,
|
paid: 87000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Khách sạn Mường Thanh',
|
name: "Khách sạn Mường Thanh",
|
||||||
order_count: 28,
|
order_count: 28,
|
||||||
paid: 76000000,
|
paid: 76000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Cửa hàng Bách Hoá',
|
name: "Cửa hàng Bách Hoá",
|
||||||
order_count: 24,
|
order_count: 24,
|
||||||
paid: 64000000,
|
paid: 64000000,
|
||||||
},
|
},
|
||||||
|
];
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -44,4 +42,4 @@ const customers = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ const { $shortenCurrency } = useNuxtApp();
|
|||||||
</div>
|
</div>
|
||||||
<p class="font-semibold">{{ $shortenCurrency(revenue) }}</p>
|
<p class="font-semibold">{{ $shortenCurrency(revenue) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<progress class="progress is-small is-primary" :value="revenue" :max="topRevenue">
|
<progress
|
||||||
|
class="progress is-small is-primary"
|
||||||
|
:value="revenue"
|
||||||
|
:max="topRevenue"
|
||||||
|
>
|
||||||
15%
|
15%
|
||||||
</progress>
|
</progress>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,4 +30,4 @@ const { $shortenCurrency } = useNuxtApp();
|
|||||||
.progress {
|
.progress {
|
||||||
--bulma-size-small: 0.5rem;
|
--bulma-size-small: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TopProduct from '@/components/dashboard/TopProduct.vue';
|
import TopProduct from "@/components/dashboard/TopProduct.vue";
|
||||||
|
|
||||||
const products = [
|
const products = [
|
||||||
{
|
{
|
||||||
name: 'Gạo ST25 - Bao 5kg',
|
name: "Gạo ST25 - Bao 5kg",
|
||||||
sold_count: 1250,
|
sold_count: 1250,
|
||||||
revenue: 156000000
|
revenue: 156000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nước mắm Phú Quốc - Chai 500ml',
|
name: "Nước mắm Phú Quốc - Chai 500ml",
|
||||||
sold_count: 980,
|
sold_count: 980,
|
||||||
revenue: 132000000
|
revenue: 132000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Đường tinh luyện - Bao 1kg',
|
name: "Đường tinh luyện - Bao 1kg",
|
||||||
sold_count: 856,
|
sold_count: 856,
|
||||||
revenue: 98000000
|
revenue: 98000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Dầu ăn Neptune - Chai 1L',
|
name: "Dầu ăn Neptune - Chai 1L",
|
||||||
sold_count: 742,
|
sold_count: 742,
|
||||||
revenue: 87000000
|
revenue: 87000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Bột mì đa dụng - Bao 1kg',
|
name: "Bột mì đa dụng - Bao 1kg",
|
||||||
sold_count: 623,
|
sold_count: 623,
|
||||||
revenue: 72000000
|
revenue: 72000000,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -35,14 +35,14 @@ const products = [
|
|||||||
<p class="fs-17 font-semibold mb-4">Top sản phẩm</p>
|
<p class="fs-17 font-semibold mb-4">Top sản phẩm</p>
|
||||||
<div class="is-flex is-flex-direction-column is-gap-2">
|
<div class="is-flex is-flex-direction-column is-gap-2">
|
||||||
<TopProduct
|
<TopProduct
|
||||||
v-for="product in products"
|
v-for="product in products"
|
||||||
:key="product.name"
|
:key="product.name"
|
||||||
v-bind="{
|
v-bind="{
|
||||||
...product,
|
...product,
|
||||||
topRevenue: products[0].revenue
|
topRevenue: products[0].revenue,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ const props = defineProps({
|
|||||||
name: String,
|
name: String,
|
||||||
details: String,
|
details: String,
|
||||||
level: Number,
|
level: Number,
|
||||||
type: String
|
type: String,
|
||||||
})
|
});
|
||||||
|
|
||||||
const color = computed(() => {
|
const color = computed(() => {
|
||||||
if (props.level === 1) return 'yellow';
|
if (props.level === 1) return "yellow";
|
||||||
if (props.level === 2) return 'orange';
|
if (props.level === 2) return "orange";
|
||||||
if (props.level === 3) return 'red';
|
if (props.level === 3) return "red";
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="['card m-0', `has-background-${color}-95`]"
|
:class="['card m-0', `has-background-${color}-95`]"
|
||||||
@@ -22,7 +21,7 @@ const color = computed(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="card-content p-4 is-flex is-align-items-center is-gap-2">
|
<div class="card-content p-4 is-flex is-align-items-center is-gap-2">
|
||||||
<Icon
|
<Icon
|
||||||
:name="type === 'time' ? 'material-symbols:clock-loader-40' : 'material-symbols:box-outline-rounded'"
|
:name="type === 'time' ? 'material-symbols:clock-loader-40' : 'material-symbols:box-outline-rounded'"
|
||||||
:size="20"
|
:size="20"
|
||||||
:class="`has-text-${color}-45`"
|
:class="`has-text-${color}-45`"
|
||||||
@@ -33,4 +32,4 @@ const color = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Warning from '@/components/dashboard/Warning.vue';
|
import Warning from "@/components/dashboard/Warning.vue";
|
||||||
|
|
||||||
const warnings = [
|
const warnings = [
|
||||||
{
|
{
|
||||||
name: 'Công nợ sắp đến hạn',
|
name: "Công nợ sắp đến hạn",
|
||||||
details: 'Công ty TNHH ABC - 35.000.000đ - Hạn: 25/03/2026',
|
details: "Công ty TNHH ABC - 35.000.000đ - Hạn: 25/03/2026",
|
||||||
level: 1,
|
level: 1,
|
||||||
type: 'time',
|
type: "time",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Đơn giao trễ',
|
name: "Đơn giao trễ",
|
||||||
details: 'Đơn hàng #DH-2156 - Đã trễ 2 giờ - Khách: Siêu thị XYZ',
|
details: "Đơn hàng #DH-2156 - Đã trễ 2 giờ - Khách: Siêu thị XYZ",
|
||||||
level: 3,
|
level: 3,
|
||||||
type: 'time',
|
type: "time",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tồn kho thấp',
|
name: "Tồn kho thấp",
|
||||||
details: 'Gạo ST25 - Chỉ còn 45 bao - Cần nhập thêm',
|
details: "Gạo ST25 - Chỉ còn 45 bao - Cần nhập thêm",
|
||||||
level: 2,
|
level: 2,
|
||||||
type: 'inventory'
|
type: "inventory",
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -35,4 +35,4 @@ const warnings = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })"
|
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })"
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }"
|
v-bind="{
|
||||||
|
name: 'az.svg',
|
||||||
|
type: checkFilter() ? 'grey' : 'primary',
|
||||||
|
size: 22,
|
||||||
|
}"
|
||||||
></SvgIcon>
|
></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -20,7 +24,11 @@
|
|||||||
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })"
|
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })"
|
||||||
>
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }"
|
v-bind="{
|
||||||
|
name: 'az.svg',
|
||||||
|
type: checkFilter() ? 'grey' : 'primary',
|
||||||
|
size: 22,
|
||||||
|
}"
|
||||||
></SvgIcon>
|
></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -30,7 +38,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="moveLeft()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="moveLeft()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'left5.png', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'left5.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -40,7 +51,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="moveRight()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="moveRight()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'right5.png', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'right5.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -50,7 +64,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="resizeWidth()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="resizeWidth()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'thick.svg', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'thick.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -60,7 +77,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="resizeWidth(true)">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="resizeWidth(true)"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'thin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'thin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -70,7 +90,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="hideField()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="hideField()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'eye-off.svg', type: 'primary', size: 23 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'eye-off.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -81,7 +104,10 @@
|
|||||||
</span>
|
</span>
|
||||||
<!-- <template v-if="store.login ? store.login.is_admin : false"> -->
|
<!-- <template v-if="store.login ? store.login.is_admin : false"> -->
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="currentField.mandatory ? false : doRemove()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="currentField.mandatory ? false : doRemove()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'bin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'bin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -94,11 +120,7 @@
|
|||||||
<a
|
<a
|
||||||
class="mr-4"
|
class="mr-4"
|
||||||
:class="currentField.format === 'number' ? null : 'has-text-grey-light'"
|
:class="currentField.format === 'number' ? null : 'has-text-grey-light'"
|
||||||
@click="
|
@click="currentField.format === 'number' ? $emit('modalevent', { name: 'copyfield', data: currentField }) : false"
|
||||||
currentField.format === 'number'
|
|
||||||
? $emit('modalevent', { name: 'copyfield', data: currentField })
|
|
||||||
: false
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
@@ -109,7 +131,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="fieldList()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="fieldList()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'menu4.png', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'menu4.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -119,7 +144,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="createField()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="createField()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'add.png', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'add.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -129,7 +157,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="tableOption()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="tableOption()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'more.svg', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'more.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -139,7 +170,10 @@
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-4" @click="saveSetting()">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="saveSetting()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
@@ -156,7 +190,7 @@
|
|||||||
? currentField.formula
|
? currentField.formula
|
||||||
? true
|
? true
|
||||||
: x.code !== 'formula'
|
: x.code !== 'formula'
|
||||||
: !['filter', 'formula'].find((y) => y === x.code)
|
: !['filter', 'formula'].find((y) => y === x.code),
|
||||||
)"
|
)"
|
||||||
:key="i"
|
:key="i"
|
||||||
:class="selectTab.code === v.code ? 'is-active' : 'has-text-primary'"
|
:class="selectTab.code === v.code ? 'is-active' : 'has-text-primary'"
|
||||||
@@ -193,35 +227,44 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button" @click="editLabel()">
|
<button
|
||||||
|
class="button"
|
||||||
|
@click="editLabel()"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 19 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 19 }"></SvgIcon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'label')">
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'label')"
|
||||||
|
>
|
||||||
{{ errors.find((v) => v.name === "label").msg }}
|
{{ errors.find((v) => v.name === "label").msg }}
|
||||||
</p>
|
</p>
|
||||||
<div class="field mt-3">
|
<div class="field mt-3">
|
||||||
<label class="label fs-14"
|
<label class="label fs-14">Kiểu dữ liệu<span class="has-text-danger"> * </span></label>
|
||||||
>Kiểu dữ liệu<span class="has-text-danger"> * </span></label
|
|
||||||
>
|
|
||||||
<div class="control fs-14">
|
<div class="control fs-14">
|
||||||
<span class="mr-4" v-for="(v, i) in datatype">
|
<span
|
||||||
<span class="icon-text" v-if="radioType === v">
|
class="mr-4"
|
||||||
<SvgIcon
|
v-for="(v, i) in datatype"
|
||||||
v-bind="{ name: 'radio-checked.svg', type: 'gray', size: 22 }"
|
>
|
||||||
></SvgIcon>
|
<span
|
||||||
|
class="icon-text"
|
||||||
|
v-if="radioType === v"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'radio-checked.svg', type: 'gray', size: 22 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
{{ v.name }}
|
{{ v.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-horizontal" v-if="currentField.format === 'number'">
|
<div
|
||||||
|
class="field is-horizontal"
|
||||||
|
v-if="currentField.format === 'number'"
|
||||||
|
>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14"
|
<label class="label fs-14">Đơn vị <span class="has-text-danger"> * </span> </label>
|
||||||
>Đơn vị <span class="has-text-danger"> * </span>
|
|
||||||
</label>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
v-bind="{
|
v-bind="{
|
||||||
@@ -234,7 +277,10 @@
|
|||||||
@option="selected('_account', $event)"
|
@option="selected('_account', $event)"
|
||||||
></SearchBox>
|
></SearchBox>
|
||||||
</div>
|
</div>
|
||||||
<p class="help has-text-danger" v-if="errors.find((v) => v.name === 'unit')">
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'unit')"
|
||||||
|
>
|
||||||
{{ errors.find((v) => v.name === "unit").msg }}
|
{{ errors.find((v) => v.name === "unit").msg }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,7 +307,10 @@
|
|||||||
class="mr-4"
|
class="mr-4"
|
||||||
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||||
>
|
>
|
||||||
<a class="icon-text" @click="changeTemplate(v)">
|
<a
|
||||||
|
class="icon-text"
|
||||||
|
@click="changeTemplate(v)"
|
||||||
|
>
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
v-bind="{
|
v-bind="{
|
||||||
name: `radio-${radioTemplate === v.code ? '' : 'un'}checked.svg`,
|
name: `radio-${radioTemplate === v.code ? '' : 'un'}checked.svg`,
|
||||||
@@ -277,11 +326,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3" v-if="radioTemplate === 'option'">
|
<p
|
||||||
<button class="button is-primary is-small has-text-white" @click="showSidebar()">
|
class="mt-3"
|
||||||
<span class="fs-14">{{
|
v-if="radioTemplate === 'option'"
|
||||||
`${currentField.template ? "Sửa" : "Tạo"} định dạng`
|
>
|
||||||
}}</span>
|
<button
|
||||||
|
class="button is-primary is-small has-text-white"
|
||||||
|
@click="showSidebar()"
|
||||||
|
>
|
||||||
|
<span class="fs-14">{{ `${currentField.template ? "Sửa" : "Tạo"} định dạng` }}</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,14 +361,7 @@
|
|||||||
import { useStore } from "@/stores/index";
|
import { useStore } from "@/stores/index";
|
||||||
import ScrollBox from "~/components/datatable/ScrollBox";
|
import ScrollBox from "~/components/datatable/ScrollBox";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const {
|
const { $copy, $stripHtml, $clone, $arrayMove, $snackbar, $copyToClipboard } = useNuxtApp();
|
||||||
$copy,
|
|
||||||
$stripHtml,
|
|
||||||
$clone,
|
|
||||||
$arrayMove,
|
|
||||||
$snackbar,
|
|
||||||
$copyToClipboard,
|
|
||||||
} = useNuxtApp();
|
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
pagename: String,
|
pagename: String,
|
||||||
field: Object,
|
field: Object,
|
||||||
@@ -337,9 +383,7 @@ const getMenu = function () {
|
|||||||
let field = currentField;
|
let field = currentField;
|
||||||
field.disable = "display,tooltip";
|
field.disable = "display,tooltip";
|
||||||
let arr = field.disable ? field.disable.split(",") : undefined;
|
let arr = field.disable ? field.disable.split(",") : undefined;
|
||||||
let array = arr
|
let array = arr ? store.menuchoice.filter((v) => arr.findIndex((x) => x === v.code) < 0) : store.menuchoice;
|
||||||
? store.menuchoice.filter((v) => arr.findIndex((x) => x === v.code) < 0)
|
|
||||||
: store.menuchoice;
|
|
||||||
//if (store.login ? !(store.login.is_admin === false) : true) array = [array[0]];
|
//if (store.login ? !(store.login.is_admin === false) : true) array = [array[0]];
|
||||||
return array;
|
return array;
|
||||||
};
|
};
|
||||||
@@ -350,10 +394,7 @@ var value1 = undefined;
|
|||||||
var value2 = undefined;
|
var value2 = undefined;
|
||||||
var moneyunit = store.moneyunit;
|
var moneyunit = store.moneyunit;
|
||||||
var radioType = store.datatype.find((v) => v.code === currentField.format);
|
var radioType = store.datatype.find((v) => v.code === currentField.format);
|
||||||
var selectUnit =
|
var selectUnit = currentField.format === "number" ? moneyunit.find((v) => v.detail === currentField.unit) : undefined;
|
||||||
currentField.format === "number"
|
|
||||||
? moneyunit.find((v) => v.detail === currentField.unit)
|
|
||||||
: undefined;
|
|
||||||
var bgcolor = undefined;
|
var bgcolor = undefined;
|
||||||
var radioBGcolor = colorchoice.find((v) => v.code === "none");
|
var radioBGcolor = colorchoice.find((v) => v.code === "none");
|
||||||
var color = undefined;
|
var color = undefined;
|
||||||
@@ -366,16 +407,12 @@ var radioMaxWidth = colorchoice.find((v) => v.code === "none");
|
|||||||
var maxwidth = undefined;
|
var maxwidth = undefined;
|
||||||
var selectAlign = undefined;
|
var selectAlign = undefined;
|
||||||
var radioAlign = colorchoice.find((v) => v.code === "none");
|
var radioAlign = colorchoice.find((v) => v.code === "none");
|
||||||
var radioTemplate = ref(
|
var radioTemplate = ref(colorchoice.find((v) => v.code === (currentField.template ? "option" : "none"))["code"]);
|
||||||
colorchoice.find((v) => v.code === (currentField.template ? "option" : "none"))["code"]
|
|
||||||
);
|
|
||||||
var selectPlacement = store.placement.find((v) => v.code === "is-right");
|
var selectPlacement = store.placement.find((v) => v.code === "is-right");
|
||||||
var selectScheme = store.colorscheme.find((v) => v.code === "is-primary");
|
var selectScheme = store.colorscheme.find((v) => v.code === "is-primary");
|
||||||
var radioTooltip = store.colorchoice.find((v) => v.code === "none");
|
var radioTooltip = store.colorchoice.find((v) => v.code === "none");
|
||||||
var selectField = undefined;
|
var selectField = undefined;
|
||||||
var tags = currentField.tags
|
var tags = currentField.tags ? currentField.tags.map((v) => fields.find((x) => x.name === v)) : [];
|
||||||
? currentField.tags.map((v) => fields.find((x) => x.name === v))
|
|
||||||
: [];
|
|
||||||
var formula = currentField.formula ? currentField.formula : undefined;
|
var formula = currentField.formula ? currentField.formula : undefined;
|
||||||
var decimal = currentField.decimal;
|
var decimal = currentField.decimal;
|
||||||
let shortmenu = store.menuchoice.filter((x) =>
|
let shortmenu = store.menuchoice.filter((x) =>
|
||||||
@@ -383,7 +420,7 @@ let shortmenu = store.menuchoice.filter((x) =>
|
|||||||
? currentField.formula
|
? currentField.formula
|
||||||
? true
|
? true
|
||||||
: x.code !== "formula"
|
: x.code !== "formula"
|
||||||
: !["filter", "formula"].find((y) => y === x.code)
|
: !["filter", "formula"].find((y) => y === x.code),
|
||||||
);
|
);
|
||||||
var selectTab = shortmenu.find((v) => selectTab.code === v.code)
|
var selectTab = shortmenu.find((v) => selectTab.code === v.code)
|
||||||
? selectTab
|
? selectTab
|
||||||
@@ -448,9 +485,7 @@ function tableOption() {
|
|||||||
}
|
}
|
||||||
const getFields = function () {
|
const getFields = function () {
|
||||||
fields = pagedata ? $copy(pagedata.fields) : [];
|
fields = pagedata ? $copy(pagedata.fields) : [];
|
||||||
fields.map(
|
fields.map((v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label));
|
||||||
(v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
const doSelect = function (evt) {
|
const doSelect = function (evt) {
|
||||||
emit("modalevent", { name: "selected", data: evt[props.field.name] });
|
emit("modalevent", { name: "selected", data: evt[props.field.name] });
|
||||||
@@ -510,15 +545,16 @@ const saveSetting = function () {
|
|||||||
const showSidebar = function () {
|
const showSidebar = function () {
|
||||||
let event = { name: "template", field: currentField };
|
let event = { name: "template", field: currentField };
|
||||||
let title = "Danh sách cột";
|
let title = "Danh sách cột";
|
||||||
if (event.name === "bgcolor")
|
if (event.name === "bgcolor") title = `Đổi màu nền: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||||
title = `Đổi màu nền: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
else if (event.name === "color") title = `Đổi màu chữ: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||||
else if (event.name === "color")
|
else if (event.name === "template") title = `Định dạng nâng cao: ${$stripHtml(event.field.label, 30)}`;
|
||||||
title = `Đổi màu chữ: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
|
||||||
else if (event.name === "template")
|
|
||||||
title = `Định dạng nâng cao: ${$stripHtml(event.field.label, 30)}`;
|
|
||||||
showmodal.value = {
|
showmodal.value = {
|
||||||
component: "datatable/FormatOption",
|
component: "datatable/FormatOption",
|
||||||
vbind: { event: event, currentField: currentField, pagename: props.pagename },
|
vbind: {
|
||||||
|
event: event,
|
||||||
|
currentField: currentField,
|
||||||
|
pagename: props.pagename,
|
||||||
|
},
|
||||||
width: "850px",
|
width: "850px",
|
||||||
height: "700px",
|
height: "700px",
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
@@ -1,180 +1,312 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="docid">
|
<div v-if="docid">
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-body">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Đối tượng</label>
|
|
||||||
<p class="control fs-14">
|
|
||||||
<b-radio v-for="(v,i) in types" :key="i" v-model="type"
|
|
||||||
:native-value="v" @input="changeType(v)">
|
|
||||||
{{v.name}}
|
|
||||||
</b-radio>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Kích cỡ</label>
|
|
||||||
<p class="control fs-14">
|
|
||||||
<b-radio v-for="(v,i) in sizes.filter(v=>type? (type.code==='tag'? v.code!=='is-small' : 1>0) : true)" :key="i" v-model="size"
|
|
||||||
:native-value="v" @input="changeType(v)">
|
|
||||||
{{v.name}}
|
|
||||||
</b-radio>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" v-if="['tag'].find(v=>v===type.code)">Hình khối</label>
|
|
||||||
<p class="control fs-14" v-if="['tag'].find(v=>v===type.code)">
|
|
||||||
<b-radio v-for="(v,i) in shapes" :key="i" v-model="shape"
|
|
||||||
:native-value="v" @input="changeType(v)">
|
|
||||||
{{v.name}}
|
|
||||||
</b-radio>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field is-horizontal" v-if="['tag'].find(v=>v===type.code)">
|
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field" v-if="type.code!=='tag'">
|
<div class="field">
|
||||||
<label class="label">Outline</label>
|
<label class="label">Đối tượng</label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<b-radio v-for="(v,i) in outlines" :key="i" v-model="outline"
|
<b-radio
|
||||||
:native-value="v" @input="changeType(v)">
|
v-for="(v, i) in types"
|
||||||
{{v.name}}
|
:key="i"
|
||||||
|
v-model="type"
|
||||||
|
:native-value="v"
|
||||||
|
@input="changeType(v)"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</b-radio>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Kích cỡ</label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<b-radio
|
||||||
|
v-for="(v, i) in sizes.filter((v) =>
|
||||||
|
type ? (type.code === 'tag' ? v.code !== 'is-small' : 1 > 0) : true,
|
||||||
|
)"
|
||||||
|
:key="i"
|
||||||
|
v-model="size"
|
||||||
|
:native-value="v"
|
||||||
|
@input="changeType(v)"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</b-radio>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
v-if="['tag'].find((v) => v === type.code)"
|
||||||
|
>Hình khối</label
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="control fs-14"
|
||||||
|
v-if="['tag'].find((v) => v === type.code)"
|
||||||
|
>
|
||||||
|
<b-radio
|
||||||
|
v-for="(v, i) in shapes"
|
||||||
|
:key="i"
|
||||||
|
v-model="shape"
|
||||||
|
:native-value="v"
|
||||||
|
@input="changeType(v)"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
</b-radio>
|
</b-radio>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="tags" v-if="type.code==='tag'">
|
|
||||||
<a :class="getClass(v)" v-for="(v,i) in colorscheme" :key="i"
|
|
||||||
@click="doSelect(v)" :ref="'tag' + i"> {{v.name}} </a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-2" v-else-if="type.code==='span'">
|
<div
|
||||||
<a class="mr-3" :class="getSpanClass(v)" v-for="(v,i) in colorscheme" :key="i"
|
class="field is-horizontal"
|
||||||
@click="doSelectSpan(v)" :ref="'span' + i"> {{v.name}} </a>
|
v-if="['tag'].find((v) => v === type.code)"
|
||||||
|
>
|
||||||
|
<div class="field-body">
|
||||||
|
<div
|
||||||
|
class="field"
|
||||||
|
v-if="type.code !== 'tag'"
|
||||||
|
>
|
||||||
|
<label class="label">Outline</label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<b-radio
|
||||||
|
v-for="(v, i) in outlines"
|
||||||
|
:key="i"
|
||||||
|
v-model="outline"
|
||||||
|
:native-value="v"
|
||||||
|
@input="changeType(v)"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</b-radio>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`tabs is-boxed mt-5 mb-5 ${tab.code==='template'? '' : 'pb-2'}`">
|
<div
|
||||||
|
class="tags"
|
||||||
|
v-if="type.code === 'tag'"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:class="getClass(v)"
|
||||||
|
v-for="(v, i) in colorscheme"
|
||||||
|
:key="i"
|
||||||
|
@click="doSelect(v)"
|
||||||
|
:ref="'tag' + i"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pt-2"
|
||||||
|
v-else-if="type.code === 'span'"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="mr-3"
|
||||||
|
:class="getSpanClass(v)"
|
||||||
|
v-for="(v, i) in colorscheme"
|
||||||
|
:key="i"
|
||||||
|
@click="doSelectSpan(v)"
|
||||||
|
:ref="'span' + i"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div :class="`tabs is-boxed mt-5 mb-5 ${tab.code === 'template' ? '' : 'pb-2'}`">
|
||||||
<ul>
|
<ul>
|
||||||
<li :class="tab.code===v.code? 'is-active' : ''"
|
<li
|
||||||
v-for="(v,i) in tabs" :key="i" @click="changeTab(v)"><a class="fs-15">{{v.name}}</a>
|
:class="tab.code === v.code ? 'is-active' : ''"
|
||||||
|
v-for="(v, i) in tabs"
|
||||||
|
:key="i"
|
||||||
|
@click="changeTab(v)"
|
||||||
|
>
|
||||||
|
<a class="fs-15">{{ v.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="tab.code==='selected'">
|
<template v-if="tab.code === 'selected'">
|
||||||
<a v-for="(v,i) in tags" :key="i" @click="selected=v">
|
<a
|
||||||
<div class="field is-grouped is-grouped-multiline mt-4">
|
v-for="(v, i) in tags"
|
||||||
<p class="control">
|
:key="i"
|
||||||
<a :class="v.class">
|
@click="selected = v"
|
||||||
{{v.name}}
|
>
|
||||||
|
<div class="field is-grouped is-grouped-multiline mt-4">
|
||||||
|
<p class="control">
|
||||||
|
<a :class="v.class">
|
||||||
|
{{ v.name }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<input
|
||||||
|
class="input is-small"
|
||||||
|
type="text"
|
||||||
|
v-model="v.name"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<a @click="remove(i)">
|
||||||
|
<SvgIcon v-bind="{ name: 'close.svg', type: 'danger', size: 22 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="control has-text-right ml-5"
|
||||||
|
v-if="selected ? selected.id === v.id : false"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</template>
|
||||||
<p class="control">
|
|
||||||
<input class="input is-small" type="text" v-model="v.name">
|
|
||||||
</p>
|
|
||||||
<p class="control">
|
|
||||||
<a @click="remove(i)">
|
|
||||||
<SvgIcon v-bind="{name: 'close.svg', type: 'danger', size: 22}"></SvgIcon>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p class="control has-text-right ml-5" v-if="selected? selected.id===v.id : false">
|
|
||||||
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 22}"></SvgIcon>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="tab.code==='condition'">
|
<template v-else-if="tab.code === 'condition'">
|
||||||
<div class="mb-5" v-if="selected">
|
<div
|
||||||
<b-radio v-for="(v,i) in conditions" :key="i" v-model="condition"
|
class="mb-5"
|
||||||
:native-value="v" @input="changeCondition(v)">
|
v-if="selected"
|
||||||
{{v.name}}
|
>
|
||||||
</b-radio>
|
<b-radio
|
||||||
</div>
|
v-for="(v, i) in conditions"
|
||||||
|
:key="i"
|
||||||
<template v-if="condition? condition.code==='yes' : false">
|
v-model="condition"
|
||||||
<div class="field mt-3">
|
:native-value="v"
|
||||||
<label class="label fs-14">Chọn trường xây dựng biểu thức <span class="has-text-danger"> * </span> </label>
|
@input="changeCondition(v)"
|
||||||
<div class="control">
|
>
|
||||||
<b-taginput
|
{{ v.name }}
|
||||||
size="is-small"
|
</b-radio>
|
||||||
v-model="tagsField"
|
|
||||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
|
||||||
type="is-dark is-light"
|
|
||||||
autocomplete
|
|
||||||
:open-on-focus="true"
|
|
||||||
field="name"
|
|
||||||
icon="plus"
|
|
||||||
placeholder="Chọn trường"
|
|
||||||
>
|
|
||||||
<template slot-scope="props">
|
|
||||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
|
||||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 50)}} </span>
|
|
||||||
</template>
|
|
||||||
<template slot="empty">
|
|
||||||
Không có trường thỏa mãn
|
|
||||||
</template>
|
|
||||||
</b-taginput>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
|
|
||||||
</div>
|
<template v-if="condition ? condition.code === 'yes' : false">
|
||||||
<div class="field mt-1" v-if="tagsField.length>0">
|
<div class="field mt-3">
|
||||||
<p class="help is-primary"> Click đúp vào để thêm vào biểu thức.</p>
|
<label class="label fs-14"
|
||||||
|
>Chọn trường xây dựng biểu thức
|
||||||
|
<span class="has-text-danger"> * </span>
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<b-taginput
|
||||||
|
size="is-small"
|
||||||
|
v-model="tagsField"
|
||||||
|
:data="pageData ? pageData.fields.filter((v) => v.format === 'number') : []"
|
||||||
|
type="is-dark is-light"
|
||||||
|
autocomplete
|
||||||
|
:open-on-focus="true"
|
||||||
|
field="name"
|
||||||
|
icon="plus"
|
||||||
|
placeholder="Chọn trường"
|
||||||
|
>
|
||||||
|
<template slot-scope="props">
|
||||||
|
<span class="mr-3 has-text-danger has-text-weight-bold"> {{ props.option.name }}</span>
|
||||||
|
<span :class="tagsField.find((v) => v.id === props.option.id) ? 'has-text-dark' : ''">
|
||||||
|
{{ $stripHtml(props.option.label, 50) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template slot="empty"> Không có trường thỏa mãn </template>
|
||||||
|
</b-taginput>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'tagsField')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "tagsField").message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="field mt-1"
|
||||||
|
v-if="tagsField.length > 0"
|
||||||
|
>
|
||||||
|
<p class="help is-primary">Click đúp vào để thêm vào biểu thức.</p>
|
||||||
<div class="tagsField">
|
<div class="tagsField">
|
||||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
<a
|
||||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
@dblclick="expression = expression ? expression + ' ' + v.name : v.name"
|
||||||
<span class="tooltip">
|
class="tag is-rounded"
|
||||||
{{v.name}}
|
v-for="(v, i) in tagsField"
|
||||||
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
|
:key="i"
|
||||||
</span>
|
>
|
||||||
|
<span class="tooltip">
|
||||||
|
{{ v.name }}
|
||||||
|
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14"
|
||||||
|
>Biểu thức có dạng Đúng / Sai
|
||||||
|
<span class="has-text-danger"> * </span>
|
||||||
|
</label>
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<input class="input" type="text" v-model="expression" placeholder="Tạo biểu thức tại đây">
|
<input
|
||||||
</p>
|
class="input"
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
type="text"
|
||||||
|
v-model="expression"
|
||||||
|
placeholder="Tạo biểu thức tại đây"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'expression')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "expression").message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="tab.code==='option' && selected">
|
<template v-else-if="tab.code === 'option' && selected">
|
||||||
<div class="field is-horizontal border-bottom pb-2 mt-1">
|
<div class="field is-horizontal border-bottom pb-2 mt-1">
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14">Màu nền </label>
|
<label class="label fs-14">Màu nền </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioBGcolor"
|
<b-radio
|
||||||
:native-value="v" @input="changeStyle()">
|
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||||
{{v.name}}
|
:key="i"
|
||||||
</b-radio>
|
v-model="radioBGcolor"
|
||||||
|
:native-value="v"
|
||||||
|
@input="changeStyle()"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</b-radio>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" v-if="radioBGcolor? radioBGcolor.code==='option' : false">
|
<div
|
||||||
|
class="field"
|
||||||
|
v-if="radioBGcolor ? radioBGcolor.code === 'option' : false"
|
||||||
|
>
|
||||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<input type="color" v-model="bgcolor" @change="changeStyle()">
|
<input
|
||||||
|
type="color"
|
||||||
|
v-model="bgcolor"
|
||||||
|
@change="changeStyle()"
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field is-horizontal border-bottom pb-2">
|
<div class="field is-horizontal border-bottom pb-2">
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14">Màu chữ </label>
|
<label class="label fs-14">Màu chữ </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioColor"
|
<b-radio
|
||||||
:native-value="v" @input="changeStyle()">
|
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||||
{{v.name}}
|
:key="i"
|
||||||
|
v-model="radioColor"
|
||||||
|
:native-value="v"
|
||||||
|
@input="changeStyle()"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
</b-radio>
|
</b-radio>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" v-if="radioColor? radioColor.code==='option' : false">
|
<div
|
||||||
|
class="field"
|
||||||
|
v-if="radioColor ? radioColor.code === 'option' : false"
|
||||||
|
>
|
||||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<input type="color" v-model="color" @change="changeStyle()">
|
<input
|
||||||
|
type="color"
|
||||||
|
v-model="color"
|
||||||
|
@change="changeStyle()"
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,119 +316,189 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14">Cỡ chữ </label>
|
<label class="label fs-14">Cỡ chữ </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioSize"
|
<b-radio
|
||||||
:native-value="v" @input="changeStyle()">
|
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||||
{{v.name}}
|
:key="i"
|
||||||
</b-radio>
|
v-model="radioSize"
|
||||||
|
:native-value="v"
|
||||||
|
@input="changeStyle()"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</b-radio>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" v-if="radioSize? radioSize.code==='option' : false">
|
<div
|
||||||
|
class="field"
|
||||||
|
v-if="radioSize ? radioSize.code === 'option' : false"
|
||||||
|
>
|
||||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="textsize" @change="changeStyle()">
|
<input
|
||||||
|
class="input is-small"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nhập số"
|
||||||
|
v-model="textsize"
|
||||||
|
@change="changeStyle()"
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="tab.code==='template'">
|
<template v-else-if="tab.code === 'template'">
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
<a @click="copyContent()" class="mr-6">
|
<a
|
||||||
<span class="icon-text">
|
@click="copyContent()"
|
||||||
<SvgIcon class="mr-2" v-bind="{name: 'copy.svg', type: 'primary', siz: 18}"></SvgIcon>
|
class="mr-6"
|
||||||
|
>
|
||||||
|
<span class="icon-text">
|
||||||
|
<SvgIcon
|
||||||
|
class="mr-2"
|
||||||
|
v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"
|
||||||
|
></SvgIcon>
|
||||||
<span class="fs-16">Copy</span>
|
<span class="fs-16">Copy</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a @click="paste()" class="mr-6">
|
<a
|
||||||
|
@click="paste()"
|
||||||
|
class="mr-6"
|
||||||
|
>
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<SvgIcon class="mr-2" v-bind="{name: 'pen1.svg', type: 'primary', siz: 18}"></SvgIcon>
|
<SvgIcon
|
||||||
|
class="mr-2"
|
||||||
|
v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"
|
||||||
|
></SvgIcon>
|
||||||
<span class="fs-16">Paste</span>
|
<span class="fs-16">Paste</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<textarea class="textarea fs-14" rows="8" v-model="text" @dblclick="doCheck"></textarea>
|
<textarea
|
||||||
</div>
|
class="textarea fs-14"
|
||||||
<p class="mt-5">
|
rows="8"
|
||||||
|
v-model="text"
|
||||||
|
@dblclick="doCheck"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<p class="mt-5">
|
||||||
<span class="icon-text fsb-18">
|
<span class="icon-text fsb-18">
|
||||||
Replace
|
Replace
|
||||||
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 22}"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="field is-grouped mt-4">
|
<div class="field is-grouped mt-4">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<p class="fsb-14 mb-1">Đoạn text</p>
|
<p class="fsb-14 mb-1">Đoạn text</p>
|
||||||
<input class="input" type="text" placeholder="" v-model="source">
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="source"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<p class="fsb-14 mb-1">Thay bằng</p>
|
<p class="fsb-14 mb-1">Thay bằng</p>
|
||||||
<input class="input" type="text" placeholder="" v-model="target">
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="target"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control pl-5">
|
<div class="control pl-5">
|
||||||
<button class="button is-primary is-outlined mt-5" @click="replace()">Replace</button>
|
<button
|
||||||
|
class="button is-primary is-outlined mt-5"
|
||||||
|
@click="replace()"
|
||||||
|
>
|
||||||
|
Replace
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-5">
|
<p class="mt-5">
|
||||||
<button class="button is-primary has-text-white" @click="changeTemplate()">Áp dụng</button>
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="changeTemplate()"
|
||||||
|
>
|
||||||
|
Áp dụng
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import { useStore } from '@/stores/index'
|
import { useStore } from "@/stores/index";
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp()
|
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp();
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
pagename: String,
|
pagename: String,
|
||||||
field: Object
|
field: Object,
|
||||||
})
|
});
|
||||||
var colorscheme = store.colorscheme
|
var colorscheme = store.colorscheme;
|
||||||
var colorchoice = store.colorchoice
|
var colorchoice = store.colorchoice;
|
||||||
var pageData = store[props.pagename]
|
var pageData = store[props.pagename];
|
||||||
var field = props.field
|
var field = props.field;
|
||||||
var type = undefined
|
var type = undefined;
|
||||||
var size = undefined
|
var size = undefined;
|
||||||
var types = [{code: 'span', name: 'span'}, {code: 'tag', name: 'tag'}]
|
var types = [
|
||||||
var sizes = [{code: 'is-small', name: 'Nhỏ', value: 'is-size-6'}, {code: 'is-normal', name: 'Trung bình', value: 'is-size-5'},
|
{ code: "span", name: "span" },
|
||||||
{code: 'is-medium', name: 'Lớn', value: 'is-size-4'}]
|
{ code: "tag", name: "tag" },
|
||||||
var shapes = [{code: 'default', name: 'Mặc định'}, {code: 'is-rounded', name: 'Tròn góc'}]
|
];
|
||||||
var shape = undefined
|
var sizes = [
|
||||||
var outlines = [{code: 'default', name: 'Mặc định'}, {code: 'is-outlined', name: 'Outline'}]
|
{ code: "is-small", name: "Nhỏ", value: "is-size-6" },
|
||||||
var outline = undefined
|
{ code: "is-normal", name: "Trung bình", value: "is-size-5" },
|
||||||
var conditions = [{code: 'no', name: 'Không áp dụng'}, {code: 'yes', name: 'Có áp dụng'}]
|
{ code: "is-medium", name: "Lớn", value: "is-size-4" },
|
||||||
var condition = undefined
|
];
|
||||||
var tags = []
|
var shapes = [
|
||||||
var selected = undefined
|
{ code: "default", name: "Mặc định" },
|
||||||
var tabs = [{code: 'selected', name: 'Bước 1: Tạo nội dung'}, {code: 'condition', name: 'Bước 2: Đặt điều kiện'}, {code: 'option', name: 'Bước 3: Chọn màu, cỡ chữ'},
|
{ code: "is-rounded", name: "Tròn góc" },
|
||||||
{code: 'template', name: 'Bước 4: Mã lệnh & áp dụng'}]
|
];
|
||||||
var tab = ref(undefined)
|
var shape = undefined;
|
||||||
var tagsField = []
|
var outlines = [
|
||||||
var errors = []
|
{ code: "default", name: "Mặc định" },
|
||||||
var expression = ''
|
{ code: "is-outlined", name: "Outline" },
|
||||||
var text = ref(null)
|
];
|
||||||
var radioBGcolor = undefined
|
var outline = undefined;
|
||||||
var radioColor = undefined
|
var conditions = [
|
||||||
var radioSize = undefined
|
{ code: "no", name: "Không áp dụng" },
|
||||||
var bgcolor = undefined
|
{ code: "yes", name: "Có áp dụng" },
|
||||||
var color = undefined
|
];
|
||||||
var textsize = undefined
|
var condition = undefined;
|
||||||
var source = undefined
|
var tags = [];
|
||||||
var target = $copy(field.name)
|
var selected = undefined;
|
||||||
|
var tabs = [
|
||||||
const initData = function() {
|
{ code: "selected", name: "Bước 1: Tạo nội dung" },
|
||||||
type = types.find(v=>v.code==='tag')
|
{ code: "condition", name: "Bước 2: Đặt điều kiện" },
|
||||||
size = sizes.find(v=>v.code==='is-normal')
|
{ code: "option", name: "Bước 3: Chọn màu, cỡ chữ" },
|
||||||
shape = shapes.find(v=>v.code==='is-rounded')
|
{ code: "template", name: "Bước 4: Mã lệnh & áp dụng" },
|
||||||
outline = shapes.find(v=>v.code==='default')
|
];
|
||||||
if($empty(field.template)) tab.value =tabs.find(v=>v.code==='selected')
|
var tab = ref(undefined);
|
||||||
else {
|
var tagsField = [];
|
||||||
text.value =$copy(field.template)
|
var errors = [];
|
||||||
tab.value =tabs.find(v=>v.code==='template')
|
var expression = "";
|
||||||
}
|
var text = ref(null);
|
||||||
condition =conditions.find(v=>v.code==='no')
|
var radioBGcolor = undefined;
|
||||||
|
var radioColor = undefined;
|
||||||
|
var radioSize = undefined;
|
||||||
|
var bgcolor = undefined;
|
||||||
|
var color = undefined;
|
||||||
|
var textsize = undefined;
|
||||||
|
var source = undefined;
|
||||||
|
var target = $copy(field.name);
|
||||||
|
|
||||||
|
const initData = function () {
|
||||||
|
type = types.find((v) => v.code === "tag");
|
||||||
|
size = sizes.find((v) => v.code === "is-normal");
|
||||||
|
shape = shapes.find((v) => v.code === "is-rounded");
|
||||||
|
outline = shapes.find((v) => v.code === "default");
|
||||||
|
if ($empty(field.template)) tab.value = tabs.find((v) => v.code === "selected");
|
||||||
|
else {
|
||||||
|
text.value = $copy(field.template);
|
||||||
|
tab.value = tabs.find((v) => v.code === "template");
|
||||||
}
|
}
|
||||||
/*watch: {
|
condition = conditions.find((v) => v.code === "no");
|
||||||
|
};
|
||||||
|
/*watch: {
|
||||||
expression: function(newVal) {
|
expression: function(newVal) {
|
||||||
if($empty(newVal)) return
|
if($empty(newVal)) return
|
||||||
elsecheckExpression()
|
elsecheckExpression()
|
||||||
@@ -332,100 +534,98 @@ const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = use
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},*/
|
},*/
|
||||||
function changeTab(v) {
|
function changeTab(v) {
|
||||||
tab.value = v
|
tab.value = v;
|
||||||
|
}
|
||||||
|
const paste = async function () {
|
||||||
|
text.value = await navigator.clipboard.readText();
|
||||||
|
};
|
||||||
|
const replace = function () {
|
||||||
|
if ($empty(text.value)) return;
|
||||||
|
text.value = text.value.replaceAll(source, target);
|
||||||
|
};
|
||||||
|
const doCheck = function () {
|
||||||
|
let text = window.getSelection().toString();
|
||||||
|
if ($empty(text)) return;
|
||||||
|
source = text;
|
||||||
|
};
|
||||||
|
const changeStyle = function () {
|
||||||
|
selected.bgcolor = selected.color = selected.textsize = selected.style = undefined;
|
||||||
|
let style = "";
|
||||||
|
if (radioBGcolor.code === "option" ? !$empty(bgcolor) : false) {
|
||||||
|
selected.bgcolor = bgcolor;
|
||||||
|
style += "background-color: " + bgcolor + " !important; ";
|
||||||
}
|
}
|
||||||
const paste = async function() {
|
if (radioColor.code === "option" ? !$empty(color) : false) {
|
||||||
text.value = await navigator.clipboard.readText()
|
selected.color = color;
|
||||||
|
style += "color: " + color + " !important; ";
|
||||||
|
}
|
||||||
|
if (radioSize.code === "option" ? $isNumber(textsize) : false) {
|
||||||
|
selected.textsize = textsize;
|
||||||
|
style += "font-size: " + textsize + "px !important; ";
|
||||||
|
}
|
||||||
|
$empty(style) ? false : (selected.style = style);
|
||||||
|
};
|
||||||
|
const changeCondition = function (v) {
|
||||||
|
if (v.code === "no") selected.expression = undefined;
|
||||||
|
};
|
||||||
|
const copyContent = function () {
|
||||||
|
$copyToClipboard(text.value);
|
||||||
|
};
|
||||||
|
const changeTemplate = function () {
|
||||||
|
let copy = pageData;
|
||||||
|
let found = copy.fields.find((v) => v.name === field.name);
|
||||||
|
found.template = text.value;
|
||||||
|
store.commit(props.pagename, copy);
|
||||||
|
};
|
||||||
|
const checkExpression = function () {
|
||||||
|
errors = [];
|
||||||
|
let val = $copy(expression);
|
||||||
|
let exp = $copy(expression);
|
||||||
|
tagsField.forEach((v) => {
|
||||||
|
let myRegExp = new RegExp(v.name, "g");
|
||||||
|
val = val.replace(myRegExp, Math.random());
|
||||||
|
exp = exp.replace(myRegExp, "formatNumber(row['" + v.name + "'])");
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
let value = $calc(val);
|
||||||
|
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
|
||||||
|
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
|
||||||
|
} else if (!(eval(value) === true || eval(value) === false)) {
|
||||||
|
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
|
||||||
|
} else if (selected) {
|
||||||
|
selected.expression = exp;
|
||||||
|
selected.formula = expression;
|
||||||
|
selected.tags = $copy(tagsField);
|
||||||
}
|
}
|
||||||
const replace = function() {
|
} catch (err) {
|
||||||
if($empty(text.value)) return
|
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
|
||||||
text.value =text.value.replaceAll(source,target)
|
}
|
||||||
}
|
returnerrors.length > 0 ? false : true;
|
||||||
const doCheck = function() {
|
};
|
||||||
let text = window.getSelection().toString()
|
const changeType = function (v) {};
|
||||||
if($empty(text)) return
|
const doSelect = function (v) {
|
||||||
source = text
|
tags.push({ id: $id(), name: v.name, class: getClass(v) });
|
||||||
}
|
tab = tabs.find((v) => v.code === "selected");
|
||||||
const changeStyle = function() {
|
selected = tags[tags.length - 1];
|
||||||
selected.bgcolor =selected.color =selected.textsize =selected.style = undefined
|
};
|
||||||
let style = ''
|
const doSelectSpan = function (v) {
|
||||||
if(radioBGcolor.code==='option'? !$empty(bgcolor) : false) {
|
tags.push({ id: $id(), name: v.name, class: getSpanClass(v) });
|
||||||
selected.bgcolor =bgcolor
|
tab = tabs.find((v) => v.code === "selected");
|
||||||
style += 'background-color: ' +bgcolor + ' !important; '
|
selected = tags[tags.length - 1];
|
||||||
}
|
};
|
||||||
if(radioColor.code==='option'? !$empty(color) : false) {
|
const remove = function (i) {
|
||||||
selected.color =color
|
$remove(tags, i);
|
||||||
style += 'color: ' +color + ' !important; '
|
};
|
||||||
}
|
const getClass = function (v) {
|
||||||
if(radioSize.code==='option'?$isNumber(textsize) : false) {
|
let value = type.code + " " + v.code + " " + size.code + (shape.code === "default" ? "" : " " + shape.code);
|
||||||
selected.textsize =textsize
|
value += outline.code === "default" ? "" : " " + outline.code;
|
||||||
style += 'font-size: ' +textsize + 'px !important; '
|
return value;
|
||||||
}
|
};
|
||||||
$empty(style)? false :selected.style = style
|
const getSpanClass = function (v) {
|
||||||
}
|
let value = "has-text-" + v.name.toLowerCase() + " " + size.value;
|
||||||
const changeCondition = function(v) {
|
return value;
|
||||||
if(v.code==='no')selected.expression = undefined
|
};
|
||||||
}
|
initData();
|
||||||
const copyContent = function() {
|
var docid = $id();
|
||||||
$copyToClipboard(text.value)
|
</script>
|
||||||
}
|
|
||||||
const changeTemplate = function() {
|
|
||||||
let copy = pageData
|
|
||||||
let found = copy.fields.find(v=>v.name===field.name)
|
|
||||||
found.template = text.value
|
|
||||||
store.commit(props.pagename, copy)
|
|
||||||
}
|
|
||||||
const checkExpression = function() {
|
|
||||||
errors = []
|
|
||||||
let val =$copy(expression)
|
|
||||||
let exp =$copy(expression)
|
|
||||||
tagsField.forEach(v => {
|
|
||||||
let myRegExp = new RegExp(v.name, 'g')
|
|
||||||
val = val.replace(myRegExp, Math.random())
|
|
||||||
exp = exp.replace(myRegExp, "formatNumber(row['" + v.name + "'])")
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
let value =$calc(val)
|
|
||||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
|
||||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
|
||||||
} else if(!(eval(value)===true || eval(value)===false)) {
|
|
||||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
|
||||||
} else if(selected) {
|
|
||||||
selected.expression = exp
|
|
||||||
selected.formula =expression
|
|
||||||
selected.tags =$copy(tagsField)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
|
||||||
}
|
|
||||||
returnerrors.length>0? false : true
|
|
||||||
}
|
|
||||||
const changeType = function(v) {
|
|
||||||
}
|
|
||||||
const doSelect = function(v) {
|
|
||||||
tags.push({id:$id(), name: v.name, class:getClass(v)})
|
|
||||||
tab =tabs.find(v=>v.code==='selected')
|
|
||||||
selected =tags[tags.length-1]
|
|
||||||
}
|
|
||||||
const doSelectSpan = function(v) {
|
|
||||||
tags.push({id:$id(), name: v.name, class:getSpanClass(v)})
|
|
||||||
tab =tabs.find(v=>v.code==='selected')
|
|
||||||
selected =tags[tags.length-1]
|
|
||||||
}
|
|
||||||
const remove = function(i) {
|
|
||||||
$remove(tags, i)
|
|
||||||
}
|
|
||||||
const getClass = function(v) {
|
|
||||||
let value =type.code + ' ' + v.code + ' ' +size.code + (shape.code==='default'? '' : ' ' +shape.code)
|
|
||||||
value += (outline.code==='default'? '' : ' ' +outline.code)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
const getSpanClass = function(v) {
|
|
||||||
let value = 'has-text-' + v.name.toLowerCase() + ' ' +size.value
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
initData()
|
|
||||||
var docid = $id()
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,193 +1,314 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="columns mx-0">
|
<div class="columns mx-0">
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<Caption class="mb-2" v-bind="{title: 'Tên model (bảng)', type: 'has-text-warning'}"></Caption>
|
<Caption
|
||||||
<div class="mb-2">
|
class="mb-2"
|
||||||
<input class="input" v-model="text" placeholder="Tìm model" @change="findModel()">
|
v-bind="{ title: 'Tên model (bảng)', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<div class="mb-2">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
v-model="text"
|
||||||
|
placeholder="Tìm model"
|
||||||
|
@change="findModel()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style="max-height: 80vh; overflow: auto">
|
||||||
|
<div
|
||||||
|
:class="`py-1 border-bottom is-clickable ${current.model === v.model ? 'has-background-primary has-text-white' : ''}`"
|
||||||
|
v-for="v in displayData"
|
||||||
|
@click="changeMenu(v)"
|
||||||
|
>
|
||||||
|
{{ v.model }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="max-height: 80vh; overflow: auto;">
|
<div class="column is-10 py-0 px-0">
|
||||||
<div :class="`py-1 border-bottom is-clickable ${current.model===v.model? 'has-background-primary has-text-white' : ''}`"
|
<div class="tabs mb-3">
|
||||||
v-for="v in displayData" @click="changeMenu(v)">
|
<ul>
|
||||||
{{ v.model}}
|
<li
|
||||||
</div>
|
:class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`"
|
||||||
</div>
|
v-for="v in tabs"
|
||||||
</div>
|
>
|
||||||
<div class="column is-10 py-0 px-0">
|
<a @click="changeTab(v)">{{ v.name }}</a>
|
||||||
<div class="tabs mb-3">
|
</li>
|
||||||
<ul>
|
</ul>
|
||||||
<li :class="`${v.code===tab? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs">
|
</div>
|
||||||
<a @click="changeTab(v)">{{v.name}}</a>
|
<div v-if="tab === 'datatype'">
|
||||||
</li>
|
<Caption
|
||||||
</ul>
|
class="mb-2"
|
||||||
</div>
|
v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"
|
||||||
<div v-if="tab==='datatype'">
|
></Caption>
|
||||||
<Caption class="mb-2" v-bind="{title: 'Kiểu dữ liệu (type)', type: 'has-text-warning'}"></Caption>
|
<div style="max-height: 75vh; overflow-y: auto">
|
||||||
<div style="max-height:75vh; overflow-y: auto;">
|
<div
|
||||||
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields">
|
class="py-1 border-bottom is-clickable"
|
||||||
{{ x.name}}
|
v-for="x in current.fields"
|
||||||
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
>
|
||||||
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a>
|
{{ x.name }}
|
||||||
|
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
||||||
|
<a
|
||||||
|
class="ml-6 has-text-primary"
|
||||||
|
v-if="x.model"
|
||||||
|
@click="openModel(x)"
|
||||||
|
>{{ x.model }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else-if="tab === 'table'">
|
||||||
|
<div class="columns mx-0 mb-0 pb-0">
|
||||||
|
<div class="column is-5">
|
||||||
|
<Caption
|
||||||
|
class="mb-1"
|
||||||
|
v-bind="{ title: 'Values', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
rows="1"
|
||||||
|
v-model="values"
|
||||||
|
placeholder="Tên trường không chứa dấu cách, vd: code,name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="column is-4">
|
||||||
|
<Caption
|
||||||
|
class="mb-1"
|
||||||
|
v-bind="{ title: 'Filter', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
rows="1"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="{'code': 'xyz'}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="column is-2">
|
||||||
|
<Caption
|
||||||
|
class="mb-1"
|
||||||
|
v-bind="{ title: 'Sort', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
rows="1"
|
||||||
|
v-model="sort"
|
||||||
|
placeholder="vd: -code,name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="column is-1">
|
||||||
|
<Caption
|
||||||
|
class="mb-1"
|
||||||
|
v-bind="{ title: 'Load', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="loadData()"
|
||||||
|
>
|
||||||
|
Load
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Caption
|
||||||
|
class="mb-1"
|
||||||
|
v-bind="{ title: 'Query', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<div class="mb-2">
|
||||||
|
{{ query }}
|
||||||
|
<a
|
||||||
|
class="has-text-primary ml-5"
|
||||||
|
@click="copy()"
|
||||||
|
>copy</a
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ apiUrl }}
|
||||||
|
<a
|
||||||
|
class="has-text-primary ml-5"
|
||||||
|
@click="$copyToClipboard(apiUrl)"
|
||||||
|
>copy</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="has-text-primary ml-5"
|
||||||
|
target="_blank"
|
||||||
|
:href="apiUrl"
|
||||||
|
>open</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DataTable
|
||||||
|
v-bind="{ pagename: pagename }"
|
||||||
|
v-if="pagedata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else>
|
||||||
|
<img
|
||||||
|
id="image"
|
||||||
|
:src="filePath"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<p class="pl-5">
|
||||||
|
<a
|
||||||
|
class="mr-5"
|
||||||
|
@click="downloadFile()"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'download.svg', type: 'black', size: 24 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
:href="filePath"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'open.svg', type: 'black', size: 24 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Modal
|
||||||
<template v-else-if="tab==='table'">
|
@close="showmodal = undefined"
|
||||||
<div class="columns mx-0 mb-0 pb-0">
|
v-bind="showmodal"
|
||||||
<div class="column is-5">
|
v-if="showmodal"
|
||||||
<Caption class="mb-1" v-bind="{title: 'Values', type: 'has-text-warning'}"></Caption>
|
></Modal>
|
||||||
<input class="input" rows="1" v-model="values" placeholder="Tên trường không chứa dấu cách, vd: code,name">
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<Caption class="mb-1" v-bind="{title: 'Filter', type: 'has-text-warning'}"></Caption>
|
|
||||||
<input class="input" rows="1" v-model="filter" placeholder="{'code': 'xyz'}">
|
|
||||||
</div>
|
|
||||||
<div class="column is-2">
|
|
||||||
<Caption class="mb-1" v-bind="{title: 'Sort', type: 'has-text-warning'}"></Caption>
|
|
||||||
<input class="input" rows="1" v-model="sort" placeholder="vd: -code,name">
|
|
||||||
</div>
|
|
||||||
<div class="column is-1">
|
|
||||||
<Caption class="mb-1" v-bind="{title: 'Load', type: 'has-text-warning'}"></Caption>
|
|
||||||
<div>
|
|
||||||
<button class="button is-primary has-text-white" @click="loadData()">Load</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Caption class="mb-1" v-bind="{title: 'Query', type: 'has-text-warning'}"></Caption>
|
|
||||||
<div class="mb-2">
|
|
||||||
{{ query }}
|
|
||||||
<a class="has-text-primary ml-5" @click="copy()">copy</a>
|
|
||||||
<p>{{apiUrl}}
|
|
||||||
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a>
|
|
||||||
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DataTable v-bind="{pagename: pagename}" v-if="pagedata" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-else>
|
|
||||||
<img id="image" :src="filePath" alt="">
|
|
||||||
<p class="pl-5">
|
|
||||||
<a class="mr-5" @click="downloadFile()">
|
|
||||||
<SvgIcon v-bind="{name: 'download.svg', type: 'black', size: 24}"></SvgIcon>
|
|
||||||
</a>
|
|
||||||
<a target="_blank" :href="filePath">
|
|
||||||
<SvgIcon v-bind="{name: 'open.svg', type: 'black', size: 24}"></SvgIcon>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStore } from '@/stores/index'
|
import { useStore } from "@/stores/index";
|
||||||
const { $getdata, $getapi, $createField, $clone, $getpage, $empty, $copyToClipboard, $find, $multiSort, $download, $getpath } = useNuxtApp()
|
const {
|
||||||
const store = useStore()
|
$getdata,
|
||||||
var pagename = 'pagedata3'
|
$getapi,
|
||||||
var pagedata = ref()
|
$createField,
|
||||||
pagedata.value = $getpage()
|
$clone,
|
||||||
pagedata.value.perPage = 10
|
$getpage,
|
||||||
store.commit(pagename, pagedata)
|
$empty,
|
||||||
let list = ['LogEntry', 'Permission', 'ContentType', 'Session', 'Group']
|
$copyToClipboard,
|
||||||
var data = (await $getdata('getmodel')).filter(v=>list.findIndex(x=>x===v.model)<0)
|
$find,
|
||||||
data = $multiSort(data, {model: 'asc'})
|
$multiSort,
|
||||||
var current = ref({fields: []})
|
$download,
|
||||||
var tabs = [{code: 'datatype', name: 'Kiểu dữ liệu'}, {code: 'table', name: 'Dữ liệu'}, {code: 'datamodel', name: 'Data model'}]
|
$getpath,
|
||||||
var tab = ref('datatype')
|
} = useNuxtApp();
|
||||||
var datatable = ref()
|
const store = useStore();
|
||||||
var query = ref()
|
var pagename = "pagedata3";
|
||||||
var values, filter
|
var pagedata = ref();
|
||||||
var apiUrl = ref()
|
pagedata.value = $getpage();
|
||||||
var showmodal = ref()
|
pagedata.value.perPage = 10;
|
||||||
var text = null
|
store.commit(pagename, pagedata);
|
||||||
var displayData = ref(data)
|
let list = ["LogEntry", "Permission", "ContentType", "Session", "Group"];
|
||||||
var filePath = `${$getpath()}static/files/datamodel.png`
|
var data = (await $getdata("getmodel")).filter((v) => list.findIndex((x) => x === v.model) < 0);
|
||||||
var sort = "-id"
|
data = $multiSort(data, { model: "asc" });
|
||||||
current.value = data[0]
|
var current = ref({ fields: [] });
|
||||||
|
var tabs = [
|
||||||
|
{ code: "datatype", name: "Kiểu dữ liệu" },
|
||||||
|
{ code: "table", name: "Dữ liệu" },
|
||||||
|
{ code: "datamodel", name: "Data model" },
|
||||||
|
];
|
||||||
|
var tab = ref("datatype");
|
||||||
|
var datatable = ref();
|
||||||
|
var query = ref();
|
||||||
|
var values, filter;
|
||||||
|
var apiUrl = ref();
|
||||||
|
var showmodal = ref();
|
||||||
|
var text = null;
|
||||||
|
var displayData = ref(data);
|
||||||
|
var filePath = `${$getpath()}static/files/datamodel.png`;
|
||||||
|
var sort = "-id";
|
||||||
|
current.value = data[0];
|
||||||
function changeMenu(v) {
|
function changeMenu(v) {
|
||||||
values = undefined
|
values = undefined;
|
||||||
filter = undefined
|
filter = undefined;
|
||||||
sort = undefined
|
sort = undefined;
|
||||||
current.value = v
|
current.value = v;
|
||||||
if(tab.value==='table') loadData()
|
if (tab.value === "table") loadData();
|
||||||
}
|
}
|
||||||
async function changeTab(v) {
|
async function changeTab(v) {
|
||||||
tab.value = v.code
|
tab.value = v.code;
|
||||||
if(v.code==='table') loadData()
|
if (v.code === "table") loadData();
|
||||||
}
|
}
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
let vfilter = filter? filter.trim() : undefined
|
let vfilter = filter ? filter.trim() : undefined;
|
||||||
if(vfilter) {
|
if (vfilter) {
|
||||||
try {
|
try {
|
||||||
vfilter = JSON.parse(vfilter)
|
vfilter = JSON.parse(vfilter);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Cấu trúc filter có lỗi')
|
alert("Cấu trúc filter có lỗi");
|
||||||
vfilter = undefined
|
vfilter = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let params = {values: $empty(values)? undefined : values.trim(), filter: filter, sort: $empty(sort)? undefined : sort.trim()}
|
let params = {
|
||||||
let modelName = current.value.model
|
values: $empty(values) ? undefined : values.trim(),
|
||||||
let found = {name: modelName.toLowerCase().replace('_', ''), url: `data/${modelName}/`, url_detail: `data-detail/${modelName}/`, params: params}
|
filter: filter,
|
||||||
query.value = $clone(found)
|
sort: $empty(sort) ? undefined : sort.trim(),
|
||||||
let rs = await $getapi([found])
|
};
|
||||||
if(rs==='error') return alert('Đã xảy ra lỗi, hãy xem lại câu lệnh.')
|
let modelName = current.value.model;
|
||||||
datatable.value = rs[0].data.rows
|
let found = {
|
||||||
showData()
|
name: modelName.toLowerCase().replace("_", ""),
|
||||||
|
url: `data/${modelName}/`,
|
||||||
|
url_detail: `data-detail/${modelName}/`,
|
||||||
|
params: params,
|
||||||
|
};
|
||||||
|
query.value = $clone(found);
|
||||||
|
let rs = await $getapi([found]);
|
||||||
|
if (rs === "error") return alert("Đã xảy ra lỗi, hãy xem lại câu lệnh.");
|
||||||
|
datatable.value = rs[0].data.rows;
|
||||||
|
showData();
|
||||||
|
|
||||||
// api query
|
// api query
|
||||||
const baseUrl = $getpath() + `${query.value.url}`
|
const baseUrl = $getpath() + `${query.value.url}`;
|
||||||
apiUrl.value = baseUrl
|
apiUrl.value = baseUrl;
|
||||||
let vparams = !$empty(values)? {values: values} : null
|
let vparams = !$empty(values) ? { values: values } : null;
|
||||||
if(!$empty(filter)) {
|
if (!$empty(filter)) {
|
||||||
vparams = vparams? {values: values, filter:filter} : {filter:filter}
|
vparams = vparams ? { values: values, filter: filter } : { filter: filter };
|
||||||
}
|
}
|
||||||
if(!$empty(sort)) {
|
if (!$empty(sort)) {
|
||||||
if(vparams) {
|
if (vparams) {
|
||||||
vparams.sort = sort.trim()
|
vparams.sort = sort.trim();
|
||||||
} else {
|
} else {
|
||||||
vparams = {sort: sort.trim()}
|
vparams = { sort: sort.trim() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(vparams) {
|
if (vparams) {
|
||||||
let url = new URL(baseUrl);
|
let url = new URL(baseUrl);
|
||||||
let searchParams = new URLSearchParams(vparams);
|
let searchParams = new URLSearchParams(vparams);
|
||||||
url.search = searchParams.toString();
|
url.search = searchParams.toString();
|
||||||
apiUrl.value = baseUrl + url.search
|
apiUrl.value = baseUrl + url.search;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function showData() {
|
function showData() {
|
||||||
let arr = []
|
let arr = [];
|
||||||
if(!$empty(values)) {
|
if (!$empty(values)) {
|
||||||
let arr1 = values.trim().split(',')
|
let arr1 = values.trim().split(",");
|
||||||
arr1.map(v=>{
|
arr1.map((v) => {
|
||||||
let val = v.trim()
|
let val = v.trim();
|
||||||
let field = $createField(val, val, 'string', true)
|
let field = $createField(val, val, "string", true);
|
||||||
arr.push(field)
|
arr.push(field);
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
current.value.fields.map(v=>{
|
current.value.fields.map((v) => {
|
||||||
let field = $createField(v.name, v.name, 'string', true)
|
let field = $createField(v.name, v.name, "string", true);
|
||||||
arr.push(field)
|
arr.push(field);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
let clone = $clone(pagedata.value)
|
let clone = $clone(pagedata.value);
|
||||||
clone.fields = arr
|
clone.fields = arr;
|
||||||
clone.data = datatable.value
|
clone.data = datatable.value;
|
||||||
pagedata.value = undefined
|
pagedata.value = undefined;
|
||||||
setTimeout(()=>pagedata.value = clone)
|
setTimeout(() => (pagedata.value = clone));
|
||||||
}
|
}
|
||||||
function copy() {
|
function copy() {
|
||||||
$copyToClipboard(JSON.stringify(query.value))
|
$copyToClipboard(JSON.stringify(query.value));
|
||||||
}
|
}
|
||||||
function openModel(x) {
|
function openModel(x) {
|
||||||
showmodal.value = {component: 'datatable/ModelInfo', title: x.model, width: '70%', height: '600px',
|
showmodal.value = {
|
||||||
vbind: {data: data, info: $find(data, {model: x.model})}}
|
component: "datatable/ModelInfo",
|
||||||
|
title: x.model,
|
||||||
|
width: "70%",
|
||||||
|
height: "600px",
|
||||||
|
vbind: { data: data, info: $find(data, { model: x.model }) },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function downloadFile() {
|
function downloadFile() {
|
||||||
$download(`${$getpath()}download/?name=datamodel.png&type=file`, 'datamodel.png')
|
$download(`${$getpath()}download/?name=datamodel.png&type=file`, "datamodel.png");
|
||||||
}
|
}
|
||||||
function findModel() {
|
function findModel() {
|
||||||
if($empty(text)) return displayData.value = data
|
if ($empty(text)) return (displayData.value = data);
|
||||||
displayData.value = data.filter(v=>v.model.toLowerCase().indexOf(text.toLowerCase())>=0)
|
displayData.value = data.filter((v) => v.model.toLowerCase().indexOf(text.toLowerCase()) >= 0);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,57 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="field is-grouped is-grouped-multiline pl-2" v-if="filters? filters.length>0 : false">
|
<div
|
||||||
<div class="control mr-5">
|
class="field is-grouped is-grouped-multiline pl-2"
|
||||||
<a class="button is-primary is-small has-text-white has-text-weight-bold" @click="updateData({filters: []})">
|
v-if="filters ? filters.length > 0 : false"
|
||||||
<span class="fs-14">Xóa lọc</span>
|
>
|
||||||
</a>
|
<div class="control mr-5">
|
||||||
</div>
|
<a
|
||||||
<div class="control pr-2 mr-5">
|
class="button is-primary is-small has-text-white has-text-weight-bold"
|
||||||
<span class="icon-text">
|
@click="updateData({ filters: [] })"
|
||||||
<SvgIcon v-bind="{name: 'sigma.svg', type: 'primary', size: 20}"></SvgIcon>
|
>
|
||||||
<span class="fsb-18 has-text-primary">{{totalRows}}</span>
|
<span class="fs-14">Xóa lọc</span>
|
||||||
</span>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="control" v-for="(v,i) in filters" :key="i">
|
<div class="control pr-2 mr-5">
|
||||||
<div class="tags has-addons is-marginless">
|
<span class="icon-text">
|
||||||
<a class="tag is-primary has-text-white is-marginless" @click="showCondition(v)">{{v.label.indexOf('>')>=0? $stripHtml(v.label,30) : v.label}}</a>
|
<SvgIcon v-bind="{ name: 'sigma.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||||
<a class="tag is-delete is-marginless has-text-black-bis" @click="removeFilter(i)"></a>
|
<span class="fsb-18 has-text-primary">{{ totalRows }}</span>
|
||||||
</div>
|
</span>
|
||||||
<span class="help has-text-black-bis">
|
</div>
|
||||||
{{v.sort? v.sort : (v.select? ('[' + (v.select.length>0? $stripHtml(v.select[0],20) : '') + '...Σ' + v.select.length + ']') :
|
<div
|
||||||
(v.condition))}}</span>
|
class="control"
|
||||||
</div>
|
v-for="(v, i) in filters"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<div class="tags has-addons is-marginless">
|
||||||
|
<a
|
||||||
|
class="tag is-primary has-text-white is-marginless"
|
||||||
|
@click="showCondition(v)"
|
||||||
|
>{{ v.label.indexOf(">") >= 0 ? $stripHtml(v.label, 30) : v.label }}</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="tag is-delete is-marginless has-text-black-bis"
|
||||||
|
@click="removeFilter(i)"
|
||||||
|
></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container mb-0" ref="container" id="docid">
|
<span class="help has-text-black-bis">
|
||||||
<table class="table is-fullwidth is-bordered is-narrow is-hoverable" :style="tableStyle">
|
{{
|
||||||
<thead>
|
v.sort
|
||||||
<tr>
|
? v.sort
|
||||||
<th v-for="(field,i) in displayFields" :key="i" :style="field.headerStyle">
|
: v.select
|
||||||
<div @click="showField(field)" :style="field.dropStyle">
|
? "[" + (v.select.length > 0 ? $stripHtml(v.select[0], 20) : "") + "...Σ" + v.select.length + "]"
|
||||||
<a v-if="field.label.indexOf('<')<0">{{field.label}}</a>
|
: v.condition
|
||||||
<a v-else>
|
}}</span
|
||||||
<component :is="dynamicComponent(field.label)" :row="v" @clickevent="clickEvent($event, v, field)" />
|
>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
<div
|
||||||
</tr>
|
class="table-container mb-0"
|
||||||
</thead>
|
ref="container"
|
||||||
<tbody>
|
id="docid"
|
||||||
<tr v-for="(v,i) in displayData" :key="i">
|
>
|
||||||
<td
|
<table
|
||||||
v-for="(field, j) in displayFields"
|
class="table is-fullwidth is-bordered is-narrow is-hoverable"
|
||||||
:key="j"
|
:style="tableStyle"
|
||||||
:id="field.name"
|
>
|
||||||
:style="v[`${field.name}color`]"
|
<thead>
|
||||||
style="
|
<tr>
|
||||||
overflow: hidden;
|
<th
|
||||||
text-overflow: ellipsis;
|
v-for="(field, i) in displayFields"
|
||||||
"
|
:key="i"
|
||||||
@dblclick="doubleClick(field, v)">
|
:style="field.headerStyle"
|
||||||
<component :is="dynamicComponent(field.template)" :row="v" v-if="field.template" @clickevent="clickEvent($event, v, field)" />
|
>
|
||||||
<span v-else>{{ v[field.name] }}</span>
|
<div
|
||||||
</td>
|
@click="showField(field)"
|
||||||
</tr>
|
:style="field.dropStyle"
|
||||||
</tbody>
|
>
|
||||||
|
<a v-if="field.label.indexOf('<') < 0">{{ field.label }}</a>
|
||||||
|
<a v-else>
|
||||||
|
<component
|
||||||
|
:is="dynamicComponent(field.label)"
|
||||||
|
:row="v"
|
||||||
|
@clickevent="clickEvent($event, v, field)"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(v, i) in displayData"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
v-for="(field, j) in displayFields"
|
||||||
|
:key="j"
|
||||||
|
:id="field.name"
|
||||||
|
:style="v[`${field.name}color`]"
|
||||||
|
style="overflow: hidden; text-overflow: ellipsis"
|
||||||
|
@dblclick="doubleClick(field, v)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="dynamicComponent(field.template)"
|
||||||
|
:row="v"
|
||||||
|
v-if="field.template"
|
||||||
|
@clickevent="clickEvent($event, v, field)"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ v[field.name] }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<DatatablePagination
|
<DatatablePagination
|
||||||
v-bind="{ data: data, perPage: perPage }"
|
v-bind="{ data: data, perPage: perPage }"
|
||||||
@@ -124,7 +171,7 @@ watch(
|
|||||||
() => store[props.pagename],
|
() => store[props.pagename],
|
||||||
(newVal, oldVal) => {
|
(newVal, oldVal) => {
|
||||||
updateChange();
|
updateChange();
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
function updateChange() {
|
function updateChange() {
|
||||||
pagedata = store[props.pagename];
|
pagedata = store[props.pagename];
|
||||||
@@ -141,24 +188,21 @@ function updateChange() {
|
|||||||
const updateShow = function (full_data) {
|
const updateShow = function (full_data) {
|
||||||
// allowed JS expressions - should return a boolean
|
// allowed JS expressions - should return a boolean
|
||||||
const allowedFns = {
|
const allowedFns = {
|
||||||
'$getEditRights()': $getEditRights,
|
"$getEditRights()": $getEditRights,
|
||||||
};
|
};
|
||||||
const arr = pagedata.fields.filter(({ show }) => {
|
const arr = pagedata.fields.filter(({ show }) => {
|
||||||
if (typeof show === 'boolean') return show;
|
if (typeof show === "boolean") return show;
|
||||||
else {
|
else {
|
||||||
// show is a string
|
// show is a string
|
||||||
if (show === 'true') return true;
|
if (show === "true") return true;
|
||||||
if (show === 'false') return false;
|
if (show === "false") return false;
|
||||||
return allowedFns[show]?.() || false;
|
return allowedFns[show]?.() || false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (full_data === false) displayData = $copy(data);
|
if (full_data === false) displayData = $copy(data);
|
||||||
else
|
else
|
||||||
displayData = $copy(
|
displayData = $copy(
|
||||||
data.filter(
|
data.filter((ele, index) => index >= (currentPage - 1) * perPage && index < currentPage * perPage),
|
||||||
(ele, index) =>
|
|
||||||
index >= (currentPage - 1) * perPage && index < currentPage * perPage
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
displayData.map((v) => {
|
displayData.map((v) => {
|
||||||
arr.map((x) => (v[`${x.name}color`] = getStyle(x, v)));
|
arr.map((x) => (v[`${x.name}color`] = getStyle(x, v)));
|
||||||
@@ -222,9 +266,7 @@ const getStyle = function (field, record) {
|
|||||||
field.bgcolor.map((v) => {
|
field.bgcolor.map((v) => {
|
||||||
if (v.type === "search") {
|
if (v.type === "search") {
|
||||||
if (
|
if (
|
||||||
record[field.name] && !stop
|
record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
|
||||||
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
|
|
||||||
: false
|
|
||||||
) {
|
) {
|
||||||
val += ` background-color:${v.color}; `;
|
val += ` background-color:${v.color}; `;
|
||||||
stop = true;
|
stop = true;
|
||||||
@@ -245,9 +287,7 @@ const getStyle = function (field, record) {
|
|||||||
field.color.map((v) => {
|
field.color.map((v) => {
|
||||||
if (v.type === "search") {
|
if (v.type === "search") {
|
||||||
if (
|
if (
|
||||||
record[field.name] && !stop
|
record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
|
||||||
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
|
|
||||||
: false
|
|
||||||
) {
|
) {
|
||||||
val += ` color:${v.color}; `;
|
val += ` color:${v.color}; `;
|
||||||
stop = true;
|
stop = true;
|
||||||
@@ -268,9 +308,7 @@ const getStyle = function (field, record) {
|
|||||||
field.textsize.map((v) => {
|
field.textsize.map((v) => {
|
||||||
if (v.type === "search") {
|
if (v.type === "search") {
|
||||||
if (
|
if (
|
||||||
record[field.name] && !stop
|
record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
|
||||||
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
|
|
||||||
: false
|
|
||||||
) {
|
) {
|
||||||
val += ` font-size:${v.size}px; `;
|
val += ` font-size:${v.size}px; `;
|
||||||
stop = true;
|
stop = true;
|
||||||
@@ -283,10 +321,7 @@ const getStyle = function (field, record) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else
|
} else val += ` font-size:${tablesetting.find((v) => v.code === "table-font-size").detail}px;`;
|
||||||
val += ` font-size:${
|
|
||||||
tablesetting.find((v) => v.code === "table-font-size").detail
|
|
||||||
}px;`;
|
|
||||||
if (field.textalign) val += ` text-align:${field.textalign}; `;
|
if (field.textalign) val += ` text-align:${field.textalign}; `;
|
||||||
if (field.minwidth) val += ` min-width:${field.minwidth}px; `;
|
if (field.minwidth) val += ` min-width:${field.minwidth}px; `;
|
||||||
if (field.maxwidth) val += ` max-width:${field.maxwidth}px; `;
|
if (field.maxwidth) val += ` max-width:${field.maxwidth}px; `;
|
||||||
@@ -295,56 +330,28 @@ const getStyle = function (field, record) {
|
|||||||
const getSettingStyle = function (name, field) {
|
const getSettingStyle = function (name, field) {
|
||||||
let value = "";
|
let value = "";
|
||||||
if (name === "container") {
|
if (name === "container") {
|
||||||
value =
|
value = "min-height:" + tablesetting.find((v) => v.code === "container-height").detail + "rem; ";
|
||||||
"min-height:" +
|
|
||||||
tablesetting.find((v) => v.code === "container-height").detail +
|
|
||||||
"rem; ";
|
|
||||||
} else if (name === "table") {
|
} else if (name === "table") {
|
||||||
value +=
|
value += "background-color:" + tablesetting.find((v) => v.code === "table-background").detail + "; ";
|
||||||
"background-color:" +
|
value += "font-size:" + tablesetting.find((v) => v.code === "table-font-size").detail + "px;";
|
||||||
tablesetting.find((v) => v.code === "table-background").detail +
|
value += "color:" + tablesetting.find((v) => v.code === "table-font-color").detail + "; ";
|
||||||
"; ";
|
|
||||||
value +=
|
|
||||||
"font-size:" +
|
|
||||||
tablesetting.find((v) => v.code === "table-font-size").detail +
|
|
||||||
"px;";
|
|
||||||
value +=
|
|
||||||
"color:" + tablesetting.find((v) => v.code === "table-font-color").detail + "; ";
|
|
||||||
} else if (name === "header") {
|
} else if (name === "header") {
|
||||||
value +=
|
value += "background-color:" + tablesetting.find((v) => v.code === "header-background").detail + "; ";
|
||||||
"background-color:" +
|
|
||||||
tablesetting.find((v) => v.code === "header-background").detail +
|
|
||||||
"; ";
|
|
||||||
if (field.minwidth) value += " min-width: " + field.minwidth + "px; ";
|
if (field.minwidth) value += " min-width: " + field.minwidth + "px; ";
|
||||||
if (field.maxwidth) value += " max-width: " + field.maxwidth + "px; ";
|
if (field.maxwidth) value += " max-width: " + field.maxwidth + "px; ";
|
||||||
} else if (name === "menu") {
|
} else if (name === "menu") {
|
||||||
let arg = tablesetting.find((v) => v.code === "menu-width").detail;
|
let arg = tablesetting.find((v) => v.code === "menu-width").detail;
|
||||||
arg = field ? (field.menuwidth ? field.menuwidth : arg) : arg;
|
arg = field ? (field.menuwidth ? field.menuwidth : arg) : arg;
|
||||||
value += "width:" + arg + "rem; ";
|
value += "width:" + arg + "rem; ";
|
||||||
value +=
|
value += "min-height:" + tablesetting.find((v) => v.code === "menu-min-height").detail + "rem; ";
|
||||||
"min-height:" +
|
value += "max-height:" + tablesetting.find((v) => v.code === "menu-max-height").detail + "rem; ";
|
||||||
tablesetting.find((v) => v.code === "menu-min-height").detail +
|
|
||||||
"rem; ";
|
|
||||||
value +=
|
|
||||||
"max-height:" +
|
|
||||||
tablesetting.find((v) => v.code === "menu-max-height").detail +
|
|
||||||
"rem; ";
|
|
||||||
value += "overflow:auto; ";
|
value += "overflow:auto; ";
|
||||||
} else if (name === "dropdown") {
|
} else if (name === "dropdown") {
|
||||||
value +=
|
value += "font-size:" + tablesetting.find((v) => v.code === "header-font-size").detail + "px; ";
|
||||||
"font-size:" +
|
|
||||||
tablesetting.find((v) => v.code === "header-font-size").detail +
|
|
||||||
"px; ";
|
|
||||||
let found = filters.find((v) => v.name === field.name);
|
let found = filters.find((v) => v.name === field.name);
|
||||||
found
|
found
|
||||||
? (value +=
|
? (value += "color:" + tablesetting.find((v) => v.code === "header-filter-color").detail + "; ")
|
||||||
"color:" +
|
: (value += "color:" + tablesetting.find((v) => v.code === "header-font-color").detail + "; ");
|
||||||
tablesetting.find((v) => v.code === "header-filter-color").detail +
|
|
||||||
"; ")
|
|
||||||
: (value +=
|
|
||||||
"color:" +
|
|
||||||
tablesetting.find((v) => v.code === "header-font-color").detail +
|
|
||||||
"; ");
|
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
@@ -369,12 +376,8 @@ const frontendFilter = function (newVal) {
|
|||||||
else {
|
else {
|
||||||
let text = "";
|
let text = "";
|
||||||
filter.map((y, k) => {
|
filter.map((y, k) => {
|
||||||
text += `${
|
text += `${k > 0 ? (filter[k - 1].operator === "and" ? " &&" : " ||") : ""} ${$formatNumber(x[name])}
|
||||||
k > 0 ? (filter[k - 1].operator === "and" ? " &&" : " ||") : ""
|
${y.condition === "=" ? "==" : y.condition === "<>" ? "!==" : y.condition} ${$formatNumber(y.value)}`;
|
||||||
} ${$formatNumber(x[name])}
|
|
||||||
${
|
|
||||||
y.condition === "=" ? "==" : y.condition === "<>" ? "!==" : y.condition
|
|
||||||
} ${$formatNumber(y.value)}`;
|
|
||||||
});
|
});
|
||||||
return $calc(text);
|
return $calc(text);
|
||||||
}
|
}
|
||||||
@@ -385,11 +388,7 @@ const frontendFilter = function (newVal) {
|
|||||||
.filter((m) => m.select || m.filter)
|
.filter((m) => m.select || m.filter)
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
if (v.select) {
|
if (v.select) {
|
||||||
data = data.filter(
|
data = data.filter((x) => v.select.findIndex((y) => ($empty(y) ? $empty(x[v.name]) : y === x[v.name])) > -1);
|
||||||
(x) =>
|
|
||||||
v.select.findIndex((y) => ($empty(y) ? $empty(x[v.name]) : y === x[v.name])) >
|
|
||||||
-1
|
|
||||||
);
|
|
||||||
} else if (v.filter) {
|
} else if (v.filter) {
|
||||||
data = data.filter((x) => checkValid(v.name, x, v.filter));
|
data = data.filter((x) => checkValid(v.name, x, v.filter));
|
||||||
}
|
}
|
||||||
@@ -503,9 +502,7 @@ const updateData = async function (newVal) {
|
|||||||
}
|
}
|
||||||
tablesetting = $copy(pagedata.tablesetting || gridsetting);
|
tablesetting = $copy(pagedata.tablesetting || gridsetting);
|
||||||
if (tablesetting) {
|
if (tablesetting) {
|
||||||
perPage = pagedata.perPage
|
perPage = pagedata.perPage ? pagedata.perPage : Number(tablesetting.find((v) => v.code === "per-page").detail);
|
||||||
? pagedata.perPage
|
|
||||||
: Number(tablesetting.find((v) => v.code === "per-page").detail);
|
|
||||||
}
|
}
|
||||||
if (newVal.fields) {
|
if (newVal.fields) {
|
||||||
fields = $copy(newVal.fields);
|
fields = $copy(newVal.fields);
|
||||||
|
|||||||
@@ -1,20 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<TimeOption
|
<TimeOption
|
||||||
v-bind="{ pagename: vpagename, api: api, timeopt: timeopt, filter: optfilter, importdata: props.importdata, newDataAvailable: newDataAvailable, params: vparams }"
|
v-bind="{
|
||||||
ref="timeopt" @option="timeOption" @excel="exportExcel" @add="insert" @manual-refresh="manualRefresh" @refresh-data="refreshData"
|
pagename: vpagename,
|
||||||
@import="openImportModal" class="mb-3" v-if="timeopt"></TimeOption>
|
api: api,
|
||||||
<DataTable v-bind="{ pagename: vpagename }" @edit="edit" @insert="insert" @dataevent="dataEvent" v-if="pagedata" />
|
timeopt: timeopt,
|
||||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal" />
|
filter: optfilter,
|
||||||
|
importdata: props.importdata,
|
||||||
|
newDataAvailable: newDataAvailable,
|
||||||
|
params: vparams,
|
||||||
|
}"
|
||||||
|
ref="timeopt"
|
||||||
|
@option="timeOption"
|
||||||
|
@excel="exportExcel"
|
||||||
|
@add="insert"
|
||||||
|
@manual-refresh="manualRefresh"
|
||||||
|
@refresh-data="refreshData"
|
||||||
|
@import="openImportModal"
|
||||||
|
class="mb-3"
|
||||||
|
v-if="timeopt"
|
||||||
|
></TimeOption>
|
||||||
|
<DataTable
|
||||||
|
v-bind="{ pagename: vpagename }"
|
||||||
|
@edit="edit"
|
||||||
|
@insert="insert"
|
||||||
|
@dataevent="dataEvent"
|
||||||
|
v-if="pagedata"
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import TimeOption from '~/components/datatable/TimeOption'
|
import TimeOption from "~/components/datatable/TimeOption";
|
||||||
import { useStore } from '~/stores/index'
|
import { useStore } from "~/stores/index";
|
||||||
// [FIX] Thêm onActivated, onDeactivated để xử lý KeepAlive
|
// [FIX] Thêm onActivated, onDeactivated để xử lý KeepAlive
|
||||||
import { ref, watch, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
|
import { ref, watch, onBeforeUnmount, onActivated, onDeactivated } from "vue";
|
||||||
|
|
||||||
const emit = defineEmits(['modalevent', 'dataevent', 'dataUpdated'])
|
const emit = defineEmits(["modalevent", "dataevent", "dataUpdated"]);
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pagename: String,
|
pagename: String,
|
||||||
@@ -26,43 +52,46 @@ const props = defineProps({
|
|||||||
modal: Object,
|
modal: Object,
|
||||||
timeopt: Object,
|
timeopt: Object,
|
||||||
realtime: Object,
|
realtime: Object,
|
||||||
importdata: Object
|
importdata: Object,
|
||||||
})
|
});
|
||||||
|
|
||||||
const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp()
|
const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp();
|
||||||
|
|
||||||
const showmodal = ref()
|
const showmodal = ref();
|
||||||
const pagedata = ref()
|
const pagedata = ref();
|
||||||
const newDataAvailable = ref(false)
|
const newDataAvailable = ref(false);
|
||||||
const pendingNewData = ref(null)
|
const pendingNewData = ref(null);
|
||||||
const lastDataHash = ref(null)
|
const lastDataHash = ref(null);
|
||||||
const pollingInterval = ref(null)
|
const pollingInterval = ref(null);
|
||||||
|
|
||||||
let vpagename = props.pagename
|
let vpagename = props.pagename;
|
||||||
let vfilter = props.filter ? $copy(props.filter) : undefined
|
let vfilter = props.filter ? $copy(props.filter) : undefined;
|
||||||
let vparams = props.params ? $copy(props.params) : undefined
|
let vparams = props.params ? $copy(props.params) : undefined;
|
||||||
let connection = undefined
|
let connection = undefined;
|
||||||
let optfilter = props.filter || (props.params ? props.params.filter : undefined)
|
let optfilter = props.filter || (props.params ? props.params.filter : undefined);
|
||||||
|
|
||||||
const realtimeConfig = ref({ time: 0, update: "true" })
|
const realtimeConfig = ref({ time: 0, update: "true" });
|
||||||
|
|
||||||
if (props.realtime) {
|
if (props.realtime) {
|
||||||
realtimeConfig.value = { time: props.realtime.time || 0, update: props.realtime.update }
|
realtimeConfig.value = {
|
||||||
|
time: props.realtime.time || 0,
|
||||||
|
update: props.realtime.update,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vparams?.filter) {
|
if (vparams?.filter) {
|
||||||
for (const [key, value] of Object.entries(vparams.filter)) {
|
for (const [key, value] of Object.entries(vparams.filter)) {
|
||||||
if (value.toString().indexOf('$') >= 0) {
|
if (value.toString().indexOf("$") >= 0) {
|
||||||
vparams.filter[key] = store[value.replace('$', '')].id
|
vparams.filter[key] = store[value.replace("$", "")].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateDataHash = (data) => {
|
const generateDataHash = (data) => {
|
||||||
if (!data) return null
|
if (!data) return null;
|
||||||
try {
|
try {
|
||||||
const replacer = (key, value) =>
|
const replacer = (key, value) =>
|
||||||
value && typeof value === 'object' && !Array.isArray(value)
|
value && typeof value === "object" && !Array.isArray(value)
|
||||||
? Object.keys(value)
|
? Object.keys(value)
|
||||||
.sort()
|
.sort()
|
||||||
.reduce((sorted, key) => {
|
.reduce((sorted, key) => {
|
||||||
@@ -73,341 +102,356 @@ const generateDataHash = (data) => {
|
|||||||
|
|
||||||
const stringToHash = JSON.stringify(data, replacer);
|
const stringToHash = JSON.stringify(data, replacer);
|
||||||
|
|
||||||
return stringToHash.split('').reduce((a, b) => {
|
return stringToHash.split("").reduce((a, b) => {
|
||||||
a = ((a << 5) - a) + b.charCodeAt(0)
|
a = (a << 5) - a + b.charCodeAt(0);
|
||||||
return a & a
|
return a & a;
|
||||||
}, 0)
|
}, 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error generating data hash:', e);
|
console.error("Error generating data hash:", e);
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// [FIX] Tách hàm dừng polling ra riêng để tái sử dụng
|
// [FIX] Tách hàm dừng polling ra riêng để tái sử dụng
|
||||||
const stopAutoCheck = () => {
|
const stopAutoCheck = () => {
|
||||||
if (pollingInterval.value) {
|
if (pollingInterval.value) {
|
||||||
clearInterval(pollingInterval.value)
|
clearInterval(pollingInterval.value);
|
||||||
pollingInterval.value = null
|
pollingInterval.value = null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const startAutoCheck = () => {
|
const startAutoCheck = () => {
|
||||||
// [FIX] Dừng interval cũ trước khi tạo mới, tránh tạo nhiều interval chồng nhau
|
// [FIX] Dừng interval cũ trước khi tạo mới, tránh tạo nhiều interval chồng nhau
|
||||||
stopAutoCheck()
|
stopAutoCheck();
|
||||||
if (realtimeConfig.value.time && realtimeConfig.value.time > 0) {
|
if (realtimeConfig.value.time && realtimeConfig.value.time > 0) {
|
||||||
pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000)
|
pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const checkDataChanges = async () => {
|
const checkDataChanges = async () => {
|
||||||
try {
|
try {
|
||||||
const connlist = []
|
const connlist = [];
|
||||||
const conn1 = $findapi(props.api)
|
const conn1 = $findapi(props.api);
|
||||||
|
|
||||||
if (vfilter) {
|
if (vfilter) {
|
||||||
const filter = $copy(conn1.params.filter) || {}
|
const filter = $copy(conn1.params.filter) || {};
|
||||||
for (const [key, value] of Object.entries(vfilter)) {
|
for (const [key, value] of Object.entries(vfilter)) {
|
||||||
filter[key] = value
|
filter[key] = value;
|
||||||
}
|
}
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
if (value.toString().indexOf('$') >= 0) {
|
if (value.toString().indexOf("$") >= 0) {
|
||||||
filter[key] = store[value.replace('$', '')].id
|
filter[key] = store[value.replace("$", "")].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conn1.params.filter = filter
|
conn1.params.filter = filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vparams) conn1.params = $copy(vparams)
|
if (vparams) conn1.params = $copy(vparams);
|
||||||
|
|
||||||
delete conn1.params.sort
|
delete conn1.params.sort;
|
||||||
delete conn1.params.values
|
delete conn1.params.values;
|
||||||
|
|
||||||
conn1.params.summary = 'aggregate'
|
conn1.params.summary = "aggregate";
|
||||||
conn1.params.distinct_values = JSON.stringify({
|
conn1.params.distinct_values = JSON.stringify({
|
||||||
total_count: { type: 'Count', field: 'id' },
|
total_count: { type: "Count", field: "id" },
|
||||||
last_updated: { type: 'Max', field: 'update_time' },
|
last_updated: { type: "Max", field: "update_time" },
|
||||||
last_created: { type: 'Max', field: 'create_time' }
|
last_created: { type: "Max", field: "create_time" },
|
||||||
})
|
});
|
||||||
|
|
||||||
connlist.push(conn1)
|
connlist.push(conn1);
|
||||||
|
|
||||||
const rs = await $getapi(connlist)
|
const rs = await $getapi(connlist);
|
||||||
const obj = $find(rs, { name: props.api })
|
const obj = $find(rs, { name: props.api });
|
||||||
const newMetadata = obj ? obj.data.rows : {}
|
const newMetadata = obj ? obj.data.rows : {};
|
||||||
const newHash = generateDataHash(newMetadata)
|
const newHash = generateDataHash(newMetadata);
|
||||||
|
|
||||||
if (lastDataHash.value === null) {
|
if (lastDataHash.value === null) {
|
||||||
lastDataHash.value = newHash
|
lastDataHash.value = newHash;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newHash !== lastDataHash.value) {
|
if (newHash !== lastDataHash.value) {
|
||||||
lastDataHash.value = newHash
|
lastDataHash.value = newHash;
|
||||||
|
|
||||||
if (realtimeConfig.value.update === "true") {
|
if (realtimeConfig.value.update === "true") {
|
||||||
await loadFullDataAsync()
|
await loadFullDataAsync();
|
||||||
emit('dataUpdated', { newData: store[vpagename].data, autoUpdate: true, hasChanges: true })
|
emit("dataUpdated", {
|
||||||
|
newData: store[vpagename].data,
|
||||||
|
autoUpdate: true,
|
||||||
|
hasChanges: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
newDataAvailable.value = true
|
newDataAvailable.value = true;
|
||||||
emit('dataUpdated', { newData: null, autoUpdate: false, hasChanges: true })
|
emit("dataUpdated", {
|
||||||
|
newData: null,
|
||||||
|
autoUpdate: false,
|
||||||
|
hasChanges: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking data:', error)
|
console.error("Error checking data:", error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const loadFullDataAsync = async () => {
|
const loadFullDataAsync = async () => {
|
||||||
try {
|
try {
|
||||||
const connlist = []
|
const connlist = [];
|
||||||
const conn1 = $findapi(props.api)
|
const conn1 = $findapi(props.api);
|
||||||
|
|
||||||
if (vfilter) {
|
if (vfilter) {
|
||||||
const filter = $copy(conn1.params.filter) || {}
|
const filter = $copy(conn1.params.filter) || {};
|
||||||
for (const [key, value] of Object.entries(vfilter)) {
|
for (const [key, value] of Object.entries(vfilter)) {
|
||||||
filter[key] = value
|
filter[key] = value;
|
||||||
}
|
}
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
if (value.toString().indexOf('$') >= 0) {
|
if (value.toString().indexOf("$") >= 0) {
|
||||||
filter[key] = store[value.replace('$', '')].id
|
filter[key] = store[value.replace("$", "")].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conn1.params.filter = filter
|
conn1.params.filter = filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vparams) conn1.params = $copy(vparams)
|
if (vparams) conn1.params = $copy(vparams);
|
||||||
|
|
||||||
delete conn1.params.summary
|
delete conn1.params.summary;
|
||||||
delete conn1.params.distinct_values
|
delete conn1.params.distinct_values;
|
||||||
|
|
||||||
connlist.push(conn1)
|
connlist.push(conn1);
|
||||||
|
|
||||||
const rs = await $getapi(connlist)
|
const rs = await $getapi(connlist);
|
||||||
const obj = $find(rs, { name: props.api })
|
const obj = $find(rs, { name: props.api });
|
||||||
const newData = obj ? $copy(obj.data.rows) : []
|
const newData = obj ? $copy(obj.data.rows) : [];
|
||||||
|
|
||||||
updateDataDisplay(newData)
|
updateDataDisplay(newData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading full data:', error)
|
console.error("Error loading full data:", error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const openImportModal = () => {
|
const openImportModal = () => {
|
||||||
const copy = $copy(props.importdata)
|
const copy = $copy(props.importdata);
|
||||||
showmodal.value = copy
|
showmodal.value = copy;
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateDataDisplay = (newData) => {
|
const updateDataDisplay = (newData) => {
|
||||||
const copy = $clone(store[vpagename])
|
const copy = $clone(store[vpagename]);
|
||||||
copy.data = newData
|
copy.data = newData;
|
||||||
copy.update = { data: newData }
|
copy.update = { data: newData };
|
||||||
store.commit(vpagename, copy)
|
store.commit(vpagename, copy);
|
||||||
newDataAvailable.value = false
|
newDataAvailable.value = false;
|
||||||
pendingNewData.value = null
|
pendingNewData.value = null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const manualRefresh = () => {
|
const manualRefresh = () => {
|
||||||
if (pendingNewData.value) {
|
if (pendingNewData.value) {
|
||||||
updateDataDisplay(pendingNewData.value)
|
updateDataDisplay(pendingNewData.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
stopAutoCheck()
|
stopAutoCheck();
|
||||||
newDataAvailable.value = false;
|
newDataAvailable.value = false;
|
||||||
pendingNewData.value = null;
|
pendingNewData.value = null;
|
||||||
|
|
||||||
await getApi();
|
await getApi();
|
||||||
|
|
||||||
lastDataHash.value = null;
|
lastDataHash.value = null;
|
||||||
await checkDataChanges();
|
await checkDataChanges();
|
||||||
|
|
||||||
newDataAvailable.value = false;
|
newDataAvailable.value = false;
|
||||||
startAutoCheck();
|
startAutoCheck();
|
||||||
}
|
};
|
||||||
|
|
||||||
watch(() => props.realtime, (newVal) => {
|
watch(
|
||||||
if (newVal) {
|
() => props.realtime,
|
||||||
realtimeConfig.value.time = newVal.time || 0
|
(newVal) => {
|
||||||
realtimeConfig.value.update = newVal.update === true
|
if (newVal) {
|
||||||
startAutoCheck()
|
realtimeConfig.value.time = newVal.time || 0;
|
||||||
}
|
realtimeConfig.value.update = newVal.update === true;
|
||||||
}, { deep: true })
|
startAutoCheck();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
stopAutoCheck()
|
stopAutoCheck();
|
||||||
})
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
startAutoCheck()
|
startAutoCheck();
|
||||||
})
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopAutoCheck()
|
stopAutoCheck();
|
||||||
})
|
});
|
||||||
|
|
||||||
const timeOption = (v) => {
|
const timeOption = (v) => {
|
||||||
if (!v) return getApi()
|
if (!v) return getApi();
|
||||||
|
|
||||||
if (v.filter_or) {
|
if (v.filter_or) {
|
||||||
if (vfilter) vfilter = undefined
|
if (vfilter) vfilter = undefined;
|
||||||
if (vparams) {
|
if (vparams) {
|
||||||
vparams.filter_or = v.filter_or
|
vparams.filter_or = v.filter_or;
|
||||||
} else {
|
} else {
|
||||||
const found = $copy($findapi(props.api))
|
const found = $copy($findapi(props.api));
|
||||||
found.params.filter_or = v.filter_or
|
found.params.filter_or = v.filter_or;
|
||||||
if (props.filter) {
|
if (props.filter) {
|
||||||
const filter = $copy(props.filter)
|
const filter = $copy(props.filter);
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
if (value.toString().indexOf('$') >= 0) {
|
if (value.toString().indexOf("$") >= 0) {
|
||||||
filter[key] = store[value.replace('$', '')].id
|
filter[key] = store[value.replace("$", "")].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
found.params.filter = filter
|
found.params.filter = filter;
|
||||||
}
|
}
|
||||||
vparams = found.params
|
vparams = found.params;
|
||||||
}
|
}
|
||||||
return getApi()
|
return getApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vfilter ? vfilter : (props.params ? props.params.filter || {} : {})
|
let filter = vfilter ? vfilter : props.params ? props.params.filter || {} : {};
|
||||||
for (const [key, value] of Object.entries(v.filter)) {
|
for (const [key, value] of Object.entries(v.filter)) {
|
||||||
filter[key] = value
|
filter[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
if (value.toString().indexOf('$') >= 0) {
|
if (value.toString().indexOf("$") >= 0) {
|
||||||
filter[key] = store[value.replace('$', '')].id
|
filter[key] = store[value.replace("$", "")].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vfilter) {
|
if (vfilter) {
|
||||||
vfilter = filter
|
vfilter = filter;
|
||||||
vparams = undefined
|
vparams = undefined;
|
||||||
} else if (vparams) {
|
} else if (vparams) {
|
||||||
vparams.filter = filter
|
vparams.filter = filter;
|
||||||
vparams.filter_or = undefined
|
vparams.filter_or = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vfilter && !vparams) vfilter = filter
|
if (!vfilter && !vparams) vfilter = filter;
|
||||||
getApi()
|
getApi();
|
||||||
}
|
};
|
||||||
|
|
||||||
const edit = (v) => {
|
const edit = (v) => {
|
||||||
const copy = props.modal ? $copy(props.modal) : {}
|
const copy = props.modal ? $copy(props.modal) : {};
|
||||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v }
|
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v };
|
||||||
f.pagename = vpagename
|
f.pagename = vpagename;
|
||||||
f.row = v
|
f.row = v;
|
||||||
copy.vbind = f
|
copy.vbind = f;
|
||||||
showmodal.value = copy
|
showmodal.value = copy;
|
||||||
}
|
};
|
||||||
|
|
||||||
const insert = () => {
|
const insert = () => {
|
||||||
const copy = props.modal ? $copy(props.modal) : {}
|
const copy = props.modal ? $copy(props.modal) : {};
|
||||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api }
|
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api };
|
||||||
f.pagename = vpagename
|
f.pagename = vpagename;
|
||||||
copy.vbind = f
|
copy.vbind = f;
|
||||||
showmodal.value = copy
|
showmodal.value = copy;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getApi = async () => {
|
const getApi = async () => {
|
||||||
const connlist = []
|
const connlist = [];
|
||||||
let row = props.setting?.id ? $copy(props.setting) : undefined
|
let row = props.setting?.id ? $copy(props.setting) : undefined;
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
const found = $find(store.settings.filter(v => v), props.setting > 0 ? { id: props.setting } : { name: props.setting })
|
const found = $find(
|
||||||
if (found) row = $copy(found)
|
store.settings.filter((v) => v),
|
||||||
|
props.setting > 0 ? { id: props.setting } : { name: props.setting },
|
||||||
|
);
|
||||||
|
if (found) row = $copy(found);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
const conn = $findapi('usersetting')
|
const conn = $findapi("usersetting");
|
||||||
conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting }
|
conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting };
|
||||||
connlist.push(conn)
|
connlist.push(conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = props.data ? $copy(props.data) : undefined
|
let data = props.data ? $copy(props.data) : undefined;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
const conn1 = $findapi(props.api)
|
const conn1 = $findapi(props.api);
|
||||||
if (vfilter) {
|
if (vfilter) {
|
||||||
const filter = conn1.params.filter || {}
|
const filter = conn1.params.filter || {};
|
||||||
for (const [key, value] of Object.entries(vfilter)) {
|
for (const [key, value] of Object.entries(vfilter)) {
|
||||||
filter[key] = value
|
filter[key] = value;
|
||||||
}
|
}
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
if (value.toString().indexOf('$') >= 0) {
|
if (value.toString().indexOf("$") >= 0) {
|
||||||
filter[key] = store[value.replace('$', '')].id
|
filter[key] = store[value.replace("$", "")].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conn1.params.filter = filter
|
conn1.params.filter = filter;
|
||||||
}
|
}
|
||||||
if (vparams) conn1.params = vparams
|
if (vparams) conn1.params = vparams;
|
||||||
connection = conn1
|
connection = conn1;
|
||||||
connlist.push(conn1)
|
connlist.push(conn1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let obj = undefined
|
let obj = undefined;
|
||||||
if (connlist.length > 0) {
|
if (connlist.length > 0) {
|
||||||
const rs = await $getapi(connlist)
|
const rs = await $getapi(connlist);
|
||||||
const ele = $find(rs, { name: 'usersetting' })
|
const ele = $find(rs, { name: "usersetting" });
|
||||||
if (ele) {
|
if (ele) {
|
||||||
row = $find(ele.data.rows, { name: props.setting.name || props.setting })
|
row = $find(ele.data.rows, { name: props.setting.name || props.setting });
|
||||||
const copy = $copy(store.settings)
|
const copy = $copy(store.settings);
|
||||||
copy.push(row)
|
copy.push(row);
|
||||||
store.commit('settings', copy)
|
store.commit("settings", copy);
|
||||||
}
|
}
|
||||||
obj = $find(rs, { name: props.api })
|
obj = $find(rs, { name: props.api });
|
||||||
if (obj) data = $copy(obj.data.rows)
|
if (obj) data = $copy(obj.data.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
pagedata.value = $setpage(vpagename, row, obj)
|
pagedata.value = $setpage(vpagename, row, obj);
|
||||||
const copy = $clone(pagedata.value)
|
const copy = $clone(pagedata.value);
|
||||||
copy.data = data
|
copy.data = data;
|
||||||
copy.update = { data: data }
|
copy.update = { data: data };
|
||||||
store.commit(vpagename, copy)
|
store.commit(vpagename, copy);
|
||||||
}
|
};
|
||||||
|
|
||||||
const dataEvent = (v, field, data) => {
|
const dataEvent = (v, field, data) => {
|
||||||
if (data?.modal) {
|
if (data?.modal) {
|
||||||
const copy = $copy(data.modal)
|
const copy = $copy(data.modal);
|
||||||
const f = copy.vbind ? copy.vbind : {}
|
const f = copy.vbind ? copy.vbind : {};
|
||||||
if (!f.api) f.api = props.api
|
if (!f.api) f.api = props.api;
|
||||||
f.pagename = vpagename
|
f.pagename = vpagename;
|
||||||
if (!f.row) f.row = v
|
if (!f.row) f.row = v;
|
||||||
copy.vbind = f
|
copy.vbind = f;
|
||||||
copy.field = field
|
copy.field = field;
|
||||||
showmodal.value = copy
|
showmodal.value = copy;
|
||||||
}
|
}
|
||||||
emit('modalevent', { name: 'dataevent', data: { row: v, field: field } })
|
emit("modalevent", { name: "dataevent", data: { row: v, field: field } });
|
||||||
emit('dataevent', v, field)
|
emit("dataevent", v, field);
|
||||||
}
|
};
|
||||||
|
|
||||||
const exportExcel = async () => {
|
const exportExcel = async () => {
|
||||||
if (!props.api) return
|
if (!props.api) return;
|
||||||
|
|
||||||
const found = $findapi('exportcsv')
|
const found = $findapi("exportcsv");
|
||||||
found.params = connection.params
|
found.params = connection.params;
|
||||||
const fields = pagedata.value.fields
|
const fields = pagedata.value.fields
|
||||||
.filter(v => (v.show && v.export !== 'no') || v.export === 'yes')
|
.filter((v) => (v.show && v.export !== "no") || v.export === "yes")
|
||||||
.map(x => ({ name: x.name, label: $stripHtml(x.label) }))
|
.map((x) => ({ name: x.name, label: $stripHtml(x.label) }));
|
||||||
found.params.fields = JSON.stringify(fields)
|
found.params.fields = JSON.stringify(fields);
|
||||||
found.url = connection.url.replace('data/', 'exportcsv/')
|
found.url = connection.url.replace("data/", "exportcsv/");
|
||||||
const rs = await $getapi([found])
|
const rs = await $getapi([found]);
|
||||||
|
|
||||||
if (rs === 'error') {
|
if (rs === "error") {
|
||||||
$snackbar('Đã xảy ra lỗi. Vui lòng thử lại.')
|
$snackbar("Đã xảy ra lỗi. Vui lòng thử lại.");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([rs[0].data]))
|
const url = window.URL.createObjectURL(new Blob([rs[0].data]));
|
||||||
const link = document.createElement('a')
|
const link = document.createElement("a");
|
||||||
const fileName = `${$dayjs(new Date()).format('YYYYMMDDhhmmss')}-data.csv`
|
const fileName = `${$dayjs(new Date()).format("YYYYMMDDhhmmss")}-data.csv`;
|
||||||
link.href = url
|
link.href = url;
|
||||||
link.setAttribute('download', fileName)
|
link.setAttribute("download", fileName);
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link);
|
||||||
link.click()
|
link.click();
|
||||||
link.remove()
|
link.remove();
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!props.timeopt) await getApi()
|
if (!props.timeopt) await getApi();
|
||||||
startAutoCheck()
|
startAutoCheck();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,82 +1,111 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<p class="fsb-20 mb-5">Điều chỉnh tiêu đề</p>
|
<p class="fsb-20 mb-5">Điều chỉnh tiêu đề</p>
|
||||||
<div v-for="(v, i) in arr" :key="i" :class="(i>0? 'mt-4' : null)">
|
<div
|
||||||
<p class="fsb-14">Dòng thứ {{(i+1)}}<span class="has-text-danger"> *</span></p>
|
v-for="(v, i) in arr"
|
||||||
<div class="field has-addons mt-1">
|
:key="i"
|
||||||
<div class="control is-expanded">
|
:class="i > 0 ? 'mt-4' : null"
|
||||||
<input class="input" type="text" v-model="v.label">
|
>
|
||||||
</div>
|
<p class="fsb-14">Dòng thứ {{ i + 1 }}<span class="has-text-danger"> *</span></p>
|
||||||
<div class="control">
|
<div class="field has-addons mt-1">
|
||||||
<a class="button px-2 is-primary" @click="add()">
|
<div class="control is-expanded">
|
||||||
<span>
|
<input
|
||||||
<SvgIcon v-bind="{name: 'add1.png', type: 'white', size: 17}"></SvgIcon></span>
|
class="input"
|
||||||
</a>
|
type="text"
|
||||||
</div>
|
v-model="v.label"
|
||||||
<div class="control" @click="remove(i)" v-if="(i>0)">
|
/>
|
||||||
<a class="button px-2 is-dark">
|
</div>
|
||||||
<span>
|
<div class="control">
|
||||||
<SvgIcon v-bind="{name: 'bin.svg', type: 'white', size: 17}"></SvgIcon>
|
<a
|
||||||
</span>
|
class="button px-2 is-primary"
|
||||||
</a>
|
@click="add()"
|
||||||
</div>
|
>
|
||||||
</div>
|
<span> <SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 17 }"></SvgIcon></span>
|
||||||
<p class="help has-text-danger" v-if="v.error"> {{v.error}} </p>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons mt-5">
|
<div
|
||||||
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button>
|
class="control"
|
||||||
<button class="button is-dark" @click="$emit('close')">Hủy bỏ</button>
|
@click="remove(i)"
|
||||||
</div>
|
v-if="i > 0"
|
||||||
|
>
|
||||||
|
<a class="button px-2 is-dark">
|
||||||
|
<span>
|
||||||
|
<SvgIcon v-bind="{ name: 'bin.svg', type: 'white', size: 17 }"></SvgIcon>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="v.error"
|
||||||
|
>
|
||||||
|
{{ v.error }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="buttons mt-5">
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="update()"
|
||||||
|
>
|
||||||
|
Cập nhật
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-dark"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
|
Hủy bỏ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['label'],
|
props: ["label"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
arr: []
|
arr: [],
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
let arr1 = this.label.replace('<div>', '').replace('</div>', '').split("</p>")
|
let arr1 = this.label.replace("<div>", "").replace("</div>", "").split("</p>");
|
||||||
arr1.map(v=>{
|
arr1.map((v) => {
|
||||||
if(!this.$empty(v)) {
|
if (!this.$empty(v)) {
|
||||||
let label = v + '</p>'
|
let label = v + "</p>";
|
||||||
label = this.$stripHtml(label)
|
label = this.$stripHtml(label);
|
||||||
this.arr.push({label: label})
|
this.arr.push({ label: label });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
add() {
|
add() {
|
||||||
this.arr.push({label: undefined})
|
this.arr.push({ label: undefined });
|
||||||
},
|
},
|
||||||
remove(i) {
|
remove(i) {
|
||||||
this.$remove(this.arr, i)
|
this.$remove(this.arr, i);
|
||||||
},
|
},
|
||||||
checkError() {
|
checkError() {
|
||||||
let error = false
|
let error = false;
|
||||||
this.arr.map(v=>{
|
this.arr.map((v) => {
|
||||||
if(this.$empty(v.label)) {
|
if (this.$empty(v.label)) {
|
||||||
v.error = 'Nội dung không được bỏ trống'
|
v.error = "Nội dung không được bỏ trống";
|
||||||
error = true
|
error = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
if(error) this.arr = this.$copy(this.arr)
|
if (error) this.arr = this.$copy(this.arr);
|
||||||
return error
|
return error;
|
||||||
},
|
},
|
||||||
update() {
|
update() {
|
||||||
if(this.checkError()) return
|
if (this.checkError()) return;
|
||||||
let label = ''
|
let label = "";
|
||||||
if(this.arr.length>1) {
|
if (this.arr.length > 1) {
|
||||||
this.arr.map((v,i)=>{
|
this.arr.map((v, i) => {
|
||||||
label += `<p${i<this.arr.length-1? ' style="border-bottom: 1px solid white;"' : ''}>${v.label.trim()}</p>`
|
label += `<p${i < this.arr.length - 1 ? ' style="border-bottom: 1px solid white;"' : ""}>${v.label.trim()}</p>`;
|
||||||
})
|
});
|
||||||
label = `<div>${label}</div>`
|
label = `<div>${label}</div>`;
|
||||||
} else label = this.arr[0].label.trim()
|
} else label = this.arr[0].label.trim();
|
||||||
this.$emit('modalevent', {name: 'label', data: label})
|
this.$emit("modalevent", { name: "label", data: label });
|
||||||
this.$emit('close')
|
this.$emit("close");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,88 +1,127 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<template v-if="keys.length>0">
|
<template v-if="keys.length > 0">
|
||||||
<div class="field is-horizontal" v-for="(v,i) in keys" :key="i">
|
<div
|
||||||
<div class="field-body">
|
class="field is-horizontal"
|
||||||
<div class="field is-narrow">
|
v-for="(v, i) in keys"
|
||||||
<div class="control">
|
:key="i"
|
||||||
<input class="input fs-14" type="text" placeholder="" v-model="keys[i]">
|
>
|
||||||
</div>
|
<div class="field-body">
|
||||||
</div>
|
<div class="field is-narrow">
|
||||||
<div class="field">
|
<div class="control">
|
||||||
<div class="control">
|
<input
|
||||||
<input class="input fs-14" type="text" placeholder="" v-model="values[i]">
|
class="input fs-14"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="keys[i]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input fs-14"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="values[i]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field is-narrow">
|
||||||
|
<p class="control">
|
||||||
|
<a @click="addAttr()">
|
||||||
|
<SvgIcon v-bind="{ name: 'add1.png', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="ml-2"
|
||||||
|
@click="remove(i)"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="ml-2"
|
||||||
|
@click="jsonData(v, i)"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'apps.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="field is-narrow">
|
|
||||||
<p class="control">
|
|
||||||
<a @click="addAttr()">
|
|
||||||
<SvgIcon v-bind="{name: 'add1.png', type: 'gray', size: 18}"></SvgIcon>
|
|
||||||
</a>
|
|
||||||
<a class="ml-2" @click="remove(i)">
|
|
||||||
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 18}"></SvgIcon>
|
|
||||||
</a>
|
|
||||||
<a class="ml-2" @click="jsonData(v, i)">
|
|
||||||
<SvgIcon v-bind="{name: 'apps.svg', type: 'gray', size: 18}"></SvgIcon>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="mb-6" v-else>
|
<div
|
||||||
<button class="button is-primary has-text-white" @click="addAttr()">Thêm thuộc tính</button>
|
class="mb-6"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="addAttr()"
|
||||||
|
>
|
||||||
|
Thêm thuộc tính
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons mt-5">
|
<div class="buttons mt-5">
|
||||||
<a class="button is-primary has-text-white" @click="update()">Cập nhật</a>
|
<a
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="update()"
|
||||||
|
>Cập nhật</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<Modal @close="comp=undefined" @update="doUpdate"
|
<Modal
|
||||||
v-bind="{component: comp, width: '40%', height: '300px', vbind: vbind}" v-if="comp"></Modal>
|
@close="comp = undefined"
|
||||||
|
@update="doUpdate"
|
||||||
|
v-bind="{ component: comp, width: '40%', height: '300px', vbind: vbind }"
|
||||||
|
v-if="comp"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['field', 'close'],
|
props: ["field", "close"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
keys: [],
|
keys: [],
|
||||||
values: [],
|
values: [],
|
||||||
comp: undefined,
|
comp: undefined,
|
||||||
vbind: undefined,
|
vbind: undefined,
|
||||||
current: undefined
|
current: undefined,
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
Object.keys(this.field).map((v) => {
|
||||||
|
this.keys.push(v);
|
||||||
|
this.values.push(this.field[v]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
doUpdate(v) {
|
||||||
|
this.values[this.current.i] = v;
|
||||||
},
|
},
|
||||||
created() {
|
jsonData(v, i) {
|
||||||
Object.keys(this.field).map(v=>{
|
this.current = { v: v, i: i };
|
||||||
this.keys.push(v)
|
this.vbind = {
|
||||||
this.values.push(this.field[v])
|
field: this.$empty(this.values[i]) || typeof this.values[i] === "string" ? {} : this.values[i],
|
||||||
})
|
close: true,
|
||||||
|
};
|
||||||
|
this.comp = "datatable/FieldAttribute";
|
||||||
},
|
},
|
||||||
methods: {
|
addAttr() {
|
||||||
doUpdate(v) {
|
this.keys.push(undefined);
|
||||||
this.values[this.current.i] = v
|
this.values.push(undefined);
|
||||||
},
|
},
|
||||||
jsonData(v, i) {
|
remove(i) {
|
||||||
this.current = {v: v, i: i}
|
this.$remove(this.keys, i);
|
||||||
this.vbind = {field: this.$empty(this.values[i]) || typeof this.values[i] === 'string'? {} : this.values[i], close: true}
|
this.$remove(this.values, i);
|
||||||
this.comp = 'datatable/FieldAttribute'
|
},
|
||||||
},
|
update() {
|
||||||
addAttr() {
|
let obj = {};
|
||||||
this.keys.push(undefined)
|
this.keys.map((v, i) => {
|
||||||
this.values.push(undefined)
|
if (!this.$empty(v)) obj[v] = v.indexOf("__in") > 0 ? this.values[i].split(",") : this.values[i];
|
||||||
},
|
});
|
||||||
remove(i) {
|
this.$emit("update", obj);
|
||||||
this.$remove(this.keys, i)
|
this.$emit("modalevent", { name: "update", data: obj });
|
||||||
this.$remove(this.values, i)
|
if (this.close) this.$emit("close");
|
||||||
},
|
},
|
||||||
update() {
|
},
|
||||||
let obj = {}
|
};
|
||||||
this.keys.map((v,i)=>{
|
</script>
|
||||||
if(!this.$empty(v)) obj[v] = v.indexOf('__in')>0? this.values[i].split(',') : this.values[i]
|
|
||||||
})
|
|
||||||
this.$emit('update', obj)
|
|
||||||
this.$emit('modalevent', {name: 'update', data: obj})
|
|
||||||
if(this.close) this.$emit('close')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,52 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="field mt-3 mb-1" v-if="field.format==='number'">
|
<div
|
||||||
|
class="field mt-3 mb-1"
|
||||||
|
v-if="field.format === 'number'"
|
||||||
|
>
|
||||||
<label class="label fs-14">Chọn trường<span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14">Chọn trường<span class="has-text-danger"> * </span> </label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<b-taginput
|
<b-taginput
|
||||||
size="is-small"
|
size="is-small"
|
||||||
v-model="tagsField"
|
v-model="tagsField"
|
||||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
:data="pageData ? pageData.fields.filter((v) => v.format === 'number') : []"
|
||||||
type="is-dark is-light"
|
type="is-dark is-light"
|
||||||
autocomplete
|
autocomplete
|
||||||
:open-on-focus="true"
|
:open-on-focus="true"
|
||||||
field="name"
|
field="name"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
placeholder="Chọn trường"
|
placeholder="Chọn trường"
|
||||||
>
|
>
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
<span class="mr-3 has-text-danger has-text-weight-bold"> {{ props.option.name }}</span>
|
||||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 60)}} </span>
|
<span :class="tagsField.find((v) => v.id === props.option.id) ? 'has-text-dark' : ''">
|
||||||
</template>
|
{{ $stripHtml(props.option.label, 60) }}
|
||||||
<template slot="empty">
|
</span>
|
||||||
Không có trường thỏa mãn
|
|
||||||
</template>
|
</template>
|
||||||
|
<template slot="empty"> Không có trường thỏa mãn </template>
|
||||||
</b-taginput>
|
</b-taginput>
|
||||||
</div>
|
</div>
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'tagsField')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "tagsField").message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2" v-if="tagsField.length>0">
|
<div
|
||||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
class="mt-2"
|
||||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
v-if="tagsField.length > 0"
|
||||||
<span class="tooltip">
|
>
|
||||||
{{ v.name }}
|
<a
|
||||||
<span class="tooltiptext" style="top: 60%; bottom: unset; min-width: max-content; left: 25px;">{{ $stripHtml(v.label) }}</span>
|
@dblclick="expression = expression ? expression + ' ' + v.name : v.name"
|
||||||
</span>
|
class="tag is-rounded"
|
||||||
|
v-for="(v, i) in tagsField"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<span class="tooltip">
|
||||||
|
{{ v.name }}
|
||||||
|
<span
|
||||||
|
class="tooltiptext"
|
||||||
|
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||||
|
>{{ $stripHtml(v.label) }}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-horizontal mt-3">
|
<div class="field is-horizontal mt-3">
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field" v-if="field.format==='number'">
|
<div
|
||||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span> </label>
|
class="field"
|
||||||
|
v-if="field.format === 'number'"
|
||||||
|
>
|
||||||
|
<label class="label fs-14"
|
||||||
|
>Biểu thức có dạng Đúng / Sai
|
||||||
|
<span class="has-text-danger"> * </span>
|
||||||
|
</label>
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<input class="input is-small" type="text" v-model="expression">
|
<input
|
||||||
</p>
|
class="input is-small"
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
type="text"
|
||||||
|
v-model="expression"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'expression')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "expression").message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field" v-else>
|
<div
|
||||||
<label class="label"> Chuỗi kí tự <span class="has-text-danger"> * </span>
|
class="field"
|
||||||
</label>
|
v-else
|
||||||
|
>
|
||||||
|
<label class="label"> Chuỗi kí tự <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<input
|
<input
|
||||||
class="input is-small"
|
class="input is-small"
|
||||||
@@ -63,27 +99,53 @@
|
|||||||
{{ errors.find((v) => v.name === "searchText").msg }}
|
{{ errors.find((v) => v.name === "searchText").msg }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-narrow" v-if="filterType==='color'">
|
<div
|
||||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
class="field is-narrow"
|
||||||
<p class="control fs-14">
|
v-if="filterType === 'color'"
|
||||||
<input type="color" v-model="color" @change="changeStyle()">
|
>
|
||||||
</p>
|
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='color')"> {{errors.find(v=>v.name==='color').message}} </p>
|
<p class="control fs-14">
|
||||||
</div>
|
<input
|
||||||
<div class="field is-narrow" v-else-if="filterType==='size'">
|
type="color"
|
||||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
v-model="color"
|
||||||
<p class="control fs-14">
|
@change="changeStyle()"
|
||||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="size" @change="changeStyle()">
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='size')"> {{errors.find(v=>v.name==='size').message}} </p>
|
<p
|
||||||
</div>
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'color')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "color").message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="field is-narrow"
|
||||||
|
v-else-if="filterType === 'size'"
|
||||||
|
>
|
||||||
|
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<input
|
||||||
|
class="input is-small"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nhập số"
|
||||||
|
v-model="size"
|
||||||
|
@change="changeStyle()"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'size')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "size").message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['filterObj', 'filterType', 'pagename', 'field'],
|
props: ["filterObj", "filterType", "pagename", "field"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tagsField: [],
|
tagsField: [],
|
||||||
@@ -92,79 +154,113 @@ export default {
|
|||||||
color: undefined,
|
color: undefined,
|
||||||
size: undefined,
|
size: undefined,
|
||||||
errors: [],
|
errors: [],
|
||||||
searchText: undefined
|
searchText: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.color = this.filterObj.color
|
this.color = this.filterObj.color;
|
||||||
this.size = this.filterObj.size
|
this.size = this.filterObj.size;
|
||||||
this.expression = this.filterObj.expression? this.filterObj.expression : this.field.name
|
this.expression = this.filterObj.expression ? this.filterObj.expression : this.field.name;
|
||||||
if(this.filterObj.tags) {
|
if (this.filterObj.tags) {
|
||||||
this.filterObj.tags.map(v=>{
|
this.filterObj.tags.map((v) => {
|
||||||
this.tagsField.push(this.pageData.fields.find(x=>x.name===v))
|
this.tagsField.push(this.pageData.fields.find((x) => x.name === v));
|
||||||
})
|
});
|
||||||
} else if(this.field.format==='number') this.tagsField.push(this.pageData.fields.find(v=>v.name===this.field.name))
|
} else if (this.field.format === "number")
|
||||||
|
this.tagsField.push(this.pageData.fields.find((v) => v.name === this.field.name));
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
expression: function(newVal) {
|
expression: function (newVal) {
|
||||||
if(this.$empty(newVal)) return
|
if (this.$empty(newVal)) return;
|
||||||
else this.changeStyle()
|
else this.changeStyle();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
colorscheme: {
|
colorscheme: {
|
||||||
get: function() {return this.$store.state.colorscheme},
|
get: function () {
|
||||||
set: function(val) {this.$store.commit("updateColorScheme", {colorscheme: val})}
|
return this.$store.state.colorscheme;
|
||||||
|
},
|
||||||
|
set: function (val) {
|
||||||
|
this.$store.commit("updateColorScheme", { colorscheme: val });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
pageData: {
|
pageData: {
|
||||||
get: function() {return this.$store.state[this.pagename]},
|
get: function () {
|
||||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
return this.$store.state[this.pagename];
|
||||||
|
},
|
||||||
|
set: function (val) {
|
||||||
|
this.$store.commit("updateStore", { name: this.pagename, data: val });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
colorchoice: {
|
colorchoice: {
|
||||||
get: function() {return this.$store.state.colorchoice},
|
get: function () {
|
||||||
set: function(val) {this.$store.commit("updateColorChoice", {colorchoice: val})}
|
return this.$store.state.colorchoice;
|
||||||
}
|
},
|
||||||
|
set: function (val) {
|
||||||
|
this.$store.commit("updateColorChoice", { colorchoice: val });
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeStyle() {
|
changeStyle() {
|
||||||
let check = this.field.format==='number'? this.checkExpression() : this.checkCondition()
|
let check = this.field.format === "number" ? this.checkExpression() : this.checkCondition();
|
||||||
if(!check) return
|
if (!check) return;
|
||||||
var row = this.field.format==='number'? {expression: this.expression, tags: this.tagsField.map(v=>v.name)}
|
var row =
|
||||||
: {keyword: this.searchText, type: 'search'}
|
this.field.format === "number"
|
||||||
this.filterType==='color'? row.color = this.color : row.size = this.size
|
? {
|
||||||
this.$emit('databack', row)
|
expression: this.expression,
|
||||||
|
tags: this.tagsField.map((v) => v.name),
|
||||||
|
}
|
||||||
|
: { keyword: this.searchText, type: "search" };
|
||||||
|
this.filterType === "color" ? (row.color = this.color) : (row.size = this.size);
|
||||||
|
this.$emit("databack", row);
|
||||||
},
|
},
|
||||||
checkCondition() {
|
checkCondition() {
|
||||||
this.errors = []
|
this.errors = [];
|
||||||
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
|
if (this.filterType === "color" && this.$empty(this.color))
|
||||||
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'})
|
this.errors.push({ name: "color", message: "Chọn màu" });
|
||||||
if(this.$empty(this.searchText)) this.errors.push({name: 'searchText', message: 'Chưa nhập chuỗi kí tự'})
|
if (this.filterType === "size" && this.$empty(this.size))
|
||||||
return this.errors.length>0? false : true
|
this.errors.push({ name: "size", message: "Nhập cỡ chữ" });
|
||||||
|
if (this.$empty(this.searchText))
|
||||||
|
this.errors.push({
|
||||||
|
name: "searchText",
|
||||||
|
message: "Chưa nhập chuỗi kí tự",
|
||||||
|
});
|
||||||
|
return this.errors.length > 0 ? false : true;
|
||||||
},
|
},
|
||||||
checkExpression() {
|
checkExpression() {
|
||||||
this.errors = []
|
this.errors = [];
|
||||||
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
|
if (this.filterType === "color" && this.$empty(this.color))
|
||||||
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'})
|
this.errors.push({ name: "color", message: "Chọn màu" });
|
||||||
let val = this.$copy(this.expression)
|
if (this.filterType === "size" && this.$empty(this.size))
|
||||||
let exp = this.$copy(this.expression)
|
this.errors.push({ name: "size", message: "Nhập cỡ chữ" });
|
||||||
this.tagsField.forEach(v => {
|
let val = this.$copy(this.expression);
|
||||||
let myRegExp = new RegExp(v.name, 'g')
|
let exp = this.$copy(this.expression);
|
||||||
val = val.replace(myRegExp, Math.random())
|
this.tagsField.forEach((v) => {
|
||||||
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])")
|
let myRegExp = new RegExp(v.name, "g");
|
||||||
})
|
val = val.replace(myRegExp, Math.random());
|
||||||
try {
|
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])");
|
||||||
let value = this.$calc(val)
|
});
|
||||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
try {
|
||||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
let value = this.$calc(val);
|
||||||
} else if(!(eval(value)===true || eval(value)===false)) {
|
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
|
||||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
this.errors.push({
|
||||||
|
name: "expression",
|
||||||
|
message: "Biểu thức không hợp lệ",
|
||||||
|
});
|
||||||
|
} else if (!(eval(value) === true || eval(value) === false)) {
|
||||||
|
this.errors.push({
|
||||||
|
name: "expression",
|
||||||
|
message: "Biểu thức không hợp lệ",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
} catch (err) {
|
||||||
catch(err) {
|
this.errors.push({
|
||||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
name: "expression",
|
||||||
|
message: "Biểu thức không hợp lệ",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this.errors.length>0? false : true
|
return this.errors.length > 0 ? false : true;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="tabs is-boxed">
|
<div class="tabs is-boxed">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(v, i) in tabs" :key="i" :class="tab.code === v.code ? 'is-active' : ''" @click="tab = v">
|
<li
|
||||||
|
v-for="(v, i) in tabs"
|
||||||
|
:key="i"
|
||||||
|
:class="tab.code === v.code ? 'is-active' : ''"
|
||||||
|
@click="tab = v"
|
||||||
|
>
|
||||||
<a>{{ v.name }}</a>
|
<a>{{ v.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -14,17 +19,37 @@
|
|||||||
|
|
||||||
<template v-if="tab.code === 'expression' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
<template v-if="tab.code === 'expression' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
||||||
<template v-if="radio ? radio.code === 'condition' && sideBar === 'bgcolor' : false">
|
<template v-if="radio ? radio.code === 'condition' && sideBar === 'bgcolor' : false">
|
||||||
<div v-for="(v, i) in bgcolorFilter" :key="v.id" class="px-4">
|
<div
|
||||||
|
v-for="(v, i) in bgcolorFilter"
|
||||||
|
:key="v.id"
|
||||||
|
class="px-4"
|
||||||
|
>
|
||||||
<FilterOption
|
<FilterOption
|
||||||
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }"
|
v-bind="{
|
||||||
|
filterObj: v,
|
||||||
|
filterType: 'color',
|
||||||
|
pagename: pagename,
|
||||||
|
field: openField,
|
||||||
|
}"
|
||||||
:ref="v.id"
|
:ref="v.id"
|
||||||
@databack="doConditionFilter($event, 'bgcolor', v.id)"
|
@databack="doConditionFilter($event, 'bgcolor', v.id)"
|
||||||
/>
|
/>
|
||||||
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'">
|
<p
|
||||||
<a class="has-text-primary mr-5" @click="addCondition(bgcolorFilter)" v-if="bgcolorFilter.length <= 30">
|
class="fs-14 mt-1"
|
||||||
|
:class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="has-text-primary mr-5"
|
||||||
|
@click="addCondition(bgcolorFilter)"
|
||||||
|
v-if="bgcolorFilter.length <= 30"
|
||||||
|
>
|
||||||
Thêm
|
Thêm
|
||||||
</a>
|
</a>
|
||||||
<a class="has-text-danger" @click="removeCondition(bgcolorFilter, i)" v-if="bgcolorFilter.length > 1">
|
<a
|
||||||
|
class="has-text-danger"
|
||||||
|
@click="removeCondition(bgcolorFilter, i)"
|
||||||
|
v-if="bgcolorFilter.length > 1"
|
||||||
|
>
|
||||||
Bớt
|
Bớt
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -32,29 +57,77 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'color' : false">
|
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'color' : false">
|
||||||
<div v-for="(v, i) in colorFilter" :key="v.id" class="px-4">
|
<div
|
||||||
|
v-for="(v, i) in colorFilter"
|
||||||
|
:key="v.id"
|
||||||
|
class="px-4"
|
||||||
|
>
|
||||||
<FilterOption
|
<FilterOption
|
||||||
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }"
|
v-bind="{
|
||||||
|
filterObj: v,
|
||||||
|
filterType: 'color',
|
||||||
|
pagename: pagename,
|
||||||
|
field: openField,
|
||||||
|
}"
|
||||||
:ref="v.id"
|
:ref="v.id"
|
||||||
@databack="doConditionFilter($event, 'color', v.id)"
|
@databack="doConditionFilter($event, 'color', v.id)"
|
||||||
/>
|
/>
|
||||||
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'">
|
<p
|
||||||
<a class="has-text-primary mr-5" @click="addCondition(colorFilter)" v-if="colorFilter.length <= 30"> Thêm </a>
|
class="fs-14 mt-1"
|
||||||
<a class="has-text-danger" @click="removeCondition(colorFilter, i)" v-if="colorFilter.length > 1"> Bớt </a>
|
:class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="has-text-primary mr-5"
|
||||||
|
@click="addCondition(colorFilter)"
|
||||||
|
v-if="colorFilter.length <= 30"
|
||||||
|
>
|
||||||
|
Thêm
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="has-text-danger"
|
||||||
|
@click="removeCondition(colorFilter, i)"
|
||||||
|
v-if="colorFilter.length > 1"
|
||||||
|
>
|
||||||
|
Bớt
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'textsize' : false">
|
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'textsize' : false">
|
||||||
<div v-for="(v, i) in sizeFilter" :key="v.id" class="px-4">
|
<div
|
||||||
|
v-for="(v, i) in sizeFilter"
|
||||||
|
:key="v.id"
|
||||||
|
class="px-4"
|
||||||
|
>
|
||||||
<FilterOption
|
<FilterOption
|
||||||
v-bind="{ filterObj: v, filterType: 'size', pagename: pagename, field: openField }"
|
v-bind="{
|
||||||
|
filterObj: v,
|
||||||
|
filterType: 'size',
|
||||||
|
pagename: pagename,
|
||||||
|
field: openField,
|
||||||
|
}"
|
||||||
:ref="v.id"
|
:ref="v.id"
|
||||||
@databack="doConditionFilter($event, 'textsize', v.id)"
|
@databack="doConditionFilter($event, 'textsize', v.id)"
|
||||||
/>
|
/>
|
||||||
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'">
|
<p
|
||||||
<a class="has-text-primary mr-5" @click="addCondition(sizeFilter)" v-if="sizeFilter.length <= 30"> Thêm </a>
|
class="fs-14 mt-1"
|
||||||
<a class="has-text-danger" @click="removeCondition(sizeFilter, i)" v-if="sizeFilter.length > 1"> Bớt </a>
|
:class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="has-text-primary mr-5"
|
||||||
|
@click="addCondition(sizeFilter)"
|
||||||
|
v-if="sizeFilter.length <= 30"
|
||||||
|
>
|
||||||
|
Thêm
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="has-text-danger"
|
||||||
|
@click="removeCondition(sizeFilter, i)"
|
||||||
|
v-if="sizeFilter.length > 1"
|
||||||
|
>
|
||||||
|
Bớt
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -62,21 +135,39 @@
|
|||||||
|
|
||||||
<template v-else-if="tab.code === 'script' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
<template v-else-if="tab.code === 'script' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
||||||
<p class="my-4 mx-4">
|
<p class="my-4 mx-4">
|
||||||
<a @click="copyContent(script ? script : '')" class="mr-6">
|
<a
|
||||||
|
@click="copyContent(script ? script : '')"
|
||||||
|
class="mr-6"
|
||||||
|
>
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<SvgIcon class="mr-2" v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"></SvgIcon>
|
<SvgIcon
|
||||||
|
class="mr-2"
|
||||||
|
v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"
|
||||||
|
></SvgIcon>
|
||||||
<span class="fs-16">Copy</span>
|
<span class="fs-16">Copy</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a @click="paste()" class="mr-6">
|
<a
|
||||||
|
@click="paste()"
|
||||||
|
class="mr-6"
|
||||||
|
>
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<SvgIcon class="mr-2" v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"></SvgIcon>
|
<SvgIcon
|
||||||
|
class="mr-2"
|
||||||
|
v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"
|
||||||
|
></SvgIcon>
|
||||||
<span class="fs-16">Paste</span>
|
<span class="fs-16">Paste</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="mx-4">
|
<div class="mx-4">
|
||||||
<textarea class="textarea fs-14" rows="8" v-model="script" @change="checkScript()" @dblclick="doCheck"></textarea>
|
<textarea
|
||||||
|
class="textarea fs-14"
|
||||||
|
rows="8"
|
||||||
|
v-model="script"
|
||||||
|
@change="checkScript()"
|
||||||
|
@dblclick="doCheck"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-5 mx-4">
|
<p class="mt-5 mx-4">
|
||||||
<span class="icon-text fsb-18">
|
<span class="icon-text fsb-18">
|
||||||
@@ -87,14 +178,29 @@
|
|||||||
<div class="field is-grouped mx-4 mt-4">
|
<div class="field is-grouped mx-4 mt-4">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<p class="fsb-14 mb-1">Đoạn text</p>
|
<p class="fsb-14 mb-1">Đoạn text</p>
|
||||||
<input class="input" type="text" placeholder="" v-model="source" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="source"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<p class="fsb-14 mb-1">Thay bằng</p>
|
<p class="fsb-14 mb-1">Thay bằng</p>
|
||||||
<input class="input" type="text" placeholder="" v-model="target" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="target"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control pl-5">
|
<div class="control pl-5">
|
||||||
<button class="button is-primary is-rounded is-outlined mt-5" @click="replace()">Replace</button>
|
<button
|
||||||
|
class="button is-primary is-rounded is-outlined mt-5"
|
||||||
|
@click="replace()"
|
||||||
|
>
|
||||||
|
Replace
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-5 pt-2 mx-4">
|
<p class="mt-5 pt-2 mx-4">
|
||||||
@@ -103,10 +209,24 @@
|
|||||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="mx-4 mt-4"><button class="button is-primary is-rounded" @click="changeScript()">Cập nhật</button></p>
|
<p class="mx-4 mt-4">
|
||||||
|
<button
|
||||||
|
class="button is-primary is-rounded"
|
||||||
|
@click="changeScript()"
|
||||||
|
>
|
||||||
|
Cập nhật
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<TableOption v-bind="{ pagename: pagename }" v-else-if="sideBar === 'option'"> </TableOption>
|
<TableOption
|
||||||
<CreateTemplate v-else-if="sideBar === 'template'" v-bind="{ pagename: pagename, field: openField }">
|
v-bind="{ pagename: pagename }"
|
||||||
|
v-else-if="sideBar === 'option'"
|
||||||
|
>
|
||||||
|
</TableOption>
|
||||||
|
<CreateTemplate
|
||||||
|
v-else-if="sideBar === 'template'"
|
||||||
|
v-bind="{ pagename: pagename, field: openField }"
|
||||||
|
>
|
||||||
</CreateTemplate>
|
</CreateTemplate>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-4" v-if="currentsetting ? currentsetting.user === login.id : false">
|
<div
|
||||||
|
class="mb-4"
|
||||||
|
v-if="currentsetting ? currentsetting.user === login.id : false"
|
||||||
|
>
|
||||||
<p class="fs-16 has-text-findata">
|
<p class="fs-16 has-text-findata">
|
||||||
Đang mở: <b>{{ $stripHtml(currentsetting.name, 40) }}</b>
|
Đang mở: <b>{{ $stripHtml(currentsetting.name, 40) }}</b>
|
||||||
</p>
|
</p>
|
||||||
@@ -7,7 +10,11 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14">Chọn chế độ lưu <span class="has-text-danger"> * </span></label>
|
<label class="label fs-14">Chọn chế độ lưu <span class="has-text-danger"> * </span></label>
|
||||||
<div class="control is-expanded fs-14">
|
<div class="control is-expanded fs-14">
|
||||||
<a class="mr-5" v-if="isOverwrite()" @click="changeType('overwrite')">
|
<a
|
||||||
|
class="mr-5"
|
||||||
|
v-if="isOverwrite()"
|
||||||
|
@click="changeType('overwrite')"
|
||||||
|
>
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
v-bind="{
|
v-bind="{
|
||||||
@@ -22,7 +29,11 @@
|
|||||||
<a @click="changeType('new')">
|
<a @click="changeType('new')">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
v-bind="{ name: radioSave === 'new' ? 'radio-checked.svg' : 'radio-unchecked.svg', type: 'gray', size: 22 }"
|
v-bind="{
|
||||||
|
name: radioSave === 'new' ? 'radio-checked.svg' : 'radio-unchecked.svg',
|
||||||
|
type: 'gray',
|
||||||
|
size: 22,
|
||||||
|
}"
|
||||||
></SvgIcon>
|
></SvgIcon>
|
||||||
Tạo mới
|
Tạo mới
|
||||||
</span>
|
</span>
|
||||||
@@ -33,16 +44,30 @@
|
|||||||
<div class="field mt-4 px-0 mx-0">
|
<div class="field mt-4 px-0 mx-0">
|
||||||
<label class="label fs-14">Tên thiết lập <span class="has-text-danger"> * </span></label>
|
<label class="label fs-14">Tên thiết lập <span class="has-text-danger"> * </span></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" placeholder="" v-model="name" ref="name" v-on:keyup.enter="saveSetting" />
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="name"
|
||||||
|
ref="name"
|
||||||
|
v-on:keyup.enter="saveSetting"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="help has-text-danger" v-if="errors.find((v) => v.name === 'name')">
|
<div
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'name')"
|
||||||
|
>
|
||||||
{{ errors.find((v) => v.name === "name").msg }}
|
{{ errors.find((v) => v.name === "name").msg }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mt-4 px-0 mx-0">
|
<div class="field mt-4 px-0 mx-0">
|
||||||
<label class="label fs-14"> Mô tả </label>
|
<label class="label fs-14"> Mô tả </label>
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<textarea class="textarea" rows="4" v-model="note"></textarea>
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
rows="4"
|
||||||
|
v-model="note"
|
||||||
|
></textarea>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
<!--
|
||||||
@@ -60,11 +85,19 @@
|
|||||||
</div>-->
|
</div>-->
|
||||||
</template>
|
</template>
|
||||||
<div class="field mt-5 px-0 mx-0">
|
<div class="field mt-5 px-0 mx-0">
|
||||||
<label class="label fs-14" v-if="status !== undefined" :class="status ? 'has-text-primary' : 'has-text-danger'">
|
<label
|
||||||
|
class="label fs-14"
|
||||||
|
v-if="status !== undefined"
|
||||||
|
:class="status ? 'has-text-primary' : 'has-text-danger'"
|
||||||
|
>
|
||||||
{{ status ? "Lưu thiết lập thành công." : "Lỗi. Lưu thiết lập thất bại." }}
|
{{ status ? "Lưu thiết lập thành công." : "Lỗi. Lưu thiết lập thất bại." }}
|
||||||
</label>
|
</label>
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<a class="button is-primary has-text-white" @click="saveSetting()">Lưu lại</a>
|
<a
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="saveSetting()"
|
||||||
|
>Lưu lại</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -118,7 +151,10 @@ async function saveSetting() {
|
|||||||
let result;
|
let result;
|
||||||
if (radioSave.value === "new") {
|
if (radioSave.value === "new") {
|
||||||
if ($empty(name)) {
|
if ($empty(name)) {
|
||||||
return errors.push({ name: "name", msg: "Tên thiết lập không được bỏ trống" });
|
return errors.push({
|
||||||
|
name: "name",
|
||||||
|
msg: "Tên thiết lập không được bỏ trống",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
result = await $insertapi("usersetting", data);
|
result = await $insertapi("usersetting", data);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,52 +1,114 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
<li :class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs">
|
<li
|
||||||
|
:class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`"
|
||||||
|
v-for="v in tabs"
|
||||||
|
>
|
||||||
<a @click="changeTab(v)">{{ v.name }}</a>
|
<a @click="changeTab(v)">{{ v.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="tab === 'datatype'">
|
<template v-if="tab === 'datatype'">
|
||||||
<Caption class="mb-3" v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"></Caption>
|
<Caption
|
||||||
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields">
|
class="mb-3"
|
||||||
|
v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<div
|
||||||
|
class="py-1 border-bottom is-clickable"
|
||||||
|
v-for="x in current.fields"
|
||||||
|
>
|
||||||
{{ x.name }}
|
{{ x.name }}
|
||||||
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
||||||
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a>
|
<a
|
||||||
|
class="ml-6 has-text-primary"
|
||||||
|
v-if="x.model"
|
||||||
|
@click="openModel(x)"
|
||||||
|
>{{ x.model }}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="columns mx-0 mb-0 pb-0">
|
<div class="columns mx-0 mb-0 pb-0">
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
<Caption class="mb-2" v-bind="{ title: 'Values', type: 'has-text-warning' }"></Caption>
|
<Caption
|
||||||
<input class="input" rows="1" v-model="values" />
|
class="mb-2"
|
||||||
|
v-bind="{ title: 'Values', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
rows="1"
|
||||||
|
v-model="values"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4s">
|
<div class="column is-4s">
|
||||||
<Caption class="mb-2" v-bind="{ title: 'Filter', type: 'has-text-warning' }"></Caption>
|
<Caption
|
||||||
<input class="input" rows="1" v-model="filter" />
|
class="mb-2"
|
||||||
|
v-bind="{ title: 'Filter', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
rows="1"
|
||||||
|
v-model="filter"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-1">
|
<div class="column is-1">
|
||||||
<Caption class="mb-2" v-bind="{ title: 'Load', type: 'has-text-warning' }"></Caption>
|
<Caption
|
||||||
|
class="mb-2"
|
||||||
|
v-bind="{ title: 'Load', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
<div>
|
<div>
|
||||||
<button class="button is-primary has-text-white" @click="loadData()">Load</button>
|
<button
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="loadData()"
|
||||||
|
>
|
||||||
|
Load
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Caption class="mb-2" v-bind="{ title: 'Query', type: 'has-text-warning' }"></Caption>
|
<Caption
|
||||||
|
class="mb-2"
|
||||||
|
v-bind="{ title: 'Query', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
{{ query }}
|
{{ query }}
|
||||||
<a class="has-text-primary ml-5" @click="copy()">copy</a>
|
<a
|
||||||
|
class="has-text-primary ml-5"
|
||||||
|
@click="copy()"
|
||||||
|
>copy</a
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
{{ apiUrl }}
|
{{ apiUrl }}
|
||||||
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a>
|
<a
|
||||||
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a>
|
class="has-text-primary ml-5"
|
||||||
|
@click="$copyToClipboard(apiUrl)"
|
||||||
|
>copy</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="has-text-primary ml-5"
|
||||||
|
target="_blank"
|
||||||
|
:href="apiUrl"
|
||||||
|
>open</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Caption class="mb-2" v-bind="{ title: 'Data', type: 'has-text-warning' }"></Caption>
|
<Caption
|
||||||
<DataTable v-bind="{ pagename: pagename }" v-if="pagedata" />
|
class="mb-2"
|
||||||
|
v-bind="{ title: 'Data', type: 'has-text-warning' }"
|
||||||
|
></Caption>
|
||||||
|
<DataTable
|
||||||
|
v-bind="{ pagename: pagename }"
|
||||||
|
v-if="pagedata"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStore } from "@/stores/index";
|
import { useStore } from "@/stores/index";
|
||||||
|
|||||||
@@ -1,26 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tabs is-boxed">
|
<div class="tabs is-boxed">
|
||||||
<ul>
|
<ul>
|
||||||
<li :class="selectType.code===v.code? 'is-active fs-16' : 'fs-16'" v-for="v in fieldType">
|
<li
|
||||||
<a @click="selectType = v"><span>{{ v.name }}</span></a>
|
:class="selectType.code === v.code ? 'is-active fs-16' : 'fs-16'"
|
||||||
|
v-for="v in fieldType"
|
||||||
|
>
|
||||||
|
<a @click="selectType = v"
|
||||||
|
><span>{{ v.name }}</span></a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="selectType.code==='formula'">
|
<template v-if="selectType.code === 'formula'">
|
||||||
<b-radio :class="i===1? 'ml-5' : null" v-model="choice" v-for="(v,i) in choices" :key="i"
|
<b-radio
|
||||||
:native-value="v.code">
|
:class="i === 1 ? 'ml-5' : null"
|
||||||
<span :class="v.code===choice? 'fsb-16' : 'fs-16'">{{v.name}}</span>
|
v-model="choice"
|
||||||
</b-radio>
|
v-for="(v, i) in choices"
|
||||||
<div class="has-background-light mt-3 px-3 py-3">
|
:key="i"
|
||||||
<div class="tags are-medium mb-0" v-if="choice==='function'">
|
:native-value="v.code"
|
||||||
<span :class="`tag ${func===v.code? 'is-primary' : 'is-dark'} is-rounded is-clickable`"
|
>
|
||||||
v-for="(v,i) in funcs" :key="i" @click="changeFunc(v)" @dblclick="addFunc(v)">{{v.name}}</span>
|
<span :class="v.code === choice ? 'fsb-16' : 'fs-16'">{{ v.name }}</span>
|
||||||
|
</b-radio>
|
||||||
|
<div class="has-background-light mt-3 px-3 py-3">
|
||||||
|
<div
|
||||||
|
class="tags are-medium mb-0"
|
||||||
|
v-if="choice === 'function'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="`tag ${func === v.code ? 'is-primary' : 'is-dark'} is-rounded is-clickable`"
|
||||||
|
v-for="(v, i) in funcs"
|
||||||
|
:key="i"
|
||||||
|
@click="changeFunc(v)"
|
||||||
|
@dblclick="addFunc(v)"
|
||||||
|
>{{ v.name }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="field px-0 mx-0">
|
<div class="field px-0 mx-0">
|
||||||
<label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label>
|
<label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<!--<b-taginput
|
<!--<b-taginput
|
||||||
size="is-small"
|
size="is-small"
|
||||||
v-model="tags"
|
v-model="tags"
|
||||||
:data="fields.filter(v=>v.format==='number')"
|
:data="fields.filter(v=>v.format==='number')"
|
||||||
@@ -39,296 +58,418 @@
|
|||||||
Không có trường thỏa mãn
|
Không có trường thỏa mãn
|
||||||
</template>
|
</template>
|
||||||
</b-taginput>-->
|
</b-taginput>-->
|
||||||
</div>
|
</div>
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tags')"> {{errors.find(v=>v.name==='tags').message}} </p>
|
<p
|
||||||
</div>
|
class="help has-text-danger"
|
||||||
<div class="field mt-3" v-if="tags.length>0">
|
v-if="errors.find((v) => v.name === 'tags')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "tags").message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="field mt-3"
|
||||||
|
v-if="tags.length > 0"
|
||||||
|
>
|
||||||
<p class="help is-primary mb-1">Click đúp vào để thêm vào công thức tính.</p>
|
<p class="help is-primary mb-1">Click đúp vào để thêm vào công thức tính.</p>
|
||||||
<div class="tags mb-2">
|
<div class="tags mb-2">
|
||||||
<span @dblclick="formula = formula? (formula + ' ' + v.name) : v.name" class="tag is-dark is-rounded is-clickable"
|
<span
|
||||||
v-for="v in tags">
|
@dblclick="formula = formula ? formula + ' ' + v.name : v.name"
|
||||||
{{v.name}}
|
class="tag is-dark is-rounded is-clickable"
|
||||||
</span>
|
v-for="v in tags"
|
||||||
|
>
|
||||||
|
{{ v.name }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<span v-for="(v,i) in operator" :key="i">
|
<span
|
||||||
<span @dblclick="addOperator(v)" class="tag is-primary is-rounded is-clickable mr-4">
|
v-for="(v, i) in operator"
|
||||||
<span class="fs-16">{{v.code}}</span>
|
:key="i"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
@dblclick="addOperator(v)"
|
||||||
|
class="tag is-primary is-rounded is-clickable mr-4"
|
||||||
|
>
|
||||||
|
<span class="fs-16">{{ v.code }}</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="field mt-3 px-0 mx-0">
|
<div class="field mt-3 px-0 mx-0">
|
||||||
<label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<textarea class="textarea" rows="3" type="text" :placeholder="placeholder" v-model="formula"> </textarea>
|
<textarea
|
||||||
</p>
|
class="textarea"
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='formula')"> {{errors.find(v=>v.name==='formula').message}} </p>
|
rows="3"
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field is-horizontal mt-3 px-0 mx-0">
|
|
||||||
<div class="field-body">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label fs-14">Hiển thị theo <span class="has-text-danger"> * </span> </label>
|
|
||||||
<div class="control">
|
|
||||||
<b-autocomplete
|
|
||||||
size="is-small"
|
|
||||||
icon-right="magnify"
|
|
||||||
:value="selectUnit? selectUnit.name : ''"
|
|
||||||
placeholder=""
|
|
||||||
:keep-first=true
|
|
||||||
:open-on-focus=true
|
|
||||||
:data="moneyunit"
|
|
||||||
field="name"
|
|
||||||
@select="option => selectUnit = option">
|
|
||||||
</b-autocomplete>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label fs-14">Phần thập phân</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input is-small" type="text" placeholder="" v-model="decimal">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="field px-0 mx-0">
|
|
||||||
<label class="label">Tên trường <span class="has-text-danger"> * </span> </label>
|
|
||||||
<p class="control">
|
|
||||||
<input class="input" type="text" placeholder="Tên trường phải là duy nhất" v-model="name"
|
|
||||||
:readonly="selectType? selectType.code==='formula': false">
|
|
||||||
</p>
|
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='name')"> {{errors.find(v=>v.name==='name').message}} </p>
|
|
||||||
<p class="help has-text-primary" v-else> Tên trường do hệ thống tự sinh.</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<label class="label">Mô tả<span class="has-text-danger"> *</span></label>
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded" >
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="text"
|
type="text"
|
||||||
v-model="label"
|
:placeholder="placeholder"
|
||||||
/>
|
v-model="formula"
|
||||||
</div>
|
>
|
||||||
<div class="control">
|
</textarea>
|
||||||
<button class="button" @click="editLabel()">
|
</p>
|
||||||
<span><SvgIcon v-bind="{name: 'pen.svg', type: 'dark', size: 17}"></SvgIcon></span>
|
<p
|
||||||
</button>
|
class="help has-text-danger"
|
||||||
</div>
|
v-if="errors.find((v) => v.name === 'formula')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "formula").message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='label')"> {{errors.find(v=>v.name==='label').message}} </p>
|
</div>
|
||||||
</div>
|
<div class="field is-horizontal mt-3 px-0 mx-0">
|
||||||
<div class="field mt-5" v-if="selectType.code==='empty'">
|
<div class="field-body">
|
||||||
<label class="label"
|
<div class="field">
|
||||||
>Kiểu dữ liệu
|
<label class="label fs-14">Hiển thị theo <span class="has-text-danger"> * </span> </label>
|
||||||
<span class="has-text-danger"> * </span>
|
<div class="control">
|
||||||
</label>
|
<b-autocomplete
|
||||||
<div class="control fs-14">
|
size="is-small"
|
||||||
<span class="mr-4" v-for="(v,i) in datatype">
|
icon-right="magnify"
|
||||||
<a class="icon-text" @click="changeType(v)">
|
:value="selectUnit ? selectUnit.name : ''"
|
||||||
<SvgIcon v-bind="{name: `radio-${radioType.code===v.code? '' : 'un'}checked.svg`, type: 'gray', size: 22}"></SvgIcon>
|
placeholder=""
|
||||||
</a>
|
:keep-first="true"
|
||||||
{{v.name}}
|
:open-on-focus="true"
|
||||||
</span>
|
:data="moneyunit"
|
||||||
|
field="name"
|
||||||
|
@select="(option) => (selectUnit = option)"
|
||||||
|
>
|
||||||
|
</b-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mt-5">
|
<div class="field">
|
||||||
<p class="control">
|
<label class="label fs-14">Phần thập phân</label>
|
||||||
<a class="button is-primary has-text-white"
|
<div class="control">
|
||||||
@click="selectType.code==='formula'? createField() : createEmptyField()">Tạo cột</a>
|
<input
|
||||||
</p>
|
class="input is-small"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
v-model="decimal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Modal v-bind="showmodal" v-if="showmodal" @label="changeLabel" @close="close"></Modal>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="field px-0 mx-0">
|
||||||
|
<label class="label">Tên trường <span class="has-text-danger"> * </span> </label>
|
||||||
|
<p class="control">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Tên trường phải là duy nhất"
|
||||||
|
v-model="name"
|
||||||
|
:readonly="selectType ? selectType.code === 'formula' : false"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'name')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "name").message }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="help has-text-primary"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
Tên trường do hệ thống tự sinh.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<label class="label">Mô tả<span class="has-text-danger"> *</span></label>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
@click="editLabel()"
|
||||||
|
>
|
||||||
|
<span><SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 17 }"></SvgIcon></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="help has-text-danger"
|
||||||
|
v-if="errors.find((v) => v.name === 'label')"
|
||||||
|
>
|
||||||
|
{{ errors.find((v) => v.name === "label").message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="field mt-5"
|
||||||
|
v-if="selectType.code === 'empty'"
|
||||||
|
>
|
||||||
|
<label class="label"
|
||||||
|
>Kiểu dữ liệu
|
||||||
|
<span class="has-text-danger"> * </span>
|
||||||
|
</label>
|
||||||
|
<div class="control fs-14">
|
||||||
|
<span
|
||||||
|
class="mr-4"
|
||||||
|
v-for="(v, i) in datatype"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="icon-text"
|
||||||
|
@click="changeType(v)"
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
v-bind="{
|
||||||
|
name: `radio-${radioType.code === v.code ? '' : 'un'}checked.svg`,
|
||||||
|
type: 'gray',
|
||||||
|
size: 22,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
|
</a>
|
||||||
|
{{ v.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-5">
|
||||||
|
<p class="control">
|
||||||
|
<a
|
||||||
|
class="button is-primary has-text-white"
|
||||||
|
@click="selectType.code === 'formula' ? createField() : createEmptyField()"
|
||||||
|
>Tạo cột</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
@label="changeLabel"
|
||||||
|
@close="close"
|
||||||
|
></Modal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStore } from '@/stores/index'
|
import { useStore } from "@/stores/index";
|
||||||
import ScrollBox from '~/components/datatable/ScrollBox'
|
import ScrollBox from "~/components/datatable/ScrollBox";
|
||||||
const emit = defineEmits(['modalevent'])
|
const emit = defineEmits(["modalevent"]);
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp()
|
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp();
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
pagename: String,
|
pagename: String,
|
||||||
field: Object,
|
field: Object,
|
||||||
filters: Object,
|
filters: Object,
|
||||||
filterData: Object,
|
filterData: Object,
|
||||||
width: String
|
width: String,
|
||||||
})
|
});
|
||||||
const moneyunit = store.moneyunit
|
const moneyunit = store.moneyunit;
|
||||||
const datatype = store.datatype
|
const datatype = store.datatype;
|
||||||
var showmodal = ref()
|
var showmodal = ref();
|
||||||
var pagedata = store[props.pagename]
|
var pagedata = store[props.pagename];
|
||||||
var selectUnit = moneyunit.find(v=>v.code==='one')
|
var selectUnit = moneyunit.find((v) => v.code === "one");
|
||||||
var data = []
|
var data = [];
|
||||||
var current = 1
|
var current = 1;
|
||||||
var filterData = []
|
var filterData = [];
|
||||||
var loading = false
|
var loading = false;
|
||||||
var fieldType = [{code: 'formula', name: 'Tạo công thức'}, {code: 'empty', name: 'Tạo cột rỗng'}]
|
var fieldType = [
|
||||||
var errors = []
|
{ code: "formula", name: "Tạo công thức" },
|
||||||
var tags = []
|
{ code: "empty", name: "Tạo cột rỗng" },
|
||||||
var formula = undefined
|
];
|
||||||
var name = `f${$id().toLocaleLowerCase()}`
|
var errors = [];
|
||||||
var label = undefined
|
var tags = [];
|
||||||
var errors = []
|
var formula = undefined;
|
||||||
var selectType = fieldType.find(v=>v.code==='empty')
|
var name = `f${$id().toLocaleLowerCase()}`;
|
||||||
var radioType = ref(datatype.find(v=>v.code==='string'))
|
var label = undefined;
|
||||||
var fields = []
|
var errors = [];
|
||||||
var options = undefined
|
var selectType = fieldType.find((v) => v.code === "empty");
|
||||||
var columns = $copy(pagedata.fields.filter(v=>v.format==='number'))
|
var radioType = ref(datatype.find((v) => v.code === "string"));
|
||||||
var decimal = undefined
|
var fields = [];
|
||||||
var choices = [{code: 'column', name: 'Dùng cột dữ liệu'}, {code: 'function', name: 'Dùng hàm số'}]
|
var options = undefined;
|
||||||
var choice = 'column'
|
var columns = $copy(pagedata.fields.filter((v) => v.format === "number"));
|
||||||
var funcs = [{code: 'sum', name: 'Sum'}, {code: 'max', name: 'Max'}, {code: 'min', name: 'Min'}, {code: 'avg', name: 'Avg'}]
|
var decimal = undefined;
|
||||||
var func = 'sum'
|
var choices = [
|
||||||
var placeholder = 'Minh hoạ công thức: f10001 + f10002'
|
{ code: "column", name: "Dùng cột dữ liệu" },
|
||||||
var args = undefined
|
{ code: "function", name: "Dùng hàm số" },
|
||||||
var operator = [{code: '+', name: 'Cộng'}, {code: '-', name: 'Trừ'}, {code: '*', name: 'Nhân'}, {code: '/', name: 'Chia'}, {code: '>', name: 'Lớn hơn'},
|
];
|
||||||
{code: '>=', name: 'Lớn hơn hoặc bằng'}, {code: '<', name: 'Nhỏ hơn'}, {code: '<=', name: 'Nhỏ hơn hoặc bằng'}, {code: '==', name: 'Bằng'},
|
var choice = "column";
|
||||||
{code: '&&', name: 'Và'}, {code: '||', name: 'Hoặc'}, {code: 'iif', name: 'Điều kiện rẽ nhánh'}]
|
var funcs = [
|
||||||
function editLabel() {
|
{ code: "sum", name: "Sum" },
|
||||||
if($empty(label)) return
|
{ code: "max", name: "Max" },
|
||||||
showmodal.value = {component: 'datatable/EditLabel', width: '500px', height: '300px', vbind: {label: label}}
|
{ code: "min", name: "Min" },
|
||||||
}
|
{ code: "avg", name: "Avg" },
|
||||||
function close() {
|
];
|
||||||
showmodal.value = null
|
var func = "sum";
|
||||||
}
|
var placeholder = "Minh hoạ công thức: f10001 + f10002";
|
||||||
function changeLabel(evt) {
|
var args = undefined;
|
||||||
label = evt
|
var operator = [
|
||||||
showmodal.value = null
|
{ code: "+", name: "Cộng" },
|
||||||
}
|
{ code: "-", name: "Trừ" },
|
||||||
function changeType(v) {
|
{ code: "*", name: "Nhân" },
|
||||||
radioType.value = v
|
{ code: "/", name: "Chia" },
|
||||||
}
|
{ code: ">", name: "Lớn hơn" },
|
||||||
function addFunc(v) {
|
{ code: ">=", name: "Lớn hơn hoặc bằng" },
|
||||||
formula = (formula? formula + ' ' : '') + v.name + '(C0: C2)'
|
{ code: "<", name: "Nhỏ hơn" },
|
||||||
}
|
{ code: "<=", name: "Nhỏ hơn hoặc bằng" },
|
||||||
function addOperator(v) {
|
{ code: "==", name: "Bằng" },
|
||||||
let text = v.code==='iif'? 'a>b? c : d' : v.code
|
{ code: "&&", name: "Và" },
|
||||||
formula = `${formula || ''} ${text}`
|
{ code: "||", name: "Hoặc" },
|
||||||
}
|
{ code: "iif", name: "Điều kiện rẽ nhánh" },
|
||||||
function changeFunc(v) {
|
];
|
||||||
placeholder = `${v.name}(C0:C2) hoặc ${v.name}(C0,C1,C2). C là viết tắt của cột dữ liệu, số thứ tự của cột bắt đầu từ 0`
|
function editLabel() {
|
||||||
func = v.code
|
if ($empty(label)) return;
|
||||||
}
|
showmodal.value = {
|
||||||
function getFields() {
|
component: "datatable/EditLabel",
|
||||||
fields = pagedata? $copy(pagedata.fields) : []
|
width: "500px",
|
||||||
fields.map(v=>v.caption = (v.label? v.label.indexOf('<')>=0 : false)? v.name : v.label)
|
height: "300px",
|
||||||
}
|
vbind: { label: label },
|
||||||
function checkFunc() {
|
};
|
||||||
let error = false
|
}
|
||||||
let val = formula.trim().replaceAll(' ', '')
|
function close() {
|
||||||
if(val.toLowerCase().indexOf(func)<0) error = true
|
showmodal.value = null;
|
||||||
let start = val.toLowerCase().indexOf('(')
|
}
|
||||||
let end = val.toLowerCase().indexOf(')')
|
function changeLabel(evt) {
|
||||||
if( start<0 || end<0) error = true
|
label = evt;
|
||||||
let content = val.substring(start+1, end)
|
showmodal.value = null;
|
||||||
if($empty(content)) error = true
|
}
|
||||||
let content1 = content.replaceAll(':', ',')
|
function changeType(v) {
|
||||||
let arr = content1.split(',')
|
radioType.value = v;
|
||||||
arr.map(v=>{
|
}
|
||||||
let arr1 = v.toLowerCase().split('c')
|
function addFunc(v) {
|
||||||
if(arr1.length!==2) error = true
|
formula = (formula ? formula + " " : "") + v.name + "(C0: C2)";
|
||||||
else if(!$isNumber(arr1[1])) error = true
|
}
|
||||||
})
|
function addOperator(v) {
|
||||||
return error? 'error' : content
|
let text = v.code === "iif" ? "a>b? c : d" : v.code;
|
||||||
}
|
formula = `${formula || ""} ${text}`;
|
||||||
function checkValid() {
|
}
|
||||||
errors = []
|
function changeFunc(v) {
|
||||||
if(tags.length===0 && choice==='column') {
|
placeholder = `${v.name}(C0:C2) hoặc ${v.name}(C0,C1,C2). C là viết tắt của cột dữ liệu, số thứ tự của cột bắt đầu từ 0`;
|
||||||
errors.push({name: 'tags', message: 'Chưa chọn trường xây dựng công thức.'})
|
func = v.code;
|
||||||
|
}
|
||||||
|
function getFields() {
|
||||||
|
fields = pagedata ? $copy(pagedata.fields) : [];
|
||||||
|
fields.map((v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label));
|
||||||
|
}
|
||||||
|
function checkFunc() {
|
||||||
|
let error = false;
|
||||||
|
let val = formula.trim().replaceAll(" ", "");
|
||||||
|
if (val.toLowerCase().indexOf(func) < 0) error = true;
|
||||||
|
let start = val.toLowerCase().indexOf("(");
|
||||||
|
let end = val.toLowerCase().indexOf(")");
|
||||||
|
if (start < 0 || end < 0) error = true;
|
||||||
|
let content = val.substring(start + 1, end);
|
||||||
|
if ($empty(content)) error = true;
|
||||||
|
let content1 = content.replaceAll(":", ",");
|
||||||
|
let arr = content1.split(",");
|
||||||
|
arr.map((v) => {
|
||||||
|
let arr1 = v.toLowerCase().split("c");
|
||||||
|
if (arr1.length !== 2) error = true;
|
||||||
|
else if (!$isNumber(arr1[1])) error = true;
|
||||||
|
});
|
||||||
|
return error ? "error" : content;
|
||||||
|
}
|
||||||
|
function checkValid() {
|
||||||
|
errors = [];
|
||||||
|
if (tags.length === 0 && choice === "column") {
|
||||||
|
errors.push({
|
||||||
|
name: "tags",
|
||||||
|
message: "Chưa chọn trường xây dựng công thức.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!$empty(formula) ? $empty(formula.trim()) : true) {
|
||||||
|
errors.push({ name: "formula", message: "Công thức không được bỏ trống." });
|
||||||
|
}
|
||||||
|
if (!$empty(label) ? $empty(label.trim()) : true)
|
||||||
|
errors.push({ name: "label", message: "Mô tả không được bỏ trống." });
|
||||||
|
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
|
||||||
|
errors.push({
|
||||||
|
name: "label",
|
||||||
|
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (errors.length > 0) return false;
|
||||||
|
//check formula in case use column
|
||||||
|
if (choice === "column") {
|
||||||
|
let val = $copy(formula);
|
||||||
|
tags.forEach((v) => {
|
||||||
|
let myRegExp = new RegExp(v.name, "g");
|
||||||
|
val = val.replace(myRegExp, Math.random());
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
let value = $calc(val);
|
||||||
|
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
|
||||||
|
errors.push({ name: "formula", message: "Công thức không hợp lệ" });
|
||||||
}
|
}
|
||||||
if(!$empty(formula)? $empty(formula.trim()) : true) {
|
} catch (err) {
|
||||||
errors.push({name: 'formula', message: 'Công thức không được bỏ trống.'})
|
console.log(err);
|
||||||
}
|
errors.push({ name: "formula", message: "Công thức không hợp lệ" });
|
||||||
if(!$empty(label)? $empty(label.trim()) : true )
|
|
||||||
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
|
||||||
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) {
|
|
||||||
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
|
||||||
}
|
|
||||||
if(errors.length>0) return false
|
|
||||||
//check formula in case use column
|
|
||||||
if(choice==='column') {
|
|
||||||
let val = $copy(formula)
|
|
||||||
tags.forEach(v => {
|
|
||||||
let myRegExp = new RegExp(v.name, 'g')
|
|
||||||
val = val.replace(myRegExp, Math.random())
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
let value = $calc(val)
|
|
||||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
|
||||||
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
console.log(err)
|
|
||||||
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(checkFunc()==='error') errors.push({name: 'formula', message: `Hàm ${func.toUpperCase()} không hợp lệ`})
|
|
||||||
}
|
|
||||||
return errors.length>0? false : true
|
|
||||||
}
|
|
||||||
function createField() {
|
|
||||||
if(!checkValid()) return
|
|
||||||
let field = $createField(name.trim(), label.trim(), 'number', true)
|
|
||||||
field.formula = formula.trim().replaceAll(' ', '')
|
|
||||||
if(choice==='function') {
|
|
||||||
field.func = func
|
|
||||||
field.vals = checkFunc()
|
|
||||||
} else field.tags = tags.map(v=>v.name)
|
|
||||||
field.level = Math.max(...pagedata.fields.map(v=>v.level? v.level : 0)) + 1
|
|
||||||
field.unit = selectUnit.detail
|
|
||||||
field.decimal = decimal
|
|
||||||
field.disable = 'search,value'
|
|
||||||
let copy = $copy(pagedata)
|
|
||||||
copy.fields.push(field)
|
|
||||||
store.commit(props.pagename, copy)
|
|
||||||
emit('newfield', field)
|
|
||||||
tags = []
|
|
||||||
formula = undefined
|
|
||||||
label = undefined
|
|
||||||
name = `f${$id()}`
|
|
||||||
emit('close')
|
|
||||||
}
|
}
|
||||||
function createEmptyField() {
|
} else {
|
||||||
errors = []
|
if (checkFunc() === "error")
|
||||||
if(!$empty(name)? $empty(name.trim()) : true )
|
errors.push({
|
||||||
errors.push({name: 'name', message: 'Tên không được bỏ trống.'})
|
name: "formula",
|
||||||
else if(pagedata.fields.find(v=>v.name.toLowerCase()===name.toLowerCase())) {
|
message: `Hàm ${func.toUpperCase()} không hợp lệ`,
|
||||||
errors.push({name: 'name', message: 'Tên trường bị trùng. Hãy đặt tên khác.'})
|
});
|
||||||
}
|
}
|
||||||
if(!$empty(label)? $empty(label.trim()) : true )
|
return errors.length > 0 ? false : true;
|
||||||
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
}
|
||||||
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) {
|
function createField() {
|
||||||
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
if (!checkValid()) return;
|
||||||
}
|
let field = $createField(name.trim(), label.trim(), "number", true);
|
||||||
if(errors.length>0) return
|
field.formula = formula.trim().replaceAll(" ", "");
|
||||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true)
|
if (choice === "function") {
|
||||||
if(selectType.code==='chart') field = createChartField()
|
field.func = func;
|
||||||
let copy = $clone(pagedata)
|
field.vals = checkFunc();
|
||||||
copy.fields.push(field)
|
} else field.tags = tags.map((v) => v.name);
|
||||||
copy.update = {fields: copy.fields}
|
field.level = Math.max(...pagedata.fields.map((v) => (v.level ? v.level : 0))) + 1;
|
||||||
store.commit(props.pagename, copy)
|
field.unit = selectUnit.detail;
|
||||||
//pagedata = copy
|
field.decimal = decimal;
|
||||||
emit('newfield', field)
|
field.disable = "search,value";
|
||||||
label = undefined
|
let copy = $copy(pagedata);
|
||||||
name = `f${$id()}`
|
copy.fields.push(field);
|
||||||
emit('close')
|
store.commit(props.pagename, copy);
|
||||||
}
|
emit("newfield", field);
|
||||||
function createChartField() {
|
tags = [];
|
||||||
let array = pagedata.fields.filter(v=>v.format==='number' && v.show)
|
formula = undefined;
|
||||||
if(args) array = $copy(args)
|
label = undefined;
|
||||||
let text = ''
|
name = `f${$id()}`;
|
||||||
array.map((v,i)=>text += `'${v.name}${i<array.length-1? "'," : "'"}`)
|
emit("close");
|
||||||
let label = ''
|
}
|
||||||
array.map((v,i)=>label += `'${$stripHtml(v.label)}${i<array.length-1? "'," : "'"}`)
|
function createEmptyField() {
|
||||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true)
|
errors = [];
|
||||||
field.chart = 'yes'
|
if (!$empty(name) ? $empty(name.trim()) : true) errors.push({ name: "name", message: "Tên không được bỏ trống." });
|
||||||
field.template = `<TrendingChart class="is-clickable" v-bind="{row: row, fields: [${text}], labels: [${label}], width: '80', height: '26', 'header': ['stock_code', 'name']}"/>`
|
else if (pagedata.fields.find((v) => v.name.toLowerCase() === name.toLowerCase())) {
|
||||||
return field
|
errors.push({
|
||||||
}
|
name: "name",
|
||||||
//============
|
message: "Tên trường bị trùng. Hãy đặt tên khác.",
|
||||||
getFields()
|
});
|
||||||
</script>
|
}
|
||||||
|
if (!$empty(label) ? $empty(label.trim()) : true)
|
||||||
|
errors.push({ name: "label", message: "Mô tả không được bỏ trống." });
|
||||||
|
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
|
||||||
|
errors.push({
|
||||||
|
name: "label",
|
||||||
|
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (errors.length > 0) return;
|
||||||
|
let field = $createField(name.trim(), label.trim(), radioType.value.code, true);
|
||||||
|
if (selectType.code === "chart") field = createChartField();
|
||||||
|
let copy = $clone(pagedata);
|
||||||
|
copy.fields.push(field);
|
||||||
|
copy.update = { fields: copy.fields };
|
||||||
|
store.commit(props.pagename, copy);
|
||||||
|
//pagedata = copy
|
||||||
|
emit("newfield", field);
|
||||||
|
label = undefined;
|
||||||
|
name = `f${$id()}`;
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
function createChartField() {
|
||||||
|
let array = pagedata.fields.filter((v) => v.format === "number" && v.show);
|
||||||
|
if (args) array = $copy(args);
|
||||||
|
let text = "";
|
||||||
|
array.map((v, i) => (text += `'${v.name}${i < array.length - 1 ? "'," : "'"}`));
|
||||||
|
let label = "";
|
||||||
|
array.map((v, i) => (label += `'${$stripHtml(v.label)}${i < array.length - 1 ? "'," : "'"}`));
|
||||||
|
let field = $createField(name.trim(), label.trim(), radioType.value.code, true);
|
||||||
|
field.chart = "yes";
|
||||||
|
field.template = `<TrendingChart class="is-clickable" v-bind="{row: row, fields: [${text}], labels: [${label}], width: '80', height: '26', 'header': ['stock_code', 'name']}"/>`;
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
//============
|
||||||
|
getFields();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,64 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="pagination mx-0" role="navigation" aria-label="pagination">
|
<nav
|
||||||
<ul class="pagination-list" v-if="pageInfo">
|
class="pagination mx-0"
|
||||||
<li v-for="v in pageInfo">
|
role="navigation"
|
||||||
<a v-if="currentPage===v" class="pagination-link is-current has-background-primary has-text-white" :aria-label="`Page ${v}`" aria-current="page">{{ v }}</a>
|
aria-label="pagination"
|
||||||
<a v-else href="#" class="pagination-link" :aria-label="`Goto page ${v}`" @click="changePage(v)">{{ v }}</a>
|
>
|
||||||
</li>
|
<ul
|
||||||
<a @click="previous()" class="pagination-previous ml-5">
|
class="pagination-list"
|
||||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon>
|
v-if="pageInfo"
|
||||||
</a>
|
>
|
||||||
<a @click="next()" class="pagination-next">
|
<li v-for="v in pageInfo">
|
||||||
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon>
|
<a
|
||||||
</a>
|
v-if="currentPage === v"
|
||||||
</ul>
|
class="pagination-link is-current has-background-primary has-text-white"
|
||||||
</nav>
|
:aria-label="`Page ${v}`"
|
||||||
|
aria-current="page"
|
||||||
|
>{{ v }}</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
href="#"
|
||||||
|
class="pagination-link"
|
||||||
|
:aria-label="`Goto page ${v}`"
|
||||||
|
@click="changePage(v)"
|
||||||
|
>{{ v }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<a
|
||||||
|
@click="previous()"
|
||||||
|
class="pagination-previous ml-5"
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
v-bind="{
|
||||||
|
name: 'left1.svg',
|
||||||
|
type: 'dark',
|
||||||
|
size: 20,
|
||||||
|
alt: 'Tìm kiếm',
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
@click="next()"
|
||||||
|
class="pagination-next"
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
v-bind="{
|
||||||
|
name: 'right.svg',
|
||||||
|
type: 'dark',
|
||||||
|
size: 20,
|
||||||
|
alt: 'Tìm kiếm',
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['changepage'])
|
const emit = defineEmits(["changepage"]);
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
data: Array,
|
data: Array,
|
||||||
perPage: Number
|
perPage: Number,
|
||||||
})
|
});
|
||||||
var currentPage = 1
|
var currentPage = 1;
|
||||||
var totalRows = props.data.length
|
var totalRows = props.data.length;
|
||||||
var lastPage = parseInt(totalRows / props.perPage)
|
var lastPage = parseInt(totalRows / props.perPage);
|
||||||
if(lastPage*props.perPage<totalRows) lastPage += 1
|
if (lastPage * props.perPage < totalRows) lastPage += 1;
|
||||||
var pageInfo = ref()
|
var pageInfo = ref();
|
||||||
function pages(current_page, last_page, onSides = 2) {
|
function pages(current_page, last_page, onSides = 2) {
|
||||||
// pages
|
// pages
|
||||||
let pages = [];
|
let pages = [];
|
||||||
// Loop through
|
// Loop through
|
||||||
for (let i = 1; i <= last_page; i++) {
|
for (let i = 1; i <= last_page; i++) {
|
||||||
// Define offset
|
// Define offset
|
||||||
let offset = (i == 1 || last_page) ? onSides + 1 : onSides;
|
let offset = i == 1 || last_page ? onSides + 1 : onSides;
|
||||||
// If added
|
// If added
|
||||||
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) ||
|
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) || i == current_page || i == last_page) {
|
||||||
i == current_page || i == last_page) {
|
pages.push(i);
|
||||||
pages.push(i);
|
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
|
||||||
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
|
pages.push("...");
|
||||||
pages.push('...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pages;
|
|
||||||
}
|
}
|
||||||
function changePage(page) {
|
}
|
||||||
if(page==='...') return
|
return pages;
|
||||||
currentPage = page
|
}
|
||||||
pageInfo.value = pages(page, lastPage, 2)
|
function changePage(page) {
|
||||||
emit('changepage', page)
|
if (page === "...") return;
|
||||||
}
|
currentPage = page;
|
||||||
pageInfo.value = pages(1, lastPage, 2)
|
pageInfo.value = pages(page, lastPage, 2);
|
||||||
watch(() => props.data, (newVal, oldVal) => {
|
emit("changepage", page);
|
||||||
totalRows = props.data.length
|
}
|
||||||
lastPage = parseInt(totalRows / props.perPage)
|
pageInfo.value = pages(1, lastPage, 2);
|
||||||
if(lastPage*props.perPage<totalRows) lastPage += 1
|
watch(
|
||||||
pageInfo.value = pages(1, lastPage, 2)
|
() => props.data,
|
||||||
})
|
(newVal, oldVal) => {
|
||||||
function previous() {
|
totalRows = props.data.length;
|
||||||
if(currentPage>1) changePage(currentPage-1)
|
lastPage = parseInt(totalRows / props.perPage);
|
||||||
}
|
if (lastPage * props.perPage < totalRows) lastPage += 1;
|
||||||
function next() {
|
pageInfo.value = pages(1, lastPage, 2);
|
||||||
if(currentPage<lastPage) changePage(currentPage+1)
|
},
|
||||||
}
|
);
|
||||||
</script>
|
function previous() {
|
||||||
|
if (currentPage > 1) changePage(currentPage - 1);
|
||||||
|
}
|
||||||
|
function next() {
|
||||||
|
if (currentPage < lastPage) changePage(currentPage + 1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="px-2" :style="`max-height: ${maxheight}; overflow-y: auto;`">
|
<div
|
||||||
<div
|
class="px-2"
|
||||||
v-for="(v, i) in rows" :key="i"
|
:style="`max-height: ${maxheight}; overflow-y: auto;`"
|
||||||
:class="[
|
>
|
||||||
'field is-grouped py-1 my-0',
|
<div
|
||||||
i !== rows.length - 1 && 'border-bottom'
|
v-for="(v, i) in rows"
|
||||||
]"
|
:key="i"
|
||||||
|
:class="['field is-grouped py-1 my-0', i !== rows.length - 1 && 'border-bottom']"
|
||||||
>
|
>
|
||||||
<p class="control is-expanded py-0 fs-14 hyperlink" @click="doClick(v,i)">
|
<p
|
||||||
{{ $stripHtml(v[name] || v.fullname || v.code || 'n/a', 75) }}
|
class="control is-expanded py-0 fs-14 hyperlink"
|
||||||
<span class="icon has-text-primary" v-if="checked[i] && notick!==true">
|
@click="doClick(v, i)"
|
||||||
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 15}"></SvgIcon>
|
>
|
||||||
|
{{ $stripHtml(v[name] || v.fullname || v.code || "n/a", 75) }}
|
||||||
|
<span
|
||||||
|
class="icon has-text-primary"
|
||||||
|
v-if="checked[i] && notick !== true"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 15 }"></SvgIcon>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="control py-0" v-if="show">
|
<p
|
||||||
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.author">
|
class="control py-0"
|
||||||
<SvgIcon v-bind="{name: 'user.svg', type: 'gray', size: 15}"></SvgIcon>
|
v-if="show"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon-text has-text-grey mr-2 fs-13"
|
||||||
|
v-if="show.author"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'user.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||||
<span>{{ v[show.author] }}</span>
|
<span>{{ v[show.author] }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.view">
|
<span
|
||||||
<SvgIcon v-bind="{name: 'view.svg', type: 'gray', size: 15}"></SvgIcon>
|
class="icon-text has-text-grey mr-2 fs-13"
|
||||||
|
v-if="show.view"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'view.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||||
<span>{{ v[show.view] }}</span>
|
<span>{{ v[show.view] }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="fs-13 has-text-grey" v-if="show.time">{{$dayjs(v['create_time']).fromNow(true)}}</span>
|
<span
|
||||||
<span class="tooltip">
|
class="fs-13 has-text-grey"
|
||||||
<a class="icon ml-1" v-if="show.link" @click="doClick(v,i, 'newtab')">
|
v-if="show.time"
|
||||||
<SvgIcon v-bind="{name: 'opennew.svg', type: 'gray', size: 15}"></SvgIcon>
|
>{{ $dayjs(v["create_time"]).fromNow(true) }}</span
|
||||||
|
>
|
||||||
|
<span class="tooltip">
|
||||||
|
<a
|
||||||
|
class="icon ml-1"
|
||||||
|
v-if="show.link"
|
||||||
|
@click="doClick(v, i, 'newtab')"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'opennew.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span class="tooltiptext">Mở trong tab mớ</span>
|
<span class="tooltiptext">Mở trong tab mớ</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip" v-if="show.rename">
|
<span
|
||||||
<a class="icon ml-1" @click="$emit('rename', v, i)">
|
class="tooltip"
|
||||||
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 15}"></SvgIcon>
|
v-if="show.rename"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="icon ml-1"
|
||||||
|
@click="$emit('rename', v, i)"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span class="tooltiptext">Đổi tên</span>
|
<span class="tooltiptext">Đổi tên</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip" v-if="show.rename">
|
<span
|
||||||
<a class="icon has-text-danger ml-1" @click="$emit('remove', v, i)">
|
class="tooltip"
|
||||||
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 15}"></SvgIcon>
|
v-if="show.rename"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="icon has-text-danger ml-1"
|
||||||
|
@click="$emit('remove', v, i)"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span class="tooltiptext">Xóa</span>
|
<span class="tooltiptext">Xóa</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -47,7 +83,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['data', 'name', 'maxheight', 'perpage', 'sort', 'selects', 'keyval', 'show', 'notick'],
|
props: ["data", "name", "maxheight", "perpage", "sort", "selects", "keyval", "show", "notick"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
@@ -56,62 +92,64 @@ export default {
|
|||||||
selected: [],
|
selected: [],
|
||||||
checked: {},
|
checked: {},
|
||||||
time: undefined,
|
time: undefined,
|
||||||
array: []
|
array: [],
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getdata()
|
this.getdata();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
data: function(newVal) {
|
data: function (newVal) {
|
||||||
this.getdata()
|
this.getdata();
|
||||||
|
},
|
||||||
|
selects: function (newVal) {
|
||||||
|
this.getSelect();
|
||||||
},
|
},
|
||||||
selects: function(newVal) {
|
|
||||||
this.getSelect()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getdata() {
|
getdata() {
|
||||||
this.currentPage = 1
|
this.currentPage = 1;
|
||||||
this.array = this.$copy(this.data)
|
this.array = this.$copy(this.data);
|
||||||
if(this.sort!==false) {
|
if (this.sort !== false) {
|
||||||
let f = {}
|
let f = {};
|
||||||
let showtime = this.show? this.show.time : false
|
let showtime = this.show ? this.show.time : false;
|
||||||
showtime? f['create_time'] = 'desc' : f[this.name] = 'asc'
|
showtime ? (f["create_time"] = "desc") : (f[this.name] = "asc");
|
||||||
this.$multiSort(this.array, f)
|
this.$multiSort(this.array, f);
|
||||||
}
|
}
|
||||||
this.rows = this.array.slice(0, this.perpage)
|
this.rows = this.array.slice(0, this.perpage);
|
||||||
this.getSelect()
|
this.getSelect();
|
||||||
},
|
},
|
||||||
getSelect() {
|
getSelect() {
|
||||||
if(!this.selects) return
|
if (!this.selects) return;
|
||||||
this.selected = []
|
this.selected = [];
|
||||||
this.checked = {}
|
this.checked = {};
|
||||||
this.selects.map(v=>{
|
this.selects.map((v) => {
|
||||||
let idx = this.rows.findIndex(x=>x[this.keyval? this.keyval : this.name]===v)
|
let idx = this.rows.findIndex((x) => x[this.keyval ? this.keyval : this.name] === v);
|
||||||
if(idx>=0) {
|
if (idx >= 0) {
|
||||||
this.selected.push(this.rows[idx])
|
this.selected.push(this.rows[idx]);
|
||||||
this.checked[idx] = true
|
this.checked[idx] = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
doClick(v, i, type) {
|
doClick(v, i, type) {
|
||||||
this.checked[i] = this.checked[i]? false : true
|
this.checked[i] = this.checked[i] ? false : true;
|
||||||
this.checked = this.$copy(this.checked)
|
this.checked = this.$copy(this.checked);
|
||||||
let idx = this.selected.findIndex(x=>x.id===v.id)
|
let idx = this.selected.findIndex((x) => x.id === v.id);
|
||||||
idx>=0? this.$remove(this.selected) : this.selected.push(v)
|
idx >= 0 ? this.$remove(this.selected) : this.selected.push(v);
|
||||||
this.$emit('selected', v, type)
|
this.$emit("selected", v, type);
|
||||||
},
|
},
|
||||||
handleScroll(e) {
|
handleScroll(e) {
|
||||||
const bottom = e.target.scrollHeight - e.target.scrollTop -5 < e.target.clientHeight
|
const bottom = e.target.scrollHeight - e.target.scrollTop - 5 < e.target.clientHeight;
|
||||||
if (bottom) {
|
if (bottom) {
|
||||||
if(this.total? this.total>this.rows.length : true) {
|
if (this.total ? this.total > this.rows.length : true) {
|
||||||
this.currentPage +=1
|
this.currentPage += 1;
|
||||||
let arr = this.array.filter((ele,index) => (index>=(this.currentPage-1)*this.perpage && index<this.currentPage*this.perpage))
|
let arr = this.array.filter(
|
||||||
this.rows = this.rows.concat(arr)
|
(ele, index) => index >= (this.currentPage - 1) * this.perpage && index < this.currentPage * this.perpage,
|
||||||
|
);
|
||||||
|
this.rows = this.rows.concat(arr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,17 +9,30 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="fs-14" v-for="(v, i) in fields">
|
<tr
|
||||||
|
class="fs-14"
|
||||||
|
v-for="(v, i) in fields"
|
||||||
|
>
|
||||||
<td>{{ i }}</td>
|
<td>{{ i }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="has-text-primary" @click="openField(v, i)">{{ v.name }}</a>
|
<a
|
||||||
|
class="has-text-primary"
|
||||||
|
@click="openField(v, i)"
|
||||||
|
>{{ v.name }}</a
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ $stripHtml(v.label, 50) }}</td>
|
<td>{{ $stripHtml(v.label, 50) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="mr-4" @click="moveDown(v, i)">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="moveDown(v, i)"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'down1.png', type: 'dark', size: 18 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'down1.png', type: 'dark', size: 18 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a class="mr-4" @click="moveUp(v, i)">
|
<a
|
||||||
|
class="mr-4"
|
||||||
|
@click="moveUp(v, i)"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'up.png', type: 'dark', size: 18 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'up.png', type: 'dark', size: 18 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a @click="askConfirm(v, i)">
|
<a @click="askConfirm(v, i)">
|
||||||
@@ -29,7 +42,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<Modal @close="showmodal = undefined" @update="update" @confirm="remove" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
@update="update"
|
||||||
|
@confirm="remove"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStore } from "@/stores/index";
|
import { useStore } from "@/stores/index";
|
||||||
|
|||||||
@@ -1,112 +1,144 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
|
||||||
<label class="label fs-14"> Cỡ chữ của bảng <span class="has-text-danger"> * </span> </label>
|
|
||||||
<p class="control fs-14">
|
|
||||||
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='table-font-size').detail"
|
|
||||||
@change="changeSetting($event.target.value, 'table-font-size')">
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field" >
|
|
||||||
<label class="label fs-14"> Cỡ chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
|
||||||
<p class="control fs-14">
|
|
||||||
<p class="control fs-14">
|
|
||||||
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='header-font-size').detail"
|
|
||||||
@change="changeSetting($event.target.value, 'header-font-size')">
|
|
||||||
</p>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label fs-14"> Số dòng trên 1 trang <span class="has-text-danger"> * </span> </label>
|
|
||||||
<p class="control fs-14">
|
|
||||||
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='per-page').detail"
|
|
||||||
@change="changeSetting($event.target.value, 'per-page')">
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field is-horizontal mt-5">
|
|
||||||
<div class="field-body">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label fs-14"> Màu nền bảng <span class="has-text-danger"> * </span> </label>
|
|
||||||
<p class="control fs-14">
|
|
||||||
<input type="color" :value="tablesetting.find(v=>v.code==='table-background').detail"
|
|
||||||
@change="changeSetting($event.target.value, 'table-background')">
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14"> Cỡ chữ của bảng <span class="has-text-danger"> * </span></label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<input type="color" :value="tablesetting.find(v=>v.code==='table-font-color').detail"
|
<input
|
||||||
@change="changeSetting($event.target.value, 'table-font-color')">
|
class="input is-small"
|
||||||
</p>
|
type="text"
|
||||||
|
:value="tablesetting.find((v) => v.code === 'table-font-size').detail"
|
||||||
|
@change="changeSetting($event.target.value, 'table-font-size')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label fs-14"> Cỡ chữ tiêu đề <span class="has-text-danger"> * </span></label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<input
|
||||||
|
class="input is-small"
|
||||||
|
type="text"
|
||||||
|
:value="tablesetting.find((v) => v.code === 'header-font-size').detail"
|
||||||
|
@change="changeSetting($event.target.value, 'header-font-size')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label fs-14"> Số dòng trên 1 trang <span class="has-text-danger"> * </span> </label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<input
|
||||||
|
class="input is-small"
|
||||||
|
type="text"
|
||||||
|
:value="tablesetting.find((v) => v.code === 'per-page').detail"
|
||||||
|
@change="changeSetting($event.target.value, 'per-page')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="field is-horizontal mt-5">
|
<div class="field is-horizontal mt-5">
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14"> Màu nền bảng <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-font-color').detail"
|
<input
|
||||||
@change="changeSetting($event.target.value, 'header-font-color')">
|
type="color"
|
||||||
</p>
|
:value="tablesetting.find((v) => v.code === 'table-background').detail"
|
||||||
</div>
|
@change="changeSetting($event.target.value, 'table-background')"
|
||||||
<div class="field">
|
/>
|
||||||
<label class="label fs-14"> Màu nền tiêu đề <span class="has-text-danger"> * </span> </label>
|
</p>
|
||||||
<p class="control fs-14">
|
</div>
|
||||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-background').detail"
|
<div class="field">
|
||||||
@change="changeSetting($event.target.value, 'header-background')">
|
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
|
||||||
</p>
|
<p class="control fs-14">
|
||||||
</div>
|
<input
|
||||||
<div class="field">
|
type="color"
|
||||||
<label class="label fs-14"> Màu chữ khi filter<span class="has-text-danger"> * </span> </label>
|
:value="tablesetting.find((v) => v.code === 'table-font-color').detail"
|
||||||
<p class="control fs-14">
|
@change="changeSetting($event.target.value, 'table-font-color')"
|
||||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-filter-color').detail"
|
/>
|
||||||
@change="changeSetting($event.target.value, 'header-filter-color')">
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="field is-horizontal mt-5">
|
<div class="field is-horizontal mt-5">
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field" >
|
<div class="field">
|
||||||
<label class="label fs-14"> Đường viền <span class="has-text-danger"> * </span> </label>
|
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||||
<p class="control fs-14">
|
<p class="control fs-14">
|
||||||
<input class="input is-small" type="text"
|
<input
|
||||||
:value="tablesetting.find(v=>v.code==='td-border')? tablesetting.find(v=>v.code==='td-border').detail : undefined"
|
type="color"
|
||||||
@change="changeSetting($event.target.value, 'td-border')">
|
:value="tablesetting.find((v) => v.code === 'header-font-color').detail"
|
||||||
</p>
|
@change="changeSetting($event.target.value, 'header-font-color')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label fs-14"> Màu nền tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
:value="tablesetting.find((v) => v.code === 'header-background').detail"
|
||||||
|
@change="changeSetting($event.target.value, 'header-background')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label fs-14"> Màu chữ khi filter<span class="has-text-danger"> * </span> </label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
:value="tablesetting.find((v) => v.code === 'header-filter-color').detail"
|
||||||
|
@change="changeSetting($event.target.value, 'header-filter-color')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field is-horizontal mt-5">
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label fs-14"> Đường viền <span class="has-text-danger"> * </span> </label>
|
||||||
|
<p class="control fs-14">
|
||||||
|
<input
|
||||||
|
class="input is-small"
|
||||||
|
type="text"
|
||||||
|
:value="
|
||||||
|
tablesetting.find((v) => v.code === 'td-border')
|
||||||
|
? tablesetting.find((v) => v.code === 'td-border').detail
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
@change="changeSetting($event.target.value, 'td-border')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStore } from '@/stores/index'
|
import { useStore } from "@/stores/index";
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
pagename: String
|
pagename: String,
|
||||||
})
|
});
|
||||||
const { $copy, $clone, $empty } = useNuxtApp()
|
const { $copy, $clone, $empty } = useNuxtApp();
|
||||||
var pagedata = $clone(store[props.pagename])
|
var pagedata = $clone(store[props.pagename]);
|
||||||
var errors = []
|
var errors = [];
|
||||||
var radioNote = 'no'
|
var radioNote = "no";
|
||||||
var tablesetting = pagedata.tablesetting
|
var tablesetting = pagedata.tablesetting;
|
||||||
let found = tablesetting.find(v=>v.code==='note')
|
let found = tablesetting.find((v) => v.code === "note");
|
||||||
if(found? found.detail!=='@' : false) radioNote = 'yes'
|
if (found ? found.detail !== "@" : false) radioNote = "yes";
|
||||||
function changeSetting(value, code) {
|
function changeSetting(value, code) {
|
||||||
if(code==='note' && $empty(value)) return
|
if (code === "note" && $empty(value)) return;
|
||||||
let copy = $copy(tablesetting)
|
let copy = $copy(tablesetting);
|
||||||
let found = copy.find(v=>v.code===code)
|
let found = copy.find((v) => v.code === code);
|
||||||
if(found) found.detail = value
|
if (found) found.detail = value;
|
||||||
else {
|
else {
|
||||||
found = $copy(tablesetting.find(v=>v.code===code))
|
found = $copy(tablesetting.find((v) => v.code === code));
|
||||||
found.detail = value
|
found.detail = value;
|
||||||
copy.push(found)
|
copy.push(found);
|
||||||
}
|
|
||||||
tablesetting = copy
|
|
||||||
pagedata.tablesetting = tablesetting
|
|
||||||
store.commit(props.pagename, pagedata)
|
|
||||||
}
|
}
|
||||||
</script>
|
tablesetting = copy;
|
||||||
|
pagedata.tablesetting = tablesetting;
|
||||||
|
store.commit(props.pagename, pagedata);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,92 +1,157 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pb-1" style="border-bottom: 2px solid #3c5b63" v-if="array || !enableTime">
|
<div
|
||||||
|
class="pb-1"
|
||||||
|
style="border-bottom: 2px solid #3c5b63"
|
||||||
|
v-if="array || !enableTime"
|
||||||
|
>
|
||||||
<div class="columns mx-0 mb-0">
|
<div class="columns mx-0 mb-0">
|
||||||
<div class="column is-8 px-0 pb-0" v-if="enableTime">
|
<div
|
||||||
|
class="column is-8 px-0 pb-0"
|
||||||
|
v-if="enableTime"
|
||||||
|
>
|
||||||
<div class="field is-grouped is-grouped-multiline mb-0">
|
<div class="field is-grouped is-grouped-multiline mb-0">
|
||||||
<div class="control mb-0">
|
<div class="control mb-0">
|
||||||
<Caption v-bind="{ title: lang === 'vi' ? 'Thời gian' : 'Time', type: 'has-text-warning' }" />
|
<Caption
|
||||||
|
v-bind="{
|
||||||
|
title: lang === 'vi' ? 'Thời gian' : 'Time',
|
||||||
|
type: 'has-text-warning',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control mb-0" v-for="v in array" :key="v.code">
|
<div
|
||||||
<span class="icon-text fsb-16 has-text-warning px-1" v-if="v.code === current">
|
class="control mb-0"
|
||||||
|
v-for="v in array"
|
||||||
|
:key="v.code"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon-text fsb-16 has-text-warning px-1"
|
||||||
|
v-if="v.code === current"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'tick.png', size: 20 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'tick.png', size: 20 }"></SvgIcon>
|
||||||
<span>{{ v.name }}</span>
|
<span>{{ v.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="icon-text has-text-grey hyperlink px-1 fsb-16" @click="changeOption(v)" v-else>{{
|
<span
|
||||||
v.name
|
class="icon-text has-text-grey hyperlink px-1 fsb-16"
|
||||||
}}</span>
|
@click="changeOption(v)"
|
||||||
|
v-else
|
||||||
|
>{{ v.name }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="newDataAvailable" class="has-text-danger is-italic is-size-6 ml-2">Có dữ liệu mới, vui lòng làm
|
<span
|
||||||
mới.</span>
|
v-if="newDataAvailable"
|
||||||
|
class="has-text-danger is-italic is-size-6 ml-2"
|
||||||
|
>Có dữ liệu mới, vui lòng làm mới.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4 px-0">
|
<div class="column is-4 px-0">
|
||||||
<div class="field is-grouped is-grouped-multiline mb-0">
|
<div class="field is-grouped is-grouped-multiline mb-0">
|
||||||
<div class="control mb-0">
|
<div class="control mb-0">
|
||||||
<Caption v-bind="{
|
<Caption
|
||||||
type: 'has-text-warning',
|
v-bind="{
|
||||||
title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search',
|
type: 'has-text-warning',
|
||||||
}" />
|
title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control mb-0">
|
<div class="control mb-0">
|
||||||
<input class="input is-small" type="text" v-model="text"
|
<input
|
||||||
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`" @keyup="startSearch"
|
class="input is-small"
|
||||||
id="input" :placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'" />
|
type="text"
|
||||||
|
v-model="text"
|
||||||
|
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`"
|
||||||
|
@keyup="startSearch"
|
||||||
|
id="input"
|
||||||
|
:placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="control mb-0">
|
<div class="control mb-0">
|
||||||
<span class="tooltip" v-if="importdata && $getEditRights()">
|
<span
|
||||||
<a class="mr-2" @click="openImport()">
|
class="tooltip"
|
||||||
<SvgIcon v-bind="{
|
v-if="importdata && $getEditRights()"
|
||||||
name: 'upload.svg',
|
>
|
||||||
type: 'findata',
|
<a
|
||||||
size: 22
|
class="mr-2"
|
||||||
}"></SvgIcon>
|
@click="openImport()"
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
v-bind="{
|
||||||
|
name: 'upload.svg',
|
||||||
|
type: 'findata',
|
||||||
|
size: 22,
|
||||||
|
}"
|
||||||
|
></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span class="tooltiptext" style="min-width: max-content">
|
<span
|
||||||
|
class="tooltiptext"
|
||||||
|
style="min-width: max-content"
|
||||||
|
>
|
||||||
{{ lang === "vi" ? "Nhập dữ liệu" : "Import data" }}
|
{{ lang === "vi" ? "Nhập dữ liệu" : "Import data" }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip" v-if="enableAdd && $getEditRights()">
|
<span
|
||||||
<a class="mr-2" @click="$emit('add')">
|
class="tooltip"
|
||||||
|
v-if="enableAdd && $getEditRights()"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="mr-2"
|
||||||
|
@click="$emit('add')"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'findata', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'add1.png', type: 'findata', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span class="tooltiptext" style="min-width: max-content">{{
|
<span
|
||||||
lang === "vi" ? "Thêm mới" : "Add new"
|
class="tooltiptext"
|
||||||
}}</span>
|
style="min-width: max-content"
|
||||||
|
>{{ lang === "vi" ? "Thêm mới" : "Add new" }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a class="mr-2" @click="$emit('excel')">
|
<a
|
||||||
|
class="mr-2"
|
||||||
|
@click="$emit('excel')"
|
||||||
|
>
|
||||||
<SvgIcon v-bind="{ name: 'excel.png', type: 'findata', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'excel.png', type: 'findata', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span class="tooltiptext" style="min-width: max-content">{{
|
<span
|
||||||
lang === "vi" ? "Xuất excel" : "Export excel"
|
class="tooltiptext"
|
||||||
}}</span>
|
style="min-width: max-content"
|
||||||
|
>{{ lang === "vi" ? "Xuất excel" : "Export excel" }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
<a @click="$emit('refresh-data')">
|
<a @click="$emit('refresh-data')">
|
||||||
<SvgIcon v-bind="{ name: 'refresh.svg', type: 'findata', size: 22 }"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'refresh.svg', type: 'findata', size: 22 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<span class="tooltiptext" style="min-width: max-content">{{
|
<span
|
||||||
lang === "vi" ? "Làm mới" : "Refresh"
|
class="tooltiptext"
|
||||||
}}</span>
|
style="min-width: max-content"
|
||||||
|
>{{ lang === "vi" ? "Làm mới" : "Refresh" }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<a class="button is-primary is-loading is-small ml-3" v-if="loading"></a>
|
<a
|
||||||
|
class="button is-primary is-loading is-small ml-3"
|
||||||
|
v-if="loading"
|
||||||
|
></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useStore } from "@/stores/index"
|
import { useStore } from "@/stores/index";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore()
|
const store = useStore();
|
||||||
return { store }
|
return { store };
|
||||||
},
|
},
|
||||||
|
|
||||||
props: ["pagename", "api", "timeopt", "filter", "realtime", "newDataAvailable", "params", "importdata"],
|
props: ["pagename", "api", "timeopt", "filter", "realtime", "newDataAvailable", "params", "importdata"],
|
||||||
@@ -117,19 +182,19 @@ export default {
|
|||||||
pagedata: undefined,
|
pagedata: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
pollingInterval: null,
|
pollingInterval: null,
|
||||||
searchableFields: [] // Lưu thông tin các field có thể tìm kiếm
|
searchableFields: [], // Lưu thông tin các field có thể tìm kiếm
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
pagename(newVal) {
|
pagename(newVal) {
|
||||||
this.updateSearchableFields()
|
this.updateSearchableFields();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
// Cập nhật searchable fields ngay từ đầu
|
// Cập nhật searchable fields ngay từ đầu
|
||||||
this.updateSearchableFields()
|
this.updateSearchableFields();
|
||||||
|
|
||||||
if (this.viewport < 5) {
|
if (this.viewport < 5) {
|
||||||
this.options = [
|
this.options = [
|
||||||
@@ -139,85 +204,83 @@ export default {
|
|||||||
{ code: 30, name: "1M" },
|
{ code: 30, name: "1M" },
|
||||||
{ code: 90, name: "3M" },
|
{ code: 90, name: "3M" },
|
||||||
{ code: 36000, name: "Tất cả" },
|
{ code: 36000, name: "Tất cả" },
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkTimeopt()
|
this.checkTimeopt();
|
||||||
if (!this.enableTime) return this.$emit("option")
|
if (!this.enableTime) return this.$emit("option");
|
||||||
|
|
||||||
let found = this.$findapi(this.api)
|
let found = this.$findapi(this.api);
|
||||||
found.commit = undefined
|
found.commit = undefined;
|
||||||
|
|
||||||
let filter = this.$copy(this.filter)
|
let filter = this.$copy(this.filter);
|
||||||
if (filter) {
|
if (filter) {
|
||||||
//dynamic parameter
|
//dynamic parameter
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
if (value.toString().indexOf("$") >= 0) {
|
if (value.toString().indexOf("$") >= 0) {
|
||||||
filter[key] = this.store[value.replace("$", "")].id
|
filter[key] = this.store[value.replace("$", "")].id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found.params.filter) {
|
if (found.params.filter) {
|
||||||
if (!filter) filter = {}
|
if (!filter) filter = {};
|
||||||
for (const [key, value] of Object.entries(found.params.filter)) {
|
for (const [key, value] of Object.entries(found.params.filter)) {
|
||||||
filter[key] = value
|
filter[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options.map((v) => {
|
this.options.map((v) => {
|
||||||
let f = filter ? this.$copy(filter) : {}
|
let f = filter ? this.$copy(filter) : {};
|
||||||
f["create_time__date__gte"] = this.$dayjs()
|
f["create_time__date__gte"] = this.$dayjs().subtract(v.code, "day").format("YYYY-MM-DD");
|
||||||
.subtract(v.code, "day")
|
v.filter = f;
|
||||||
.format("YYYY-MM-DD")
|
});
|
||||||
v.filter = f
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$emit("option", this.$find(this.options, { code: this.current }))
|
this.$emit("option", this.$find(this.options, { code: this.current }));
|
||||||
|
|
||||||
let f = {}
|
let f = {};
|
||||||
this.options.map((v) => {
|
this.options.map((v) => {
|
||||||
f[`${v.code}`] = {
|
f[`${v.code}`] = {
|
||||||
type: "Count",
|
type: "Count",
|
||||||
field: "create_time__date",
|
field: "create_time__date",
|
||||||
filter: v.filter
|
filter: v.filter,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
let params = { summary: "aggregate", distinct_values: f }
|
let params = { summary: "aggregate", distinct_values: f };
|
||||||
found.params = params
|
found.params = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let rs = await this.$getapi([found])
|
let rs = await this.$getapi([found]);
|
||||||
for (const [key, value] of Object.entries(rs[0].data.rows)) {
|
for (const [key, value] of Object.entries(rs[0].data.rows)) {
|
||||||
let found = this.$find(this.options, { code: Number(key) })
|
let found = this.$find(this.options, { code: Number(key) });
|
||||||
if (found) {
|
if (found) {
|
||||||
found.name = `${found.name} (${value})`
|
found.name = `${found.name} (${value})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data:", error)
|
console.error("Error fetching data:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.array = this.$copy(this.options)
|
this.array = this.$copy(this.options);
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.realtime) {
|
if (this.realtime) {
|
||||||
const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000
|
const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000;
|
||||||
this.pollingInterval = setInterval(this.refresh, interval)
|
this.pollingInterval = setInterval(this.refresh, interval);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.pollingInterval) {
|
if (this.pollingInterval) {
|
||||||
clearInterval(this.pollingInterval)
|
clearInterval(this.pollingInterval);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
lang: function () {
|
lang: function () {
|
||||||
return this.store.lang
|
return this.store.lang;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -226,140 +289,141 @@ export default {
|
|||||||
updateSearchableFields() {
|
updateSearchableFields() {
|
||||||
try {
|
try {
|
||||||
// Lấy API config
|
// Lấy API config
|
||||||
const found = this.$findapi(this.api)
|
const found = this.$findapi(this.api);
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
console.warn('Không tìm thấy API config')
|
console.warn("Không tìm thấy API config");
|
||||||
this.choices = []
|
this.choices = [];
|
||||||
this.searchableFields = []
|
this.searchableFields = [];
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ưu tiên lấy values từ props.params, nếu không có thì lấy từ API config
|
// Ưu tiên lấy values từ props.params, nếu không có thì lấy từ API config
|
||||||
let valuesString = ''
|
let valuesString = "";
|
||||||
|
|
||||||
if (this.params && this.params.values) {
|
if (this.params && this.params.values) {
|
||||||
// Lấy từ props.params (ưu tiên cao nhất)
|
// Lấy từ props.params (ưu tiên cao nhất)
|
||||||
valuesString = this.params.values
|
valuesString = this.params.values;
|
||||||
} else if (found.params && found.params.values) {
|
} else if (found.params && found.params.values) {
|
||||||
// Lấy từ API config mặc định
|
// Lấy từ API config mặc định
|
||||||
valuesString = found.params.values
|
valuesString = found.params.values;
|
||||||
} else {
|
} else {
|
||||||
console.warn('Không tìm thấy API values trong props hoặc config')
|
console.warn("Không tìm thấy API values trong props hoặc config");
|
||||||
this.choices = []
|
this.choices = [];
|
||||||
this.searchableFields = []
|
this.searchableFields = [];
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse values string từ API
|
// Parse values string từ API
|
||||||
let fieldNames = valuesString.split(',').map(v => v.trim())
|
let fieldNames = valuesString.split(",").map((v) => v.trim());
|
||||||
// Lấy pagedata để lấy label
|
// Lấy pagedata để lấy label
|
||||||
this.pagedata = this.store[this.pagename]
|
this.pagedata = this.store[this.pagename];
|
||||||
|
|
||||||
// Lấy tất cả các field để search (không lọc format)
|
// Lấy tất cả các field để search (không lọc format)
|
||||||
const searchable = fieldNames.filter(fieldName => {
|
const searchable = fieldNames.filter((fieldName) => {
|
||||||
// Loại bỏ các field kỹ thuật
|
// Loại bỏ các field kỹ thuật
|
||||||
if (fieldName === 'id' ||
|
if (
|
||||||
fieldName === 'create_time' ||
|
fieldName === "id" ||
|
||||||
fieldName === 'update_time' ||
|
fieldName === "create_time" ||
|
||||||
fieldName === 'created_at' ||
|
fieldName === "update_time" ||
|
||||||
fieldName === 'updated_at') {
|
fieldName === "created_at" ||
|
||||||
return false
|
fieldName === "updated_at"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
// Lấy tên và label các field
|
// Lấy tên và label các field
|
||||||
this.choices = searchable
|
this.choices = searchable;
|
||||||
this.searchableFields = searchable.map(fieldName => {
|
this.searchableFields = searchable.map((fieldName) => {
|
||||||
// Lấy field base name (trước dấu __)
|
// Lấy field base name (trước dấu __)
|
||||||
const baseFieldName = fieldName.split('__')[0]
|
const baseFieldName = fieldName.split("__")[0];
|
||||||
const fieldInfo = this.pagedata && this.pagedata.fields
|
const fieldInfo =
|
||||||
? this.pagedata.fields.find(f => f.name === baseFieldName)
|
this.pagedata && this.pagedata.fields ? this.pagedata.fields.find((f) => f.name === baseFieldName) : null;
|
||||||
: null
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
label: fieldInfo ? fieldInfo.label : fieldName
|
label: fieldInfo ? fieldInfo.label : fieldName,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating searchable fields:', error)
|
console.error("Error updating searchable fields:", error);
|
||||||
this.choices = []
|
this.choices = [];
|
||||||
this.searchableFields = []
|
this.searchableFields = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
let found = this.$find(this.options, { code: this.current })
|
let found = this.$find(this.options, { code: this.current });
|
||||||
this.changeOption(found)
|
this.changeOption(found);
|
||||||
},
|
},
|
||||||
|
|
||||||
changeOption(v) {
|
changeOption(v) {
|
||||||
this.current = v.code
|
this.current = v.code;
|
||||||
if (this.search) {
|
if (this.search) {
|
||||||
this.text = undefined
|
this.text = undefined;
|
||||||
this.search = undefined
|
this.search = undefined;
|
||||||
}
|
}
|
||||||
this.$emit("option", this.$find(this.array, { code: this.current }))
|
this.$emit("option", this.$find(this.array, { code: this.current }));
|
||||||
},
|
},
|
||||||
|
|
||||||
doSearch() {
|
doSearch() {
|
||||||
// Cập nhật choices trước khi search
|
// Cập nhật choices trước khi search
|
||||||
this.updateSearchableFields()
|
this.updateSearchableFields();
|
||||||
|
|
||||||
this.pagedata = this.store[this.pagename]
|
this.pagedata = this.store[this.pagename];
|
||||||
|
|
||||||
if (!this.pagedata || !this.pagedata.fields) {
|
if (!this.pagedata || !this.pagedata.fields) {
|
||||||
console.warn('Không có pagedata hoặc fields')
|
console.warn("Không có pagedata hoặc fields");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fields = this.pagedata.fields.filter(
|
let fields = this.pagedata.fields.filter((v) => this.choices.findIndex((x) => x === v.name) >= 0);
|
||||||
(v) => this.choices.findIndex((x) => x === v.name) >= 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if (fields.length === 0) {
|
if (fields.length === 0) {
|
||||||
console.warn('Không tìm thấy field để tìm kiếm')
|
console.warn("Không tìm thấy field để tìm kiếm");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = {}
|
let f = {};
|
||||||
fields.map((v) => {
|
fields.map((v) => {
|
||||||
f[`${v.name}__icontains`] = this.search
|
f[`${v.name}__icontains`] = this.search;
|
||||||
})
|
});
|
||||||
|
|
||||||
this.$emit("option", { filter_or: f })
|
this.$emit("option", { filter_or: f });
|
||||||
},
|
},
|
||||||
|
|
||||||
openImport() {
|
openImport() {
|
||||||
if (!this.importdata) return
|
if (!this.importdata) return;
|
||||||
// Emit event lên parent (DataView)
|
// Emit event lên parent (DataView)
|
||||||
this.$emit('import', this.importdata)
|
this.$emit("import", this.importdata);
|
||||||
},
|
},
|
||||||
|
|
||||||
startSearch(val) {
|
startSearch(val) {
|
||||||
this.search = this.$empty(val.target.value)
|
this.search = this.$empty(val.target.value) ? "" : val.target.value.trim();
|
||||||
? ""
|
|
||||||
: val.target.value.trim()
|
|
||||||
|
|
||||||
if (this.timer) clearTimeout(this.timer)
|
if (this.timer) clearTimeout(this.timer);
|
||||||
this.timer = setTimeout(() => this.doSearch(), 300)
|
this.timer = setTimeout(() => this.doSearch(), 300);
|
||||||
},
|
},
|
||||||
|
|
||||||
checkTimeopt() {
|
checkTimeopt() {
|
||||||
if (this.timeopt > 0) {
|
if (this.timeopt > 0) {
|
||||||
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt) })
|
let obj = this.$find(this.options, {
|
||||||
if (obj) this.current = obj.code
|
code: this.$formatNumber(this.timeopt),
|
||||||
|
});
|
||||||
|
if (obj) this.current = obj.code;
|
||||||
}
|
}
|
||||||
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return
|
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return;
|
||||||
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false
|
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false;
|
||||||
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false
|
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false;
|
||||||
if (this.timeopt.time) {
|
if (this.timeopt.time) {
|
||||||
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt.time) })
|
let obj = this.$find(this.options, {
|
||||||
if (obj) this.current = obj.code
|
code: this.$formatNumber(this.timeopt.time),
|
||||||
|
});
|
||||||
|
if (obj) this.current = obj.code;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
color: { type: String, default: "#000" }
|
color: { type: String, default: "#000" },
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :style="color? `color:${color}` : ''">{{ $dayjs(date).format('DD/MM/YYYY') }}</span>
|
<span :style="color ? `color:${color}` : ''">{{ $dayjs(date).format("DD/MM/YYYY") }}</span>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
const { $dayjs } = useNuxtApp()
|
const { $dayjs } = useNuxtApp();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
date: String,
|
date: String,
|
||||||
color: String
|
color: String,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :style="color? `color:${color}` : ''">{{ value === 0 || value === null ? '-' : $numtoString(value) }}</span>
|
<span :style="color ? `color:${color}` : ''">{{ value === 0 || value === null ? "-" : $numtoString(value) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
const { $numtoString } = useNuxtApp()
|
const { $numtoString } = useNuxtApp();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: Number,
|
value: Number,
|
||||||
color: String
|
color: String,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<span :style="color? `color:${color}` : ''">{{ $numtoString(value) }}</span>
|
<span :style="color ? `color:${color}` : ''">{{ $numtoString(value) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
const { $numtoString } = useNuxtApp()
|
const { $numtoString } = useNuxtApp();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: Number,
|
value: Number,
|
||||||
color: String
|
color: String,
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,28 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="control has-icons-left" :id="docid">
|
<div
|
||||||
<div :class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`" style="width: 100%">
|
class="control has-icons-left"
|
||||||
<div class="dropdown-trigger" style="width: 100%;">
|
:id="docid"
|
||||||
<input :disabled="disabled" :class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-dark' : ''}`" type="text" placeholder="DD/MM/YYYY"
|
>
|
||||||
maxlength="10" @focus="setFocus" @blur="lostFocus" @keyup.enter="pressEnter" @keyup="checkDate" v-model="show" />
|
<div
|
||||||
</div>
|
:class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`"
|
||||||
<div class="dropdown-menu" role="menu" @click="doClick()">
|
style="width: 100%"
|
||||||
<div class="dropdown-content">
|
>
|
||||||
<PickDay v-bind="{ date, maxdate }" @date="selectDate"></PickDay>
|
<div
|
||||||
|
class="dropdown-trigger"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-dark' : ''}`"
|
||||||
|
type="text"
|
||||||
|
placeholder="DD/MM/YYYY"
|
||||||
|
maxlength="10"
|
||||||
|
@focus="setFocus"
|
||||||
|
@blur="lostFocus"
|
||||||
|
@keyup.enter="pressEnter"
|
||||||
|
@keyup="checkDate"
|
||||||
|
v-model="show"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu"
|
||||||
|
role="menu"
|
||||||
|
@click="doClick()"
|
||||||
|
>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<PickDay
|
||||||
|
v-bind="{ date, maxdate }"
|
||||||
|
@date="selectDate"
|
||||||
|
></PickDay>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span class="icon is-left">
|
||||||
</div>
|
<Icon
|
||||||
<span class="icon is-left">
|
name="material-symbols:calendar-today-outline-rounded"
|
||||||
<Icon
|
:size="21"
|
||||||
name="material-symbols:calendar-today-outline-rounded"
|
class="has-text-grey"
|
||||||
:size="21"
|
/>
|
||||||
class="has-text-grey"
|
</span>
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['record', 'attr', 'position', 'mindate', 'maxdate', 'disabled'],
|
props: ["record", "attr", "position", "mindate", "maxdate", "disabled"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
date: undefined,
|
date: undefined,
|
||||||
@@ -32,103 +58,102 @@ export default {
|
|||||||
docid: this.$id(),
|
docid: this.$id(),
|
||||||
count1: 0,
|
count1: 0,
|
||||||
count2: 0,
|
count2: 0,
|
||||||
pos: undefined
|
pos: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getPos()
|
this.getPos();
|
||||||
if(this.record) {
|
if (this.record) {
|
||||||
this.date = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined
|
this.date = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
|
||||||
if(this.date) this.show = this.$dayjs(this.date).format('DD/MM/YYYY')
|
if (this.date) this.show = this.$dayjs(this.date).format("DD/MM/YYYY");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
record: function(newVal) {
|
record: function (newVal) {
|
||||||
if(this.record) {
|
if (this.record) {
|
||||||
this.date = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined
|
this.date = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
|
||||||
if(this.date) this.show = this.$dayjs(this.date).format('DD/MM/YYYY')
|
if (this.date) this.show = this.$dayjs(this.date).format("DD/MM/YYYY");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
pressEnter() {
|
pressEnter() {
|
||||||
this.checkDate()
|
this.checkDate();
|
||||||
if(!this.error) this.focused = false
|
if (!this.error) this.focused = false;
|
||||||
},
|
},
|
||||||
setFocus() {
|
setFocus() {
|
||||||
this.focused = true
|
this.focused = true;
|
||||||
this.count1 = 0
|
this.count1 = 0;
|
||||||
this.count2 = 0
|
this.count2 = 0;
|
||||||
},
|
},
|
||||||
lostFocus() {
|
lostFocus() {
|
||||||
let self = this
|
let self = this;
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
if(self.focused && self.count1===0) self.focused = false
|
if (self.focused && self.count1 === 0) self.focused = false;
|
||||||
}, 200)
|
}, 200);
|
||||||
},
|
},
|
||||||
processEvent(event) {
|
processEvent(event) {
|
||||||
var doc = document.getElementById(this.docid)
|
var doc = document.getElementById(this.docid);
|
||||||
if(!doc) return
|
if (!doc) return;
|
||||||
this.count2 += 1
|
this.count2 += 1;
|
||||||
var isClickInside = false
|
var isClickInside = false;
|
||||||
isClickInside = doc.contains(event.target);
|
isClickInside = doc.contains(event.target);
|
||||||
if(!isClickInside && this.focused) {
|
if (!isClickInside && this.focused) {
|
||||||
if(this.count2-1!==this.count1) {
|
if (this.count2 - 1 !== this.count1) {
|
||||||
this.focused = false
|
this.focused = false;
|
||||||
this.count1 = 0
|
this.count1 = 0;
|
||||||
this.count2 = 0
|
this.count2 = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doClick() {
|
doClick() {
|
||||||
this.count1 += 1
|
this.count1 += 1;
|
||||||
},
|
},
|
||||||
selectDate(v) {
|
selectDate(v) {
|
||||||
this.date = v
|
this.date = v;
|
||||||
this.show = this.$dayjs(v).format('DD/MM/YYYY')
|
this.show = this.$dayjs(v).format("DD/MM/YYYY");
|
||||||
this.$emit('date', this.date)
|
this.$emit("date", this.date);
|
||||||
if(this.focused) this.focused = false
|
if (this.focused) this.focused = false;
|
||||||
this.count1 = 0
|
this.count1 = 0;
|
||||||
this.count2 = 0
|
this.count2 = 0;
|
||||||
},
|
},
|
||||||
getDate(value) {
|
getDate(value) {
|
||||||
let v = value.replace(/\D/g,'').slice(0, 10);
|
let v = value.replace(/\D/g, "").slice(0, 10);
|
||||||
if (v.length >= 5) {
|
if (v.length >= 5) {
|
||||||
return `${v.slice(0,2)}/${v.slice(2,4)}/${v.slice(4)}`;
|
return `${v.slice(0, 2)}/${v.slice(2, 4)}/${v.slice(4)}`;
|
||||||
|
} else if (v.length >= 3) {
|
||||||
|
return `${v.slice(0, 2)}/${v.slice(2)}`;
|
||||||
}
|
}
|
||||||
else if (v.length >= 3) {
|
return v;
|
||||||
return `${v.slice(0,2)}/${v.slice(2)}`;
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
},
|
},
|
||||||
checkDate() {
|
checkDate() {
|
||||||
if(!this.focused) this.setFocus()
|
if (!this.focused) this.setFocus();
|
||||||
this.error = false
|
this.error = false;
|
||||||
this.date = undefined
|
this.date = undefined;
|
||||||
if(this.$empty(this.show)) return this.$emit('date', null)
|
if (this.$empty(this.show)) return this.$emit("date", null);
|
||||||
this.show = this.getDate(this.show)
|
this.show = this.getDate(this.show);
|
||||||
let val = `${this.show.substring(6,10)}-${this.show.substring(3,5)}-${this.show.substring(0,2)}`
|
let val = `${this.show.substring(6, 10)}-${this.show.substring(3, 5)}-${this.show.substring(0, 2)}`;
|
||||||
if(this.$dayjs(val, "YYYY-MM-DD", true).isValid()) {
|
if (this.$dayjs(val, "YYYY-MM-DD", true).isValid()) {
|
||||||
this.date = val
|
this.date = val;
|
||||||
this.$emit('date', this.date)
|
this.$emit("date", this.date);
|
||||||
} else this.error = true
|
} else this.error = true;
|
||||||
},
|
},
|
||||||
getPos() {
|
getPos() {
|
||||||
switch(this.position) {
|
switch (this.position) {
|
||||||
case 'is-top-left':
|
case "is-top-left":
|
||||||
this.pos = 'is-up is-left'
|
this.pos = "is-up is-left";
|
||||||
break;
|
break;
|
||||||
case 'is-top-right':
|
case "is-top-right":
|
||||||
this.pos = 'is-up is-right'
|
this.pos = "is-up is-right";
|
||||||
break;
|
break;
|
||||||
case 'is-bottom-left':
|
case "is-bottom-left":
|
||||||
this.pos = 'is-left'
|
this.pos = "is-left";
|
||||||
break;
|
break;
|
||||||
case 'is-bottom-right':
|
case "is-bottom-right":
|
||||||
this.pos = 'is-right'
|
this.pos = "is-right";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,185 +1,270 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="field is-grouped mb-4 border-bottom" v-if="1<0">
|
<div
|
||||||
<div class="control pl-2" v-if="mode!=='simple'">
|
class="field is-grouped mb-4 border-bottom"
|
||||||
<a class="mr-1" @click="previousYear()">
|
v-if="1 < 0"
|
||||||
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon>
|
>
|
||||||
</a>
|
<div
|
||||||
<a @click="previousMonth()" v-if="type==='days'">
|
class="control pl-2"
|
||||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon>
|
v-if="mode !== 'simple'"
|
||||||
</a>
|
>
|
||||||
|
<a
|
||||||
|
class="mr-1"
|
||||||
|
@click="previousYear()"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
@click="previousMonth()"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded has-text-centered">
|
<div class="control is-expanded has-text-centered">
|
||||||
<span class="fsb-16 hyperlink mr-3" @click="type='months'" v-if="type==='days'">{{`Tháng ${month}`}}</span>
|
<span
|
||||||
<span class="fsb-16 hyperlink" @click="type='years'">{{ caption || year }}</span>
|
class="fsb-16 hyperlink mr-3"
|
||||||
|
@click="type = 'months'"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>{{ `Tháng ${month}` }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="fsb-16 hyperlink"
|
||||||
|
@click="type = 'years'"
|
||||||
|
>{{ caption || year }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="control pr-2" v-if="mode!=='simple'">
|
<div
|
||||||
<a class="mr-1" @click="nextMonth()" v-if="type==='days'">
|
class="control pr-2"
|
||||||
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon>
|
v-if="mode !== 'simple'"
|
||||||
</a>
|
>
|
||||||
<a @click="nextYear()">
|
<a
|
||||||
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon>
|
class="mr-1"
|
||||||
</a>
|
@click="nextMonth()"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a @click="nextYear()">
|
||||||
|
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="type==='days'">
|
<div v-if="type === 'days'">
|
||||||
<div :class="`columns mx-0 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
|
<div
|
||||||
<div class="column px-3 py-1 has-text-centered" v-for="(m,h) in v.dates" :key="h" style="min-height: 100px;"
|
:class="`columns mx-0 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
|
||||||
:style="`border-right: 1px solid #DCDCDC; border-bottom: 1px solid #DCDCDC; ${(viewport>1 && h===0 || viewport===1)? 'border-left: 1px solid #DCDCDC;' : ''}
|
v-for="(v, i) in weeks"
|
||||||
${i===0? 'border-top: 1px solid #DCDCDC;' : ''}`">
|
:key="i"
|
||||||
<p class="mb-1" v-if="i===0"><b>{{ $find(dateOfWeek, {id: h}).text}}</b></p>
|
>
|
||||||
<span class="has-background-primary has-text-white px-1 py-1" v-if="m.date===today">{{ m.dayPrint }}</span>
|
<div
|
||||||
<span v-else>{{m.dayPrint}}</span>
|
class="column px-3 py-1 has-text-centered"
|
||||||
<div class="has-text-left fs-15 mt-1" v-if="m.event && m.currentMonth===m.mothCondition">
|
v-for="(m, h) in v.dates"
|
||||||
<p :class="`pt-1 ${j===m.event.length-1? '' : 'border-bottom'}`" v-for="(h,j) in m.event">
|
:key="h"
|
||||||
<SvgIcon v-bind="{name: h.icon, type: h.color, size: 16}"></SvgIcon>
|
style="min-height: 100px"
|
||||||
<a class="ml-3" @click="openEvent(h)">{{ h.text }}</a>
|
:style="`border-right: 1px solid #DCDCDC; border-bottom: 1px solid #DCDCDC; ${(viewport > 1 && h === 0) || viewport === 1 ? 'border-left: 1px solid #DCDCDC;' : ''}
|
||||||
|
${i === 0 ? 'border-top: 1px solid #DCDCDC;' : ''}`"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="mb-1"
|
||||||
|
v-if="i === 0"
|
||||||
|
>
|
||||||
|
<b>{{ $find(dateOfWeek, { id: h }).text }}</b>
|
||||||
</p>
|
</p>
|
||||||
|
<span
|
||||||
|
class="has-background-primary has-text-white px-1 py-1"
|
||||||
|
v-if="m.date === today"
|
||||||
|
>{{ m.dayPrint }}</span
|
||||||
|
>
|
||||||
|
<span v-else>{{ m.dayPrint }}</span>
|
||||||
|
<div
|
||||||
|
class="has-text-left fs-15 mt-1"
|
||||||
|
v-if="m.event && m.currentMonth === m.mothCondition"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
:class="`pt-1 ${j === m.event.length - 1 ? '' : 'border-bottom'}`"
|
||||||
|
v-for="(h, j) in m.event"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: h.icon, type: h.color, size: 16 }"></SvgIcon>
|
||||||
|
<a
|
||||||
|
class="ml-3"
|
||||||
|
@click="openEvent(h)"
|
||||||
|
>{{ h.text }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['date', 'events', 'mode', 'vyear', 'vmonth'],
|
props: ["date", "events", "mode", "vyear", "vmonth"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dates: [],
|
dates: [],
|
||||||
dateOfWeek: [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"},
|
dateOfWeek: [
|
||||||
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}],
|
{ id: 0, text: "CN" },
|
||||||
|
{ id: 1, text: "T2" },
|
||||||
|
{ id: 2, text: "T3" },
|
||||||
|
{ id: 3, text: "T4" },
|
||||||
|
{ id: 4, text: "T5" },
|
||||||
|
{ id: 5, text: "T6" },
|
||||||
|
{ id: 6, text: "T7" },
|
||||||
|
],
|
||||||
weeks: [],
|
weeks: [],
|
||||||
today: this.$dayjs().format('YYYY/MM/DD'),
|
today: this.$dayjs().format("YYYY/MM/DD"),
|
||||||
year: undefined,
|
year: undefined,
|
||||||
month: undefined,
|
month: undefined,
|
||||||
type: 'days',
|
type: "days",
|
||||||
caption: undefined,
|
caption: undefined,
|
||||||
action: undefined,
|
action: undefined,
|
||||||
curdate: undefined,
|
curdate: undefined,
|
||||||
showmodal: undefined,
|
showmodal: undefined,
|
||||||
viewport: 5
|
viewport: 5,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.showDate()
|
this.showDate();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
date: function(newVal) {
|
date: function (newVal) {
|
||||||
if(newVal) this.showDate()
|
if (newVal) this.showDate();
|
||||||
},
|
},
|
||||||
vmonth: function(newVal) {
|
vmonth: function (newVal) {
|
||||||
this.showDate()
|
this.showDate();
|
||||||
|
},
|
||||||
|
events: function (newVal) {
|
||||||
|
this.showDate();
|
||||||
},
|
},
|
||||||
events: function(newVal) {
|
|
||||||
this.showDate()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async openEvent(event) {
|
async openEvent(event) {
|
||||||
let row = await this.$getdata('sale', {id: event.sale}, undefined, true)
|
let row = await this.$getdata("sale", { id: event.sale }, undefined, true);
|
||||||
this.showmodal = {title: 'Bán hàng', height: '500px', width: '90%', component: 'sale/Sale', vbind: {row: row, highlight: event.id}}
|
this.showmodal = {
|
||||||
|
title: "Bán hàng",
|
||||||
|
height: "500px",
|
||||||
|
width: "90%",
|
||||||
|
component: "sale/Sale",
|
||||||
|
vbind: { row: row, highlight: event.id },
|
||||||
|
};
|
||||||
},
|
},
|
||||||
compiledComponent(value) {
|
compiledComponent(value) {
|
||||||
return {
|
return {
|
||||||
template: `${value}`
|
template: `${value}`,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
showDate() {
|
showDate() {
|
||||||
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined
|
this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
|
||||||
this.year = this.$dayjs(this.curdate || this.today).year()
|
this.year = this.$dayjs(this.curdate || this.today).year();
|
||||||
this.month = this.$dayjs(this.curdate || this.today).month() + 1
|
this.month = this.$dayjs(this.curdate || this.today).month() + 1;
|
||||||
if(this.vyear) this.year = this.$copy(this.vyear)
|
if (this.vyear) this.year = this.$copy(this.vyear);
|
||||||
if(this.vmonth) this.month = this.$copy(this.vmonth)
|
if (this.vmonth) this.month = this.$copy(this.vmonth);
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
chooseToday() {
|
chooseToday() {
|
||||||
this.$emit('date', this.today.replaceAll('/', '-'))
|
this.$emit("date", this.today.replaceAll("/", "-"));
|
||||||
this.year = this.$dayjs(this.today).year()
|
this.year = this.$dayjs(this.today).year();
|
||||||
this.month = this.$dayjs(this.today).month() + 1
|
this.month = this.$dayjs(this.today).month() + 1;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
changeCaption(v) {
|
changeCaption(v) {
|
||||||
this.caption = v
|
this.caption = v;
|
||||||
},
|
},
|
||||||
selectMonth(v) {
|
selectMonth(v) {
|
||||||
this.month = v
|
this.month = v;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
this.type = 'days'
|
this.type = "days";
|
||||||
},
|
},
|
||||||
selectYear(v) {
|
selectYear(v) {
|
||||||
this.year = v
|
this.year = v;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
this.type = 'days'
|
this.type = "days";
|
||||||
},
|
},
|
||||||
getDates() {
|
getDates() {
|
||||||
this.caption = undefined
|
this.caption = undefined;
|
||||||
this.dates = this.allDaysInMonth(this.year, this.month)
|
this.dates = this.allDaysInMonth(this.year, this.month);
|
||||||
this.dates.map(v=>{
|
this.dates.map((v) => {
|
||||||
let event = this.events? this.$filter(this.events, {isodate: v.date}) : undefined
|
let event = this.events ? this.$filter(this.events, { isodate: v.date }) : undefined;
|
||||||
if(event.length>0) v.event = event
|
if (event.length > 0) v.event = event;
|
||||||
})
|
});
|
||||||
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}})
|
this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
|
||||||
this.weeks.map(v=>{
|
return { week: v.week };
|
||||||
v.dates = this.dates.filter(x=>x.week===v.week)
|
});
|
||||||
})
|
this.weeks.map((v) => {
|
||||||
|
v.dates = this.dates.filter((x) => x.week === v.week);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
nextMonth() {
|
nextMonth() {
|
||||||
let month = this.month + 1
|
let month = this.month + 1;
|
||||||
if(month>12) {
|
if (month > 12) {
|
||||||
month = 1
|
month = 1;
|
||||||
this.year += 1
|
this.year += 1;
|
||||||
}
|
}
|
||||||
this.month = month
|
this.month = month;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
previousMonth() {
|
previousMonth() {
|
||||||
let month = this.month - 1
|
let month = this.month - 1;
|
||||||
if(month===0) {
|
if (month === 0) {
|
||||||
month = 12
|
month = 12;
|
||||||
this.year -= 1
|
this.year -= 1;
|
||||||
}
|
}
|
||||||
this.month = month
|
this.month = month;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
nextYear() {
|
nextYear() {
|
||||||
if(this.type==='years') return this.action = {name: 'next', id: this.$id()}
|
if (this.type === "years") return (this.action = { name: "next", id: this.$id() });
|
||||||
this.year += 1
|
this.year += 1;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
previousYear() {
|
previousYear() {
|
||||||
if(this.type==='years') return this.action = {name: 'previous', id: this.$id()}
|
if (this.type === "years") return (this.action = { name: "previous", id: this.$id() });
|
||||||
this.year -= 1
|
this.year -= 1;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
choose(m) {
|
choose(m) {
|
||||||
this.$emit('date', m.date.replaceAll('/', '-'))
|
this.$emit("date", m.date.replaceAll("/", "-"));
|
||||||
},
|
},
|
||||||
createDate(v, x, y) {
|
createDate(v, x, y) {
|
||||||
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString())
|
return (
|
||||||
|
v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString())
|
||||||
|
);
|
||||||
},
|
},
|
||||||
allDaysInMonth(year, month) {
|
allDaysInMonth(year, month) {
|
||||||
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
|
let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
|
||||||
let arr = []
|
let arr = [];
|
||||||
days.map(v=>{
|
days.map((v) => {
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i)
|
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i);
|
||||||
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD")
|
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
|
||||||
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD")
|
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
|
||||||
let mothCondition = this.$dayjs(date).month() +1
|
let mothCondition = this.$dayjs(date).month() + 1;
|
||||||
let currentMonth = month
|
let currentMonth = month;
|
||||||
let found = arr.find(x=>x.date===date)
|
let found = arr.find((x) => x.date === date);
|
||||||
if(!found) {
|
if (!found) {
|
||||||
let dayOfWeek = this.$dayjs(date).day()
|
let dayOfWeek = this.$dayjs(date).day();
|
||||||
let week = this.$dayjs(date).week()
|
let week = this.$dayjs(date).week();
|
||||||
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, mothCondition: mothCondition, currentMonth: currentMonth}
|
let ele = {
|
||||||
arr.push(ele)
|
date: date,
|
||||||
|
week: week,
|
||||||
|
day: v,
|
||||||
|
dayOfWeek: dayOfWeek,
|
||||||
|
dayPrint: dayPrint,
|
||||||
|
mothCondition: mothCondition,
|
||||||
|
currentMonth: currentMonth,
|
||||||
|
};
|
||||||
|
arr.push(ele);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return arr
|
return arr;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="columns is-multiline mx-0">
|
<div class="columns is-multiline mx-0">
|
||||||
<div class="column is-4" v-for="v in months">
|
<div
|
||||||
<EventSummary v-bind="{events: events, mode: 'simple', vyear: v.year, vmonth: v.month}"></EventSummary>
|
class="column is-4"
|
||||||
|
v-for="v in months"
|
||||||
|
>
|
||||||
|
<EventSummary
|
||||||
|
v-bind="{
|
||||||
|
events: events,
|
||||||
|
mode: 'simple',
|
||||||
|
vyear: v.year,
|
||||||
|
vmonth: v.month,
|
||||||
|
}"
|
||||||
|
></EventSummary>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import EventSummary from '@/components/datepicker/EventSummary'
|
import EventSummary from "@/components/datepicker/EventSummary";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EventSummary
|
EventSummary,
|
||||||
//EventSummary: () => import('@/components/datepicker/EventSummary')
|
//EventSummary: () => import('@/components/datepicker/EventSummary')
|
||||||
},
|
},
|
||||||
props: ['events', 'months']
|
props: ["events", "months"],
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,52 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="columns mx-0">
|
<div class="columns mx-0">
|
||||||
<div class="column is-narrow" v-if="1<0">
|
<div
|
||||||
|
class="column is-narrow"
|
||||||
|
v-if="1 < 0"
|
||||||
|
>
|
||||||
<EventSummary></EventSummary>
|
<EventSummary></EventSummary>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<span class="fsb-17 mr-4">{{ `T${vmonth}/${vyear}` }}</span>
|
<span class="fsb-17 mr-4">{{ `T${vmonth}/${vyear}` }}</span>
|
||||||
<a class="mr-2" @click="previous()"><SvgIcon v-bind="{name: 'left1.svg', type: 'dark', size: 18}"></SvgIcon></a>
|
<a
|
||||||
<a class="mr-3" @click="next()"><SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 18}"></SvgIcon></a>
|
class="mr-2"
|
||||||
<a @click="refresh()"><SvgIcon v-bind="{name: 'refresh.svg', type: 'dark', size: 18}"></SvgIcon></a>
|
@click="previous()"
|
||||||
|
><SvgIcon v-bind="{ name: 'left1.svg', type: 'dark', size: 18 }"></SvgIcon
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
class="mr-3"
|
||||||
|
@click="next()"
|
||||||
|
><SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 18 }"></SvgIcon
|
||||||
|
></a>
|
||||||
|
<a @click="refresh()"><SvgIcon v-bind="{ name: 'refresh.svg', type: 'dark', size: 18 }"></SvgIcon></a>
|
||||||
</div>
|
</div>
|
||||||
<EventDetail v-bind="{events: events, vyear: vyear, vmonth: vmonth}"></EventDetail>
|
<EventDetail v-bind="{ events: events, vyear: vyear, vmonth: vmonth }"></EventDetail>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import EventSummary from '@/components/datepicker/EventSummary'
|
import EventSummary from "@/components/datepicker/EventSummary";
|
||||||
import EventDetail from '@/components/datepicker/EventDetail'
|
import EventDetail from "@/components/datepicker/EventDetail";
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['events', 'year', 'month'],
|
props: ["events", "year", "month"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
vyear: this.year? this.$copy(this.year) : undefined,
|
vyear: this.year ? this.$copy(this.year) : undefined,
|
||||||
vmonth: this.month? this.$copy(this.month) : undefined
|
vmonth: this.month ? this.$copy(this.month) : undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
next() {
|
next() {
|
||||||
let month = this.vmonth + 1
|
let month = this.vmonth + 1;
|
||||||
if(month>12) {
|
if (month > 12) {
|
||||||
month = 1
|
month = 1;
|
||||||
this.vyear += 1
|
this.vyear += 1;
|
||||||
}
|
}
|
||||||
this.vmonth = month
|
this.vmonth = month;
|
||||||
},
|
},
|
||||||
previous() {
|
previous() {
|
||||||
let month = this.vmonth - 1
|
let month = this.vmonth - 1;
|
||||||
if(month===0) {
|
if (month === 0) {
|
||||||
month = 12
|
month = 12;
|
||||||
this.vyear -= 1
|
this.vyear -= 1;
|
||||||
}
|
}
|
||||||
this.vmonth = month
|
this.vmonth = month;
|
||||||
},
|
},
|
||||||
refresh() {
|
refresh() {
|
||||||
this.$emit('refresh')
|
this.$emit("refresh");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,189 +1,261 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="field is-grouped mb-4 border-bottom">
|
<div class="field is-grouped mb-4 border-bottom">
|
||||||
<div class="control pl-2" v-if="mode!=='simple'">
|
<div
|
||||||
<a class="mr-1" @click="previousYear()">
|
class="control pl-2"
|
||||||
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon>
|
v-if="mode !== 'simple'"
|
||||||
</a>
|
>
|
||||||
<a @click="previousMonth()" v-if="type==='days'">
|
<a
|
||||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon>
|
class="mr-1"
|
||||||
</a>
|
@click="previousYear()"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
@click="previousMonth()"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded has-text-centered">
|
<div class="control is-expanded has-text-centered">
|
||||||
<span class="fsb-16 hyperlink mr-3" @click="mode==='simple'? false : type='PickMonth'" v-if="type==='days'">{{`Tháng ${month}`}}</span>
|
<span
|
||||||
<span class="fsb-16 hyperlink" @click="mode==='simple'? false : type='PickYear'">{{ caption || year }}</span>
|
class="fsb-16 hyperlink mr-3"
|
||||||
|
@click="mode === 'simple' ? false : (type = 'PickMonth')"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>{{ `Tháng ${month}` }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="fsb-16 hyperlink"
|
||||||
|
@click="mode === 'simple' ? false : (type = 'PickYear')"
|
||||||
|
>{{ caption || year }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="control pr-2" v-if="mode!=='simple'">
|
<div
|
||||||
<a class="mr-1" @click="nextMonth()" v-if="type==='days'">
|
class="control pr-2"
|
||||||
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon>
|
v-if="mode !== 'simple'"
|
||||||
</a>
|
>
|
||||||
<a @click="nextYear()">
|
<a
|
||||||
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon>
|
class="mr-1"
|
||||||
</a>
|
@click="nextMonth()"
|
||||||
</div>
|
v-if="type === 'days'"
|
||||||
</div>
|
>
|
||||||
<div v-if="type==='days'">
|
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
<div class="columns is-mobile mx-0">
|
</a>
|
||||||
<div class="column px-2 py-1 has-text-grey-dark" v-for="(m,h) in dateOfWeek" :key="h">
|
<a @click="nextYear()">
|
||||||
{{ m.text }}
|
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="`columns is-mobile mx-0 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
|
|
||||||
<div class="column px-3 fs-14" v-for="(m,h) in v.dates" :key="h" style="padding-top: 1px; padding-bottom: 1px;">
|
|
||||||
<a v-if="m.event && m.currentMonth===m.mothCondition">
|
|
||||||
<Tooltip v-bind="{html: m.event.html, tooltip: m.event.tooltip}"></Tooltip>
|
|
||||||
</a>
|
</a>
|
||||||
<span class="has-background-findata has-text-white px-1 py-1" v-else-if="m.date===today">{{ m.dayPrint }}</span>
|
|
||||||
<span v-else>{{m.dayPrint}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="mode!=='simple'">
|
<div v-if="type === 'days'">
|
||||||
<div class="border-bottom"></div>
|
<div class="columns is-mobile mx-0">
|
||||||
<div class="mt-2">
|
<div
|
||||||
<span class="ml-2 mr-2">Hôm nay: </span>
|
class="column px-2 py-1 has-text-grey-dark"
|
||||||
<span class="has-text-primary hyperlink" @click="chooseToday()">{{ $dayjs(today).format('DD/MM/YYYY') }}</span>
|
v-for="(m, h) in dateOfWeek"
|
||||||
|
:key="h"
|
||||||
|
>
|
||||||
|
{{ m.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="`columns is-mobile mx-0 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
|
||||||
|
v-for="(v, i) in weeks"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="column px-3 fs-14"
|
||||||
|
v-for="(m, h) in v.dates"
|
||||||
|
:key="h"
|
||||||
|
style="padding-top: 1px; padding-bottom: 1px"
|
||||||
|
>
|
||||||
|
<a v-if="m.event && m.currentMonth === m.mothCondition">
|
||||||
|
<Tooltip v-bind="{ html: m.event.html, tooltip: m.event.tooltip }"></Tooltip>
|
||||||
|
</a>
|
||||||
|
<span
|
||||||
|
class="has-background-findata has-text-white px-1 py-1"
|
||||||
|
v-else-if="m.date === today"
|
||||||
|
>{{ m.dayPrint }}</span
|
||||||
|
>
|
||||||
|
<span v-else>{{ m.dayPrint }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="mode !== 'simple'">
|
||||||
|
<div class="border-bottom"></div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="ml-2 mr-2">Hôm nay: </span>
|
||||||
|
<span
|
||||||
|
class="has-text-primary hyperlink"
|
||||||
|
@click="chooseToday()"
|
||||||
|
>{{ $dayjs(today).format("DD/MM/YYYY") }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div v-else-if="type === 'PickMonth'">
|
||||||
|
<PickMonth @month="selectMonth"></PickMonth>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'PickYear'">
|
||||||
|
<PickYear
|
||||||
|
v-bind="{ year: year, month: month, action: action }"
|
||||||
|
@year="selectYear"
|
||||||
|
@caption="changeCaption"
|
||||||
|
></PickYear>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
v-bind="showmodal"
|
||||||
|
v-if="showmodal"
|
||||||
|
></Modal>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="type==='PickMonth'">
|
|
||||||
<PickMonth @month="selectMonth"></PickMonth>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="type==='PickYear'">
|
|
||||||
<PickYear v-bind="{year: year, month: month, action: action}" @year="selectYear" @caption="changeCaption"></PickYear>
|
|
||||||
</div>
|
|
||||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PickMonth: () => import('@/components/datepicker/PickMonth'),
|
PickMonth: () => import("@/components/datepicker/PickMonth"),
|
||||||
PickYear: () => import('@/components/datepicker/PickYear')
|
PickYear: () => import("@/components/datepicker/PickYear"),
|
||||||
},
|
},
|
||||||
props: ['date', 'events', 'mode', 'vyear', 'vmonth'],
|
props: ["date", "events", "mode", "vyear", "vmonth"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dates: [],
|
dates: [],
|
||||||
dateOfWeek: [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"},
|
dateOfWeek: [
|
||||||
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}],
|
{ id: 0, text: "CN" },
|
||||||
|
{ id: 1, text: "T2" },
|
||||||
|
{ id: 2, text: "T3" },
|
||||||
|
{ id: 3, text: "T4" },
|
||||||
|
{ id: 4, text: "T5" },
|
||||||
|
{ id: 5, text: "T6" },
|
||||||
|
{ id: 6, text: "T7" },
|
||||||
|
],
|
||||||
weeks: [],
|
weeks: [],
|
||||||
today: this.$dayjs().format('YYYY/MM/DD'),
|
today: this.$dayjs().format("YYYY/MM/DD"),
|
||||||
year: undefined,
|
year: undefined,
|
||||||
month: undefined,
|
month: undefined,
|
||||||
type: 'days',
|
type: "days",
|
||||||
caption: undefined,
|
caption: undefined,
|
||||||
action: undefined,
|
action: undefined,
|
||||||
curdate: undefined,
|
curdate: undefined,
|
||||||
showmodal: undefined
|
showmodal: undefined,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.showDate()
|
this.showDate();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
date: function(newVal) {
|
date: function (newVal) {
|
||||||
if(newVal) this.showDate()
|
if (newVal) this.showDate();
|
||||||
|
},
|
||||||
|
events: function (newVal) {
|
||||||
|
this.showDate();
|
||||||
},
|
},
|
||||||
events: function(newVal) {
|
|
||||||
this.showDate()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showDate() {
|
showDate() {
|
||||||
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined
|
this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
|
||||||
this.year = this.$dayjs(this.curdate || this.today).year()
|
this.year = this.$dayjs(this.curdate || this.today).year();
|
||||||
this.month = this.$dayjs(this.curdate || this.today).month() + 1
|
this.month = this.$dayjs(this.curdate || this.today).month() + 1;
|
||||||
if(this.vyear) this.year = this.$copy(this.vyear)
|
if (this.vyear) this.year = this.$copy(this.vyear);
|
||||||
if(this.vmonth) this.month = this.$copy(this.vmonth)
|
if (this.vmonth) this.month = this.$copy(this.vmonth);
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
chooseToday() {
|
chooseToday() {
|
||||||
this.$emit('date', this.today.replaceAll('/', '-'))
|
this.$emit("date", this.today.replaceAll("/", "-"));
|
||||||
this.year = this.$dayjs(this.today).year()
|
this.year = this.$dayjs(this.today).year();
|
||||||
this.month = this.$dayjs(this.today).month() + 1
|
this.month = this.$dayjs(this.today).month() + 1;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
changeCaption(v) {
|
changeCaption(v) {
|
||||||
this.caption = v
|
this.caption = v;
|
||||||
},
|
},
|
||||||
selectMonth(v) {
|
selectMonth(v) {
|
||||||
this.month = v
|
this.month = v;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
this.type = 'days'
|
this.type = "days";
|
||||||
},
|
},
|
||||||
selectYear(v) {
|
selectYear(v) {
|
||||||
this.year = v
|
this.year = v;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
this.type = 'days'
|
this.type = "days";
|
||||||
},
|
},
|
||||||
getDates() {
|
getDates() {
|
||||||
this.caption = undefined
|
this.caption = undefined;
|
||||||
this.dates = this.allDaysInMonth(this.year, this.month)
|
this.dates = this.allDaysInMonth(this.year, this.month);
|
||||||
this.dates.map(v=>{
|
this.dates.map((v) => {
|
||||||
let event = this.events? this.$find(this.events, {isodate: v.date}) : undefined
|
let event = this.events ? this.$find(this.events, { isodate: v.date }) : undefined;
|
||||||
if(event) v.event = event
|
if (event) v.event = event;
|
||||||
})
|
});
|
||||||
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}})
|
this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
|
||||||
this.weeks.map(v=>{
|
return { week: v.week };
|
||||||
v.dates = this.dates.filter(x=>x.week===v.week)
|
});
|
||||||
})
|
this.weeks.map((v) => {
|
||||||
|
v.dates = this.dates.filter((x) => x.week === v.week);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
nextMonth() {
|
nextMonth() {
|
||||||
let month = this.month + 1
|
let month = this.month + 1;
|
||||||
if(month>12) {
|
if (month > 12) {
|
||||||
month = 1
|
month = 1;
|
||||||
this.year += 1
|
this.year += 1;
|
||||||
}
|
}
|
||||||
this.month = month
|
this.month = month;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
previousMonth() {
|
previousMonth() {
|
||||||
let month = this.month - 1
|
let month = this.month - 1;
|
||||||
if(month===0) {
|
if (month === 0) {
|
||||||
month = 12
|
month = 12;
|
||||||
this.year -= 1
|
this.year -= 1;
|
||||||
}
|
}
|
||||||
this.month = month
|
this.month = month;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
nextYear() {
|
nextYear() {
|
||||||
if(this.type==='PickYear') return this.action = {name: 'next', id: this.$id()}
|
if (this.type === "PickYear") return (this.action = { name: "next", id: this.$id() });
|
||||||
this.year += 1
|
this.year += 1;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
previousYear() {
|
previousYear() {
|
||||||
if(this.type==='PickYear') return this.action = {name: 'previous', id: this.$id()}
|
if (this.type === "PickYear") return (this.action = { name: "previous", id: this.$id() });
|
||||||
this.year -= 1
|
this.year -= 1;
|
||||||
this.getDates()
|
this.getDates();
|
||||||
},
|
},
|
||||||
choose(m) {
|
choose(m) {
|
||||||
this.$emit('date', m.date.replaceAll('/', '-'))
|
this.$emit("date", m.date.replaceAll("/", "-"));
|
||||||
},
|
},
|
||||||
createDate(v, x, y) {
|
createDate(v, x, y) {
|
||||||
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString())
|
return (
|
||||||
|
v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString())
|
||||||
|
);
|
||||||
},
|
},
|
||||||
allDaysInMonth(year, month) {
|
allDaysInMonth(year, month) {
|
||||||
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
|
let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
|
||||||
let arr = []
|
let arr = [];
|
||||||
days.map(v=>{
|
days.map((v) => {
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i)
|
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i);
|
||||||
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD")
|
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
|
||||||
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD")
|
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
|
||||||
let mothCondition = this.$dayjs(date).month() +1
|
let mothCondition = this.$dayjs(date).month() + 1;
|
||||||
let currentMonth = month
|
let currentMonth = month;
|
||||||
let found = arr.find(x=>x.date===date)
|
let found = arr.find((x) => x.date === date);
|
||||||
if(!found) {
|
if (!found) {
|
||||||
let dayOfWeek = this.$dayjs(date).day()
|
let dayOfWeek = this.$dayjs(date).day();
|
||||||
let week = this.$dayjs(date).week()
|
let week = this.$dayjs(date).week();
|
||||||
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint,
|
let ele = {
|
||||||
mothCondition: mothCondition, currentMonth: currentMonth}
|
date: date,
|
||||||
arr.push(ele)
|
week: week,
|
||||||
|
day: v,
|
||||||
|
dayOfWeek: dayOfWeek,
|
||||||
|
dayPrint: dayPrint,
|
||||||
|
mothCondition: mothCondition,
|
||||||
|
currentMonth: currentMonth,
|
||||||
|
};
|
||||||
|
arr.push(ele);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return arr
|
return arr;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,53 +1,91 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2" style="width: 300px">
|
<div
|
||||||
|
class="p-2"
|
||||||
|
style="width: 300px"
|
||||||
|
>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control pl-2">
|
<div class="control pl-2">
|
||||||
<a class="mr-1" @click="previousYear()">
|
<a
|
||||||
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon>
|
class="mr-1"
|
||||||
</a>
|
@click="previousYear()"
|
||||||
<a @click="previousMonth()" v-if="type==='days'">
|
>
|
||||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
@click="previousMonth()"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="control is-expanded has-text-centered">
|
<div class="control is-expanded has-text-centered">
|
||||||
<a class="font-bold mr-2" @click="type='months'" v-if="type==='days'">{{`T${month}`}}</a>
|
<a
|
||||||
<a class="font-bold" @click="type='years'">{{ caption || year }}</a>
|
class="font-bold mr-2"
|
||||||
|
@click="type = 'months'"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>{{ `T${month}` }}</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="font-bold"
|
||||||
|
@click="type = 'years'"
|
||||||
|
>{{ caption || year }}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="control pr-2">
|
<div class="control pr-2">
|
||||||
<a class="mr-1" @click="nextMonth()" v-if="type==='days'">
|
<a
|
||||||
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon>
|
class="mr-1"
|
||||||
|
@click="nextMonth()"
|
||||||
|
v-if="type === 'days'"
|
||||||
|
>
|
||||||
|
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
<a @click="nextYear()">
|
<a @click="nextYear()">
|
||||||
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon>
|
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="mt-0 mb-5" />
|
<hr class="mt-0 mb-5" />
|
||||||
<div v-if="type==='days'">
|
<div v-if="type === 'days'">
|
||||||
<div class="columns is-mobile mx-0 mb-3">
|
<div class="columns is-mobile mx-0 mb-3">
|
||||||
<div
|
<div
|
||||||
v-for="(m, h) in dateOfWeek"
|
v-for="(m, h) in dateOfWeek"
|
||||||
:key="h"
|
:key="h"
|
||||||
class="fs-14 column p-0 has-text-grey is-flex is-justify-content-center is-align-items-center"
|
class="fs-14 column p-0 has-text-grey is-flex is-justify-content-center is-align-items-center"
|
||||||
>
|
>
|
||||||
{{ m.text }}
|
{{ m.text }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`columns is-mobile mx-0 mb-1 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
|
<div
|
||||||
<div class="column p-0 is-flex is-justify-content-center is-align-items-center" style="width: 40px; height: 32px" v-for="(m,h) in v.dates" :key="h">
|
:class="`columns is-mobile mx-0 mb-1 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
|
||||||
<span class="fs-13 has-text-grey-80" v-if="m.disable">
|
v-for="(v, i) in weeks"
|
||||||
{{m.dayPrint}}
|
:key="i"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="column p-0 is-flex is-justify-content-center is-align-items-center"
|
||||||
|
style="width: 40px; height: 32px"
|
||||||
|
v-for="(m, h) in v.dates"
|
||||||
|
:key="h"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="fs-13 has-text-grey-80"
|
||||||
|
v-if="m.disable"
|
||||||
|
>
|
||||||
|
{{ m.dayPrint }}
|
||||||
</span>
|
</span>
|
||||||
<span class="fs-13 is-clickable" @click="choose(m)" v-else>
|
<span
|
||||||
<span
|
class="fs-13 is-clickable"
|
||||||
|
@click="choose(m)"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<span
|
||||||
style="width: 25px; height: 25px"
|
style="width: 25px; height: 25px"
|
||||||
:class="[
|
:class="[
|
||||||
'p-1 rounded-md is-flex is-justify-content-center is-align-items-center',
|
'p-1 rounded-md is-flex is-justify-content-center is-align-items-center',
|
||||||
{
|
{
|
||||||
'has-background-primary-50 has-text-white': m.date === curdate,
|
'has-background-primary-50 has-text-white': m.date === curdate,
|
||||||
'has-background-success-50 has-text-white': m.date === today,
|
'has-background-success-50 has-text-white': m.date === today,
|
||||||
'has-text-grey-70': m.currentMonth !== m.monthCondition
|
'has-text-grey-70': m.currentMonth !== m.monthCondition,
|
||||||
}
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ m.dayPrint }}
|
{{ m.dayPrint }}
|
||||||
@@ -58,133 +96,165 @@
|
|||||||
<hr class="my-1" />
|
<hr class="my-1" />
|
||||||
<div class="mt-2 fs-14">
|
<div class="mt-2 fs-14">
|
||||||
<span class="ml-2">Hôm nay: </span>
|
<span class="ml-2">Hôm nay: </span>
|
||||||
<a class="has-text-primary" @click="chooseToday()">{{ $dayjs(today).format('DD/MM/YYYY') }}</a>
|
<a
|
||||||
|
class="has-text-primary"
|
||||||
|
@click="chooseToday()"
|
||||||
|
>{{ $dayjs(today).format("DD/MM/YYYY") }}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PickMonth v-else-if="type==='months'" @month="selectMonth"></PickMonth>
|
<PickMonth
|
||||||
<PickYear v-else-if="type==='years'" v-bind="{ year, month, action }" @year="selectYear" @caption="changeCaption"></PickYear>
|
v-else-if="type === 'months'"
|
||||||
|
@month="selectMonth"
|
||||||
|
></PickMonth>
|
||||||
|
<PickYear
|
||||||
|
v-else-if="type === 'years'"
|
||||||
|
v-bind="{ year, month, action }"
|
||||||
|
@year="selectYear"
|
||||||
|
@caption="changeCaption"
|
||||||
|
></PickYear>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import PickMonth from '@/components/datepicker/PickMonth'
|
import PickMonth from "@/components/datepicker/PickMonth";
|
||||||
import PickYear from '@/components/datepicker/PickYear'
|
import PickYear from "@/components/datepicker/PickYear";
|
||||||
const { $id, $dayjs, $unique} = useNuxtApp()
|
const { $id, $dayjs, $unique } = useNuxtApp();
|
||||||
const emit = defineEmits(['date'])
|
const emit = defineEmits(["date"]);
|
||||||
var props = defineProps({
|
var props = defineProps({
|
||||||
date: String,
|
date: String,
|
||||||
maxdate: String
|
maxdate: String,
|
||||||
})
|
});
|
||||||
var dates = []
|
var dates = [];
|
||||||
var dateOfWeek = [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"},
|
var dateOfWeek = [
|
||||||
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}]
|
{ id: 0, text: "CN" },
|
||||||
var weeks = ref([])
|
{ id: 1, text: "T2" },
|
||||||
var year= ref(undefined)
|
{ id: 2, text: "T3" },
|
||||||
var month = undefined
|
{ id: 3, text: "T4" },
|
||||||
var type = ref('days')
|
{ id: 4, text: "T5" },
|
||||||
var caption = ref(undefined)
|
{ id: 5, text: "T6" },
|
||||||
var action = ref(undefined)
|
{ id: 6, text: "T7" },
|
||||||
var curdate = undefined
|
];
|
||||||
var today = new Date()
|
var weeks = ref([]);
|
||||||
function showDate() {
|
var year = ref(undefined);
|
||||||
curdate = props.date? props.date.replaceAll('-', '/') : undefined
|
var month = undefined;
|
||||||
year.value = $dayjs(curdate || today).year()
|
var type = ref("days");
|
||||||
month = $dayjs(curdate || today).month() + 1
|
var caption = ref(undefined);
|
||||||
getDates()
|
var action = ref(undefined);
|
||||||
}
|
var curdate = undefined;
|
||||||
function chooseToday() {
|
var today = new Date();
|
||||||
emit('date', $dayjs(today).format('YYYY-MM-DD') )//today.replaceAll('/', '-'))
|
function showDate() {
|
||||||
year.value = $dayjs(today).year()
|
curdate = props.date ? props.date.replaceAll("-", "/") : undefined;
|
||||||
month = $dayjs(today).month() + 1
|
year.value = $dayjs(curdate || today).year();
|
||||||
getDates()
|
month = $dayjs(curdate || today).month() + 1;
|
||||||
}
|
getDates();
|
||||||
function changeCaption(v) {
|
}
|
||||||
caption.value = v
|
function chooseToday() {
|
||||||
}
|
emit("date", $dayjs(today).format("YYYY-MM-DD")); //today.replaceAll('/', '-'))
|
||||||
function selectMonth(v) {
|
year.value = $dayjs(today).year();
|
||||||
month = v
|
month = $dayjs(today).month() + 1;
|
||||||
getDates()
|
getDates();
|
||||||
type.value = 'days'
|
}
|
||||||
}
|
function changeCaption(v) {
|
||||||
function selectYear(v) {
|
caption.value = v;
|
||||||
year.value = v
|
}
|
||||||
getDates()
|
function selectMonth(v) {
|
||||||
type.value = 'days'
|
month = v;
|
||||||
}
|
getDates();
|
||||||
function getDates() {
|
type.value = "days";
|
||||||
caption.value = undefined
|
}
|
||||||
dates = allDaysInMonth(year.value, month)
|
function selectYear(v) {
|
||||||
weeks.value = $unique(dates, ['week']).map(v=>{return {week: v.week}})
|
year.value = v;
|
||||||
weeks.value.map(v=>{
|
getDates();
|
||||||
v.dates = dates.filter(x=>x.week===v.week)
|
type.value = "days";
|
||||||
})
|
}
|
||||||
}
|
function getDates() {
|
||||||
function nextMonth() {
|
caption.value = undefined;
|
||||||
month = month + 1
|
dates = allDaysInMonth(year.value, month);
|
||||||
if(month>12) {
|
weeks.value = $unique(dates, ["week"]).map((v) => {
|
||||||
month = 1
|
return { week: v.week };
|
||||||
year.value += 1
|
});
|
||||||
}
|
weeks.value.map((v) => {
|
||||||
getDates()
|
v.dates = dates.filter((x) => x.week === v.week);
|
||||||
}
|
});
|
||||||
function previousMonth() {
|
}
|
||||||
month = month - 1
|
function nextMonth() {
|
||||||
if(month===0) {
|
month = month + 1;
|
||||||
month = 12
|
if (month > 12) {
|
||||||
year.value -= 1
|
month = 1;
|
||||||
}
|
year.value += 1;
|
||||||
getDates()
|
}
|
||||||
}
|
getDates();
|
||||||
function nextYear() {
|
}
|
||||||
if(type.value==='years') return action.value = {name: 'next', id: $id()}
|
function previousMonth() {
|
||||||
year.value += 1
|
month = month - 1;
|
||||||
getDates()
|
if (month === 0) {
|
||||||
}
|
month = 12;
|
||||||
function previousYear() {
|
year.value -= 1;
|
||||||
if(type.value==='years') return action.value = {name: 'previous', id: $id()}
|
}
|
||||||
year.value -= 1
|
getDates();
|
||||||
getDates()
|
}
|
||||||
}
|
function nextYear() {
|
||||||
function choose(m) {
|
if (type.value === "years") return (action.value = { name: "next", id: $id() });
|
||||||
emit('date', m.date.replaceAll('/', '-'))
|
year.value += 1;
|
||||||
}
|
getDates();
|
||||||
function createDate(v, x, y) {
|
}
|
||||||
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString())
|
function previousYear() {
|
||||||
}
|
if (type.value === "years") return (action.value = { name: "previous", id: $id() });
|
||||||
function allDaysInMonth(year, month) {
|
year.value -= 1;
|
||||||
let days = Array.from({length: $dayjs(createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
|
getDates();
|
||||||
let arr = []
|
}
|
||||||
days.map(v=>{
|
function choose(m) {
|
||||||
for (let i = 0; i < 7; i++) {
|
emit("date", m.date.replaceAll("/", "-"));
|
||||||
let thedate = $dayjs(createDate(year, month, v)).weekday(i)
|
}
|
||||||
let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD")
|
function createDate(v, x, y) {
|
||||||
let dayPrint = $dayjs(new Date(thedate.$d)).format("DD")
|
return v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString());
|
||||||
let monthCondition = $dayjs(date).month() +1
|
}
|
||||||
let currentMonth = month
|
function allDaysInMonth(year, month) {
|
||||||
let found = arr.find(x=>x.date===date)
|
let days = Array.from({ length: $dayjs(createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
|
||||||
if(!found) {
|
let arr = [];
|
||||||
let dayOfWeek = $dayjs(date).day()
|
days.map((v) => {
|
||||||
let week = $dayjs(date).week()
|
for (let i = 0; i < 7; i++) {
|
||||||
let disable = false
|
let thedate = $dayjs(createDate(year, month, v)).weekday(i);
|
||||||
if(props.maxdate? $dayjs(props.maxdate).diff(props.date, 'day')>=0 : false) {
|
let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
|
||||||
if($dayjs(props.maxdate).startOf('day').diff(date, 'day')<0) disable = true
|
let dayPrint = $dayjs(new Date(thedate.$d)).format("DD");
|
||||||
}
|
let monthCondition = $dayjs(date).month() + 1;
|
||||||
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, monthCondition: monthCondition,
|
let currentMonth = month;
|
||||||
currentMonth: currentMonth, disable: disable}
|
let found = arr.find((x) => x.date === date);
|
||||||
arr.push(ele)
|
if (!found) {
|
||||||
}
|
let dayOfWeek = $dayjs(date).day();
|
||||||
|
let week = $dayjs(date).week();
|
||||||
|
let disable = false;
|
||||||
|
if (props.maxdate ? $dayjs(props.maxdate).diff(props.date, "day") >= 0 : false) {
|
||||||
|
if ($dayjs(props.maxdate).startOf("day").diff(date, "day") < 0) disable = true;
|
||||||
}
|
}
|
||||||
})
|
let ele = {
|
||||||
return arr
|
date: date,
|
||||||
|
week: week,
|
||||||
|
day: v,
|
||||||
|
dayOfWeek: dayOfWeek,
|
||||||
|
dayPrint: dayPrint,
|
||||||
|
monthCondition: monthCondition,
|
||||||
|
currentMonth: currentMonth,
|
||||||
|
disable: disable,
|
||||||
|
};
|
||||||
|
arr.push(ele);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// display
|
});
|
||||||
showDate()
|
return arr;
|
||||||
// change date
|
}
|
||||||
watch(() => props.date, (newVal, oldVal) => {
|
// display
|
||||||
showDate()
|
showDate();
|
||||||
})
|
// change date
|
||||||
|
watch(
|
||||||
|
() => props.date,
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
showDate();
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
a {
|
a {
|
||||||
color: var(--bulma-link-60);
|
color: var(--bulma-link-60);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="columns is-mobile is-multiline mx-0">
|
<div class="columns is-mobile is-multiline mx-0">
|
||||||
<span class="column has-text-centered is-4 is-clickable fs-14" v-for="v in months" @click="$emit('month', v)">Tháng {{ v }}</span>
|
<span
|
||||||
|
class="column has-text-centered is-4 is-clickable fs-14"
|
||||||
|
v-for="v in months"
|
||||||
|
@click="$emit('month', v)"
|
||||||
|
>Tháng {{ v }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
months: [1,2,3,4,5,6,7,8,9,10,11,12]
|
months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||||
}
|
};
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,57 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="columns is-mobile is-multiline mx-0">
|
<div class="columns is-mobile is-multiline mx-0">
|
||||||
<span
|
<span
|
||||||
v-for="(v,i) in years"
|
v-for="(v, i) in years"
|
||||||
class="column is-4 has-text-centered is-clickable fs-14"
|
class="column is-4 has-text-centered is-clickable fs-14"
|
||||||
:class="i===0 || i===11 ? 'has-text-grey-light' : ''"
|
:class="i === 0 || i === 11 ? 'has-text-grey-light' : ''"
|
||||||
@click="$emit('year', v)">{{ v }}</span>
|
@click="$emit('year', v)"
|
||||||
|
>{{ v }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['year', 'month', 'action'],
|
props: ["year", "month", "action"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
years: []
|
years: [],
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.years = [this.year]
|
this.years = [this.year];
|
||||||
for(let i = 1; i < 7; i++) {
|
for (let i = 1; i < 7; i++) {
|
||||||
this.years.push(this.year+i)
|
this.years.push(this.year + i);
|
||||||
this.years.push(this.year-i)
|
this.years.push(this.year - i);
|
||||||
}
|
}
|
||||||
this.years.sort(function(a, b) {return a - b;})
|
this.years.sort(function (a, b) {
|
||||||
this.years = this.years.slice(0,12)
|
return a - b;
|
||||||
this.$emit('caption', `${this.years[1]}-${this.years[10]}`)
|
});
|
||||||
|
this.years = this.years.slice(0, 12);
|
||||||
|
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
action: function(newVal) {
|
action: function (newVal) {
|
||||||
if(newVal.name==='next') this.next()
|
if (newVal.name === "next") this.next();
|
||||||
if(newVal.name==='previous') this.previous()
|
if (newVal.name === "previous") this.previous();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
next() {
|
next() {
|
||||||
let year = this.years[this.years.length-1]
|
let year = this.years[this.years.length - 1];
|
||||||
this.years = []
|
this.years = [];
|
||||||
for(let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
this.years.push(year+i)
|
this.years.push(year + i);
|
||||||
}
|
}
|
||||||
this.years.sort(function(a, b) {return a - b;})
|
this.years.sort(function (a, b) {
|
||||||
this.years = this.years.slice(0,12)
|
return a - b;
|
||||||
this.$emit('caption', `${this.years[1]}-${this.years[10]}`)
|
});
|
||||||
|
this.years = this.years.slice(0, 12);
|
||||||
|
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
|
||||||
},
|
},
|
||||||
previous() {
|
previous() {
|
||||||
let year = this.years[0]
|
let year = this.years[0];
|
||||||
this.years = []
|
this.years = [];
|
||||||
for(let i = 0; i < 12; i++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
this.years.push(year-i)
|
this.years.push(year - i);
|
||||||
}
|
}
|
||||||
this.years.sort(function(a, b) {return a - b;})
|
this.years.sort(function (a, b) {
|
||||||
this.years = this.years.slice(0,12)
|
return a - b;
|
||||||
this.$emit('caption', `${this.years[1]}-${this.years[10]}`)
|
});
|
||||||
}
|
this.years = this.years.slice(0, 12);
|
||||||
}
|
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Template1 from '@/lib/email/templates/Template1.vue';
|
import Template1 from "@/lib/email/templates/Template1.vue";
|
||||||
import { render } from '@vue-email/render';
|
import { render } from "@vue-email/render";
|
||||||
import { forEachAsync, isEqual } from 'es-toolkit';
|
import { forEachAsync, isEqual } from "es-toolkit";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
$dayjs,
|
$dayjs,
|
||||||
$getdata,
|
$getdata,
|
||||||
$insertapi,
|
$insertapi,
|
||||||
@@ -19,9 +19,9 @@ const {
|
|||||||
const payables = ref(null);
|
const payables = ref(null);
|
||||||
const defaultFilter = {
|
const defaultFilter = {
|
||||||
status: 1,
|
status: 1,
|
||||||
to_date__gte: $dayjs().format('YYYY-MM-DD'),
|
to_date__gte: $dayjs().format("YYYY-MM-DD"),
|
||||||
to_date__lte: undefined,
|
to_date__lte: undefined,
|
||||||
}
|
};
|
||||||
const filter = ref(defaultFilter);
|
const filter = ref(defaultFilter);
|
||||||
const activeDateFilter = ref(null);
|
const activeDateFilter = ref(null);
|
||||||
const key = ref(0);
|
const key = ref(0);
|
||||||
@@ -35,35 +35,42 @@ function resetDateFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'duepayables' }, sort: 'index' });
|
const payablesData = await $getdata("bizsetting", undefined, {
|
||||||
|
filter: { classify: "duepayables" },
|
||||||
|
sort: "index",
|
||||||
|
});
|
||||||
payables.value = payablesData;
|
payables.value = payablesData;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(activeDateFilter, (val) => {
|
watch(
|
||||||
if (!val) {
|
activeDateFilter,
|
||||||
filter.value = defaultFilter;
|
(val) => {
|
||||||
contents.value = null;
|
if (!val) {
|
||||||
} else {
|
filter.value = defaultFilter;
|
||||||
const cutoffDate = $dayjs().add(val.time, 'day').format('YYYY-MM-DD');
|
contents.value = null;
|
||||||
const filterField = `to_date__${val.lookup}`;
|
} else {
|
||||||
filter.value = {
|
const cutoffDate = $dayjs().add(val.time, "day").format("YYYY-MM-DD");
|
||||||
...defaultFilter,
|
const filterField = `to_date__${val.lookup}`;
|
||||||
[filterField]: cutoffDate,
|
filter.value = {
|
||||||
|
...defaultFilter,
|
||||||
|
[filterField]: cutoffDate,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, { deep: true })
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
const contents = ref(null);
|
const contents = ref(null);
|
||||||
const isSending = ref(false);
|
const isSending = ref(false);
|
||||||
|
|
||||||
function sanitizeContentPayment(text, maxLength = 80) {
|
function sanitizeContentPayment(text, maxLength = 80) {
|
||||||
if (!text) return '';
|
if (!text) return "";
|
||||||
|
|
||||||
return text
|
return text
|
||||||
.normalize('NFD') // bỏ dấu tiếng Việt
|
.normalize("NFD") // bỏ dấu tiếng Việt
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ
|
.replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, " ")
|
||||||
.trim()
|
.trim()
|
||||||
.slice(0, maxLength);
|
.slice(0, maxLength);
|
||||||
}
|
}
|
||||||
@@ -88,19 +95,19 @@ const buildContentPayment = (data) => {
|
|||||||
cycle,
|
cycle,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
if (customerType.toLowerCase() === 'cn') {
|
if (customerType.toLowerCase() === "cn") {
|
||||||
if (customerName.length < 14) {
|
if (customerName.length < 14) {
|
||||||
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||||
} else {
|
} else {
|
||||||
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function replaceTemplateVars(html, paymentScheduleItem) {
|
function replaceTemplateVars(html, paymentScheduleItem) {
|
||||||
const {
|
const {
|
||||||
txn_detail__transaction__product__trade_code,
|
txn_detail__transaction__product__trade_code,
|
||||||
txn_detail__transaction__customer__code,
|
txn_detail__transaction__customer__code,
|
||||||
txn_detail__transaction__customer__fullname,
|
txn_detail__transaction__customer__fullname,
|
||||||
@@ -112,118 +119,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
|
|||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
remain_amount,
|
remain_amount,
|
||||||
cycle
|
cycle,
|
||||||
} = paymentScheduleItem;
|
} = paymentScheduleItem;
|
||||||
return html
|
return html
|
||||||
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '')
|
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
|
||||||
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '')
|
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
|
||||||
.replace(/\[year]/g, new Date().getFullYear() || '')
|
.replace(/\[year]/g, new Date().getFullYear() || "")
|
||||||
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
|
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
|
||||||
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
|
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
|
||||||
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
|
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
|
||||||
.replace(
|
.replace(
|
||||||
/\[customer\.name\]/g,
|
/\[customer\.name\]/g,
|
||||||
`${txn_detail__transaction__customer__type__code.toLowerCase() == 'cn' ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ''}` ||
|
`${txn_detail__transaction__customer__type__code.toLowerCase() == "cn" ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ""}` ||
|
||||||
'',
|
"",
|
||||||
)
|
|
||||||
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
|
|
||||||
.replace(
|
|
||||||
/\[customer\.legal_code\]/g,
|
|
||||||
txn_detail__transaction__customer__legal_code || '',
|
|
||||||
)
|
)
|
||||||
|
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || "")
|
||||||
|
.replace(/\[customer\.legal_code\]/g, txn_detail__transaction__customer__legal_code || "")
|
||||||
.replace(
|
.replace(
|
||||||
/\[customer\.contact_address\]/g,
|
/\[customer\.contact_address\]/g,
|
||||||
txn_detail__transaction__customer__contact_address ||
|
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
|
||||||
txn_detail__transaction__customer__address ||
|
|
||||||
'',
|
|
||||||
)
|
)
|
||||||
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '')
|
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
|
||||||
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '')
|
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
|
||||||
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '')
|
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
|
||||||
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '')
|
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
|
||||||
.replace(
|
.replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
|
||||||
/\[payment_schedule\.amount_in_word\]/g,
|
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
|
||||||
$numberToVietnameseCurrency(remain_amount) || '',
|
|
||||||
)
|
|
||||||
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
|
|
||||||
.replace(
|
.replace(
|
||||||
/\[payment_schedule\.cycle-in-words\]/g,
|
/\[payment_schedule\.cycle-in-words\]/g,
|
||||||
`${cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` ||
|
`${cycle == 0 ? "đặt cọc" : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || "",
|
||||||
'',
|
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
|
||||||
/\[payment_schedule\.note\]/g,
|
|
||||||
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function quillToEmailHtml(html) {
|
function quillToEmailHtml(html) {
|
||||||
return html
|
return (
|
||||||
// ALIGN
|
html
|
||||||
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
// ALIGN
|
||||||
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
||||||
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
||||||
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
||||||
|
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
||||||
|
|
||||||
// FONT SIZE
|
// FONT SIZE
|
||||||
.replace(/ql-size-small/g, '')
|
.replace(/ql-size-small/g, "")
|
||||||
.replace(/ql-size-large/g, '')
|
.replace(/ql-size-large/g, "")
|
||||||
.replace(/ql-size-huge/g, '')
|
.replace(/ql-size-huge/g, "")
|
||||||
|
|
||||||
// REMOVE EMPTY CLASS
|
// REMOVE EMPTY CLASS
|
||||||
.replace(/class=""/g, '')
|
.replace(/class=""/g, "")
|
||||||
;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showmodal = ref(null);
|
const showmodal = ref(null);
|
||||||
|
|
||||||
function openConfirmModal() {
|
function openConfirmModal() {
|
||||||
showmodal.value = {
|
showmodal.value = {
|
||||||
component: 'dialog/Confirm',
|
component: "dialog/Confirm",
|
||||||
title: 'Xác nhận',
|
title: "Xác nhận",
|
||||||
width: '500px',
|
width: "500px",
|
||||||
height: '100px',
|
height: "100px",
|
||||||
vbind: {
|
vbind: {
|
||||||
content: 'Bạn có đồng ý gửi thông báo hàng loạt không?',
|
content: "Bạn có đồng ý gửi thông báo hàng loạt không?",
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendEmails() {
|
async function sendEmails() {
|
||||||
isSending.value = true;
|
isSending.value = true;
|
||||||
$snackbar('Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...');
|
$snackbar("Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...");
|
||||||
|
|
||||||
const paymentScheduleData = await $getdata(
|
const paymentScheduleData = await $getdata("payment_schedule", undefined, {
|
||||||
'payment_schedule',
|
filter: filter.value,
|
||||||
undefined,
|
sort: "to_date",
|
||||||
{
|
values:
|
||||||
filter: filter.value,
|
"penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry",
|
||||||
sort: 'to_date',
|
});
|
||||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailTemplate = await $getdata(
|
const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
|
||||||
'emailtemplate',
|
|
||||||
{ id: activeDateFilter.value.emailTemplate },
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
let message = emailTemplate.content.content;
|
let message = emailTemplate.content.content;
|
||||||
|
|
||||||
contents.value = paymentScheduleData.map(paymentSchedule => {
|
contents.value = paymentScheduleData.map((paymentSchedule) => {
|
||||||
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, '');
|
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
|
||||||
const transfer = {
|
const transfer = {
|
||||||
bank: {
|
bank: {
|
||||||
code: 'MB',
|
code: "MB",
|
||||||
name: 'MB Bank',
|
name: "MB Bank",
|
||||||
},
|
},
|
||||||
account: {
|
account: {
|
||||||
number: '146768686868',
|
number: "146768686868",
|
||||||
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA',
|
name: "CONG TY CO PHAN BAT DONG SAN UTOPIA",
|
||||||
},
|
},
|
||||||
content: 'Thanh toán đơn #xyz',
|
content: "Thanh toán đơn #xyz",
|
||||||
};
|
};
|
||||||
|
|
||||||
transfer.content = buildContentPayment(paymentSchedule);
|
transfer.content = buildContentPayment(paymentSchedule);
|
||||||
@@ -242,7 +230,7 @@ async function sendEmails() {
|
|||||||
...emailTemplate.content,
|
...emailTemplate.content,
|
||||||
content: undefined,
|
content: undefined,
|
||||||
message: replaceTemplateVars(message, paymentSchedule),
|
message: replaceTemplateVars(message, paymentSchedule),
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await forEachAsync(contents.value, async (bigContent, i) => {
|
await forEachAsync(contents.value, async (bigContent, i) => {
|
||||||
@@ -251,16 +239,16 @@ async function sendEmails() {
|
|||||||
content: toRaw(bigContent),
|
content: toRaw(bigContent),
|
||||||
previewMode: true,
|
previewMode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
||||||
tempEm.content.message = quillToEmailHtml(message);
|
tempEm.content.message = quillToEmailHtml(message);
|
||||||
let emailHtml = await render(Template1, tempEm);
|
let emailHtml = await render(Template1, tempEm);
|
||||||
|
|
||||||
// If no image URL provided, remove image section from HTML
|
// If no image URL provided, remove image section from HTML
|
||||||
if ((imageUrl ?? '').trim() === '') {
|
if ((imageUrl ?? "").trim() === "") {
|
||||||
emailHtml = emailHtml
|
emailHtml = emailHtml
|
||||||
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '')
|
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
|
||||||
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
.replace(/\n\s*\n\s*\n/g, "\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace keywords in HTML
|
// Replace keywords in HTML
|
||||||
@@ -268,55 +256,65 @@ async function sendEmails() {
|
|||||||
if (keyword && keyword.length > 0) {
|
if (keyword && keyword.length > 0) {
|
||||||
keyword.forEach(({ keyword, value }) => {
|
keyword.forEach(({ keyword, value }) => {
|
||||||
if (keyword && value) {
|
if (keyword && value) {
|
||||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g');
|
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
|
||||||
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await $insertapi(
|
const response = await $insertapi(
|
||||||
'sendemail',
|
"sendemail",
|
||||||
{
|
{
|
||||||
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
|
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
|
||||||
content: finalEmailHtml,
|
content: finalEmailHtml,
|
||||||
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || 'Thông báo từ Utopia Villas & Resort',
|
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || "Thông báo từ Utopia Villas & Resort",
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response !== null) {
|
if (response !== null) {
|
||||||
await $insertapi('productnote', {
|
await $insertapi(
|
||||||
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
"productnote",
|
||||||
user: $store.login.id,
|
{
|
||||||
detail: `Đã gửi email thông báo nhắc nợ cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format('HH:mm ngày DD/MM/YYYY')}.`
|
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
||||||
}, undefined, false);
|
user: $store.login.id,
|
||||||
|
detail: `Đã gửi email thông báo nhắc nợ cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format("HH:mm ngày DD/MM/YYYY")}.`,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$snackbar('Thông báo đã được gửi thành công đến các khách hàng.');
|
$snackbar("Thông báo đã được gửi thành công đến các khách hàng.");
|
||||||
isSending.value = false;
|
isSending.value = false;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(filter, () => {
|
watch(
|
||||||
key.value += 1;
|
filter,
|
||||||
}, { deep: true })
|
() => {
|
||||||
|
key.value += 1;
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
|
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
|
||||||
<div class="buttons m-0">
|
<div class="buttons m-0">
|
||||||
<p>Đến hạn:</p>
|
<p>Đến hạn:</p>
|
||||||
<button
|
<button
|
||||||
v-for="payable in payables"
|
v-for="payable in payables"
|
||||||
:key="payable.id"
|
:key="payable.id"
|
||||||
@click="setDateFilter(payable.detail)"
|
@click="setDateFilter(payable.detail)"
|
||||||
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
|
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
|
||||||
>
|
>
|
||||||
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày
|
{{ payable.detail.lookup === "lte" ? "≤" : ">" }}
|
||||||
|
{{ payable.detail.time }} ngày
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="activeDateFilter"
|
v-if="activeDateFilter"
|
||||||
@click="resetDateFilter()"
|
@click="resetDateFilter()"
|
||||||
class="button is-white"
|
class="button is-white"
|
||||||
@@ -324,7 +322,7 @@ watch(filter, () => {
|
|||||||
Xoá lọc
|
Xoá lọc
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="activeDateFilter"
|
v-if="activeDateFilter"
|
||||||
@click="openConfirmModal()"
|
@click="openConfirmModal()"
|
||||||
:class="['button', 'is-light', { 'is-loading': isSending }]"
|
:class="['button', 'is-light', { 'is-loading': isSending }]"
|
||||||
@@ -342,16 +340,18 @@ watch(filter, () => {
|
|||||||
params: {
|
params: {
|
||||||
filter,
|
filter,
|
||||||
sort: 'to_date',
|
sort: 'to_date',
|
||||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
values:
|
||||||
}
|
'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||||
}" />
|
},
|
||||||
<Modal
|
}"
|
||||||
v-if="showmodal"
|
|
||||||
v-bind="showmodal"
|
|
||||||
@confirm="sendEmails()"
|
|
||||||
@close="showmodal = undefined"
|
|
||||||
/>
|
/>
|
||||||
<!-- <div class="is-flex is-gap-1">
|
<Modal
|
||||||
|
v-if="showmodal"
|
||||||
|
v-bind="showmodal"
|
||||||
|
@confirm="sendEmails()"
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
/>
|
||||||
|
<!-- <div class="is-flex is-gap-1">
|
||||||
// debug
|
// debug
|
||||||
<Template1
|
<Template1
|
||||||
v-if="contents"
|
v-if="contents"
|
||||||
@@ -360,4 +360,4 @@ watch(filter, () => {
|
|||||||
previewMode
|
previewMode
|
||||||
/>
|
/>
|
||||||
</div> -->
|
</div> -->
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Template1 from '@/lib/email/templates/Template1.vue';
|
import Template1 from "@/lib/email/templates/Template1.vue";
|
||||||
import { render } from '@vue-email/render';
|
import { render } from "@vue-email/render";
|
||||||
import { forEachAsync, isEqual } from 'es-toolkit';
|
import { forEachAsync, isEqual } from "es-toolkit";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
$dayjs,
|
$dayjs,
|
||||||
$getdata,
|
$getdata,
|
||||||
$insertapi,
|
$insertapi,
|
||||||
@@ -19,8 +19,8 @@ const {
|
|||||||
const payables = ref(null);
|
const payables = ref(null);
|
||||||
const defaultFilter = {
|
const defaultFilter = {
|
||||||
status: 1,
|
status: 1,
|
||||||
to_date__lt: $dayjs().format('YYYY-MM-DD'),
|
to_date__lt: $dayjs().format("YYYY-MM-DD"),
|
||||||
}
|
};
|
||||||
const filter = ref(defaultFilter);
|
const filter = ref(defaultFilter);
|
||||||
const activeDateFilter = ref(null);
|
const activeDateFilter = ref(null);
|
||||||
const key = ref(0);
|
const key = ref(0);
|
||||||
@@ -34,36 +34,42 @@ function resetDateFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'overduepayables' }, sort: 'index' });
|
const payablesData = await $getdata("bizsetting", undefined, {
|
||||||
|
filter: { classify: "overduepayables" },
|
||||||
|
sort: "index",
|
||||||
|
});
|
||||||
payables.value = payablesData;
|
payables.value = payablesData;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(activeDateFilter, (val) => {
|
watch(
|
||||||
if (!val) {
|
activeDateFilter,
|
||||||
filter.value = defaultFilter;
|
(val) => {
|
||||||
contents.value = null;
|
if (!val) {
|
||||||
} else {
|
filter.value = defaultFilter;
|
||||||
const cutoffDate = $dayjs().subtract(val.time, 'day').format('YYYY-MM-DD');
|
contents.value = null;
|
||||||
const filterField = `to_date__${val.lookup === 'lte' ? 'gt' :
|
} else {
|
||||||
'lte'}`;
|
const cutoffDate = $dayjs().subtract(val.time, "day").format("YYYY-MM-DD");
|
||||||
filter.value = {
|
const filterField = `to_date__${val.lookup === "lte" ? "gt" : "lte"}`;
|
||||||
...defaultFilter,
|
filter.value = {
|
||||||
[filterField]: cutoffDate,
|
...defaultFilter,
|
||||||
|
[filterField]: cutoffDate,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, { deep: true })
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
const contents = ref(null);
|
const contents = ref(null);
|
||||||
const isSending = ref(false);
|
const isSending = ref(false);
|
||||||
|
|
||||||
function sanitizeContentPayment(text, maxLength = 80) {
|
function sanitizeContentPayment(text, maxLength = 80) {
|
||||||
if (!text) return '';
|
if (!text) return "";
|
||||||
|
|
||||||
return text
|
return text
|
||||||
.normalize('NFD') // bỏ dấu tiếng Việt
|
.normalize("NFD") // bỏ dấu tiếng Việt
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ
|
.replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, " ")
|
||||||
.trim()
|
.trim()
|
||||||
.slice(0, maxLength);
|
.slice(0, maxLength);
|
||||||
}
|
}
|
||||||
@@ -88,19 +94,19 @@ const buildContentPayment = (data) => {
|
|||||||
cycle,
|
cycle,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
if (customerType.toLowerCase() === 'cn') {
|
if (customerType.toLowerCase() === "cn") {
|
||||||
if (customerName.length < 14) {
|
if (customerName.length < 14) {
|
||||||
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||||
} else {
|
} else {
|
||||||
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function replaceTemplateVars(html, paymentScheduleItem) {
|
function replaceTemplateVars(html, paymentScheduleItem) {
|
||||||
const {
|
const {
|
||||||
txn_detail__transaction__product__trade_code,
|
txn_detail__transaction__product__trade_code,
|
||||||
txn_detail__transaction__customer__code,
|
txn_detail__transaction__customer__code,
|
||||||
txn_detail__transaction__customer__fullname,
|
txn_detail__transaction__customer__fullname,
|
||||||
@@ -112,118 +118,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
|
|||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
remain_amount,
|
remain_amount,
|
||||||
cycle
|
cycle,
|
||||||
} = paymentScheduleItem;
|
} = paymentScheduleItem;
|
||||||
return html
|
return html
|
||||||
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '')
|
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
|
||||||
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '')
|
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
|
||||||
.replace(/\[year]/g, new Date().getFullYear() || '')
|
.replace(/\[year]/g, new Date().getFullYear() || "")
|
||||||
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
|
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
|
||||||
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
|
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
|
||||||
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
|
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
|
||||||
.replace(
|
.replace(
|
||||||
/\[customer\.name\]/g,
|
/\[customer\.name\]/g,
|
||||||
`${txn_detail__transaction__customer__type__code.toLowerCase() == 'cn' ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ''}` ||
|
`${txn_detail__transaction__customer__type__code.toLowerCase() == "cn" ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ""}` ||
|
||||||
'',
|
"",
|
||||||
)
|
|
||||||
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
|
|
||||||
.replace(
|
|
||||||
/\[customer\.legal_code\]/g,
|
|
||||||
txn_detail__transaction__customer__legal_code || '',
|
|
||||||
)
|
)
|
||||||
|
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || "")
|
||||||
|
.replace(/\[customer\.legal_code\]/g, txn_detail__transaction__customer__legal_code || "")
|
||||||
.replace(
|
.replace(
|
||||||
/\[customer\.contact_address\]/g,
|
/\[customer\.contact_address\]/g,
|
||||||
txn_detail__transaction__customer__contact_address ||
|
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
|
||||||
txn_detail__transaction__customer__address ||
|
|
||||||
'',
|
|
||||||
)
|
)
|
||||||
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '')
|
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
|
||||||
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '')
|
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
|
||||||
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '')
|
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
|
||||||
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '')
|
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
|
||||||
.replace(
|
.replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
|
||||||
/\[payment_schedule\.amount_in_word\]/g,
|
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
|
||||||
$numberToVietnameseCurrency(remain_amount) || '',
|
|
||||||
)
|
|
||||||
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
|
|
||||||
.replace(
|
.replace(
|
||||||
/\[payment_schedule\.cycle-in-words\]/g,
|
/\[payment_schedule\.cycle-in-words\]/g,
|
||||||
`${cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` ||
|
`${cycle == 0 ? "đặt cọc" : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || "",
|
||||||
'',
|
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
|
||||||
/\[payment_schedule\.note\]/g,
|
|
||||||
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function quillToEmailHtml(html) {
|
function quillToEmailHtml(html) {
|
||||||
return html
|
return (
|
||||||
// ALIGN
|
html
|
||||||
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
// ALIGN
|
||||||
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
||||||
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
||||||
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
||||||
|
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
||||||
|
|
||||||
// FONT SIZE
|
// FONT SIZE
|
||||||
.replace(/ql-size-small/g, '')
|
.replace(/ql-size-small/g, "")
|
||||||
.replace(/ql-size-large/g, '')
|
.replace(/ql-size-large/g, "")
|
||||||
.replace(/ql-size-huge/g, '')
|
.replace(/ql-size-huge/g, "")
|
||||||
|
|
||||||
// REMOVE EMPTY CLASS
|
// REMOVE EMPTY CLASS
|
||||||
.replace(/class=""/g, '')
|
.replace(/class=""/g, "")
|
||||||
;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showmodal = ref(null);
|
const showmodal = ref(null);
|
||||||
|
|
||||||
function openConfirmModal() {
|
function openConfirmModal() {
|
||||||
showmodal.value = {
|
showmodal.value = {
|
||||||
component: 'dialog/Confirm',
|
component: "dialog/Confirm",
|
||||||
title: 'Xác nhận',
|
title: "Xác nhận",
|
||||||
width: '500px',
|
width: "500px",
|
||||||
height: '100px',
|
height: "100px",
|
||||||
vbind: {
|
vbind: {
|
||||||
content: 'Bạn có đồng ý gửi thông báo hàng loạt không?',
|
content: "Bạn có đồng ý gửi thông báo hàng loạt không?",
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendEmails() {
|
async function sendEmails() {
|
||||||
isSending.value = true;
|
isSending.value = true;
|
||||||
$snackbar('Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...');
|
$snackbar("Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...");
|
||||||
|
|
||||||
const paymentScheduleData = await $getdata(
|
const paymentScheduleData = await $getdata("payment_schedule", undefined, {
|
||||||
'payment_schedule',
|
filter: filter.value,
|
||||||
undefined,
|
sort: "to_date",
|
||||||
{
|
values:
|
||||||
filter: filter.value,
|
"penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry",
|
||||||
sort: 'to_date',
|
});
|
||||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailTemplate = await $getdata(
|
const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
|
||||||
'emailtemplate',
|
|
||||||
{ id: activeDateFilter.value.emailTemplate },
|
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
let message = emailTemplate.content.content;
|
let message = emailTemplate.content.content;
|
||||||
|
|
||||||
contents.value = paymentScheduleData.map(paymentSchedule => {
|
contents.value = paymentScheduleData.map((paymentSchedule) => {
|
||||||
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, '');
|
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
|
||||||
const transfer = {
|
const transfer = {
|
||||||
bank: {
|
bank: {
|
||||||
code: 'MB',
|
code: "MB",
|
||||||
name: 'MB Bank',
|
name: "MB Bank",
|
||||||
},
|
},
|
||||||
account: {
|
account: {
|
||||||
number: '146768686868',
|
number: "146768686868",
|
||||||
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA',
|
name: "CONG TY CO PHAN BAT DONG SAN UTOPIA",
|
||||||
},
|
},
|
||||||
content: 'Thanh toán đơn #xyz',
|
content: "Thanh toán đơn #xyz",
|
||||||
};
|
};
|
||||||
|
|
||||||
transfer.content = buildContentPayment(paymentSchedule);
|
transfer.content = buildContentPayment(paymentSchedule);
|
||||||
@@ -242,7 +229,7 @@ async function sendEmails() {
|
|||||||
...emailTemplate.content,
|
...emailTemplate.content,
|
||||||
content: undefined,
|
content: undefined,
|
||||||
message: replaceTemplateVars(message, paymentSchedule),
|
message: replaceTemplateVars(message, paymentSchedule),
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await forEachAsync(contents.value, async (bigContent, i) => {
|
await forEachAsync(contents.value, async (bigContent, i) => {
|
||||||
@@ -251,16 +238,16 @@ async function sendEmails() {
|
|||||||
content: toRaw(bigContent),
|
content: toRaw(bigContent),
|
||||||
previewMode: true,
|
previewMode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
||||||
tempEm.content.message = quillToEmailHtml(message);
|
tempEm.content.message = quillToEmailHtml(message);
|
||||||
let emailHtml = await render(Template1, tempEm);
|
let emailHtml = await render(Template1, tempEm);
|
||||||
|
|
||||||
// If no image URL provided, remove image section from HTML
|
// If no image URL provided, remove image section from HTML
|
||||||
if ((imageUrl ?? '').trim() === '') {
|
if ((imageUrl ?? "").trim() === "") {
|
||||||
emailHtml = emailHtml
|
emailHtml = emailHtml
|
||||||
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '')
|
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
|
||||||
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
.replace(/\n\s*\n\s*\n/g, "\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace keywords in HTML
|
// Replace keywords in HTML
|
||||||
@@ -268,55 +255,65 @@ async function sendEmails() {
|
|||||||
if (keyword && keyword.length > 0) {
|
if (keyword && keyword.length > 0) {
|
||||||
keyword.forEach(({ keyword, value }) => {
|
keyword.forEach(({ keyword, value }) => {
|
||||||
if (keyword && value) {
|
if (keyword && value) {
|
||||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g');
|
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
|
||||||
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await $insertapi(
|
const response = await $insertapi(
|
||||||
'sendemail',
|
"sendemail",
|
||||||
{
|
{
|
||||||
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
|
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
|
||||||
content: finalEmailHtml,
|
content: finalEmailHtml,
|
||||||
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || 'Thông báo từ Utopia Villas & Resort',
|
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || "Thông báo từ Utopia Villas & Resort",
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response !== null) {
|
if (response !== null) {
|
||||||
await $insertapi('productnote', {
|
await $insertapi(
|
||||||
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
"productnote",
|
||||||
user: $store.login.id,
|
{
|
||||||
detail: `Đã gửi email thông báo quá hạn cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format('HH:mm ngày DD/MM/YYYY')}.`
|
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
||||||
}, undefined, false);
|
user: $store.login.id,
|
||||||
|
detail: `Đã gửi email thông báo quá hạn cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format("HH:mm ngày DD/MM/YYYY")}.`,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$snackbar('Thông báo đã được gửi thành công đến các khách hàng.');
|
$snackbar("Thông báo đã được gửi thành công đến các khách hàng.");
|
||||||
isSending.value = false;
|
isSending.value = false;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(filter, () => {
|
watch(
|
||||||
key.value += 1;
|
filter,
|
||||||
}, { deep: true })
|
() => {
|
||||||
|
key.value += 1;
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
|
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
|
||||||
<div class="buttons m-0">
|
<div class="buttons m-0">
|
||||||
<p>Quá hạn:</p>
|
<p>Quá hạn:</p>
|
||||||
<button
|
<button
|
||||||
v-for="payable in payables"
|
v-for="payable in payables"
|
||||||
:key="payable.id"
|
:key="payable.id"
|
||||||
@click="setDateFilter(payable.detail)"
|
@click="setDateFilter(payable.detail)"
|
||||||
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
|
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
|
||||||
>
|
>
|
||||||
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày
|
{{ payable.detail.lookup === "lte" ? "≤" : ">" }}
|
||||||
|
{{ payable.detail.time }} ngày
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="activeDateFilter"
|
v-if="activeDateFilter"
|
||||||
@click="resetDateFilter()"
|
@click="resetDateFilter()"
|
||||||
class="button is-white"
|
class="button is-white"
|
||||||
@@ -324,7 +321,7 @@ watch(filter, () => {
|
|||||||
Xoá lọc
|
Xoá lọc
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="activeDateFilter"
|
v-if="activeDateFilter"
|
||||||
@click="openConfirmModal()"
|
@click="openConfirmModal()"
|
||||||
:class="['button', 'is-light', { 'is-loading': isSending }]"
|
:class="['button', 'is-light', { 'is-loading': isSending }]"
|
||||||
@@ -342,16 +339,18 @@ watch(filter, () => {
|
|||||||
params: {
|
params: {
|
||||||
filter,
|
filter,
|
||||||
sort: 'to_date',
|
sort: 'to_date',
|
||||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
values:
|
||||||
}
|
'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||||
}" />
|
},
|
||||||
<Modal
|
}"
|
||||||
v-if="showmodal"
|
|
||||||
v-bind="showmodal"
|
|
||||||
@confirm="sendEmails()"
|
|
||||||
@close="showmodal = undefined"
|
|
||||||
/>
|
/>
|
||||||
<!-- <div class="is-flex is-gap-1">
|
<Modal
|
||||||
|
v-if="showmodal"
|
||||||
|
v-bind="showmodal"
|
||||||
|
@confirm="sendEmails()"
|
||||||
|
@close="showmodal = undefined"
|
||||||
|
/>
|
||||||
|
<!-- <div class="is-flex is-gap-1">
|
||||||
// debug
|
// debug
|
||||||
<Template1
|
<Template1
|
||||||
v-if="contents"
|
v-if="contents"
|
||||||
@@ -360,4 +359,4 @@ watch(filter, () => {
|
|||||||
previewMode
|
previewMode
|
||||||
/>
|
/>
|
||||||
</div> -->
|
</div> -->
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user