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 {
|
||||
max-width: 1900px !important;
|
||||
padding: 1rem 2rem 2rem;
|
||||
@include mobile { padding: 1rem; }
|
||||
|
||||
@include mobile {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.columns .column {
|
||||
@include mobile { padding-left: 0; padding-right: 0; }
|
||||
@include mobile {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
// might break lots of stuff
|
||||
// .skeleton-block:not(:last-child), .media:not(:last-child), .level:not(:last-child), .fixed-grid:not(:last-child), .grid:not(:last-child), .tabs:not(:last-child), .pagination:not(:last-child), .message:not(:last-child), .card:not(:last-child), .breadcrumb:not(:last-child), .field:not(:last-child), .file:not(:last-child), .title:not(:last-child), .subtitle:not(:last-child), .tags:not(:last-child), .table:not(:last-child), .table-container:not(:last-child), .progress:not(:last-child), .notification:not(:last-child), .content:not(:last-child), .buttons:not(:last-child), .box:not(:last-child), .block:not(:last-child) {
|
||||
// margin-bottom: inherit;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -2,61 +2,66 @@
|
||||
|
||||
// Font size loops
|
||||
@for $i from 10 through 50 {
|
||||
.fs-#{$i} { font-size: $i + px; }
|
||||
.fsb-#{$i} { font-size: $i + px; font-weight: bold; }
|
||||
.fs-#{$i} {
|
||||
font-size: $i + px;
|
||||
}
|
||||
.fsb-#{$i} {
|
||||
font-size: $i + px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.font-thin {
|
||||
.font-thin {
|
||||
font-weight: 100;
|
||||
}
|
||||
.font-extralight {
|
||||
.font-extralight {
|
||||
font-weight: 200;
|
||||
}
|
||||
.font-light {
|
||||
.font-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
.font-normal {
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
.font-medium {
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
.font-semibold {
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
.font-bold {
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-extrabold {
|
||||
.font-extrabold {
|
||||
font-weight: 800;
|
||||
}
|
||||
.font-black {
|
||||
.font-black {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.rounded-xs {
|
||||
border-radius: 0.125rem;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
.rounded-sm {
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.rounded-2xl {
|
||||
border-radius: 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.rounded-3xl {
|
||||
border-radius: 1.5rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
.rounded-4xl {
|
||||
border-radius: 2rem;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
.rounded-none {
|
||||
border-radius: 0;
|
||||
@@ -83,20 +88,20 @@
|
||||
|
||||
// ─── CSS custom properties ─────────────────────────────────────────────────
|
||||
:root {
|
||||
--spacing: 0.25rem;
|
||||
--container-3xs: 16rem;
|
||||
--container-2xs: 18rem;
|
||||
--container-xs: 20rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--container-lg: 32rem;
|
||||
--container-xl: 36rem;
|
||||
--container-2xl: 42rem;
|
||||
--container-3xl: 48rem;
|
||||
--container-4xl: 56rem;
|
||||
--container-5xl: 64rem;
|
||||
--container-6xl: 72rem;
|
||||
--container-7xl: 80rem;
|
||||
--spacing: 0.25rem;
|
||||
--container-3xs: 16rem;
|
||||
--container-2xs: 18rem;
|
||||
--container-xs: 20rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--container-lg: 32rem;
|
||||
--container-xl: 36rem;
|
||||
--container-2xl: 42rem;
|
||||
--container-3xl: 48rem;
|
||||
--container-4xl: 56rem;
|
||||
--container-5xl: 64rem;
|
||||
--container-6xl: 72rem;
|
||||
--container-7xl: 80rem;
|
||||
}
|
||||
|
||||
// ─── Shared mixin ──────────────────────────────────────────────────────────
|
||||
@@ -108,9 +113,16 @@
|
||||
|
||||
// ─── Class types ───────────────────────────────────────────────────────────
|
||||
$class-types: (
|
||||
"w": (width),
|
||||
"h": (height),
|
||||
"size": (width, height),
|
||||
"w": (
|
||||
width,
|
||||
),
|
||||
"h": (
|
||||
height,
|
||||
),
|
||||
"size": (
|
||||
width,
|
||||
height,
|
||||
),
|
||||
);
|
||||
|
||||
// ─── Numeric: w-0 → w-48, h-0 → h-48, size-0 → size-48 ───────────────────
|
||||
@@ -124,32 +136,110 @@ $class-types: (
|
||||
|
||||
// ─── Fractions ─────────────────────────────────────────────────────────────
|
||||
$fractions: (
|
||||
"1\\/2": (1, 2),
|
||||
"1\\/3": (1, 3),
|
||||
"2\\/3": (2, 3),
|
||||
"1\\/4": (1, 4),
|
||||
"2\\/4": (2, 4),
|
||||
"3\\/4": (3, 4),
|
||||
"1\\/5": (1, 5),
|
||||
"2\\/5": (2, 5),
|
||||
"3\\/5": (3, 5),
|
||||
"4\\/5": (4, 5),
|
||||
"1\\/6": (1, 6),
|
||||
"2\\/6": (2, 6),
|
||||
"3\\/6": (3, 6),
|
||||
"4\\/6": (4, 6),
|
||||
"5\\/6": (5, 6),
|
||||
"1\\/12": (1, 12),
|
||||
"2\\/12": (2, 12),
|
||||
"3\\/12": (3, 12),
|
||||
"4\\/12": (4, 12),
|
||||
"5\\/12": (5, 12),
|
||||
"6\\/12": (6, 12),
|
||||
"7\\/12": (7, 12),
|
||||
"8\\/12": (8, 12),
|
||||
"9\\/12": (9, 12),
|
||||
"10\\/12": (10, 12),
|
||||
"11\\/12": (11, 12),
|
||||
"1\\/2": (
|
||||
1,
|
||||
2,
|
||||
),
|
||||
"1\\/3": (
|
||||
1,
|
||||
3,
|
||||
),
|
||||
"2\\/3": (
|
||||
2,
|
||||
3,
|
||||
),
|
||||
"1\\/4": (
|
||||
1,
|
||||
4,
|
||||
),
|
||||
"2\\/4": (
|
||||
2,
|
||||
4,
|
||||
),
|
||||
"3\\/4": (
|
||||
3,
|
||||
4,
|
||||
),
|
||||
"1\\/5": (
|
||||
1,
|
||||
5,
|
||||
),
|
||||
"2\\/5": (
|
||||
2,
|
||||
5,
|
||||
),
|
||||
"3\\/5": (
|
||||
3,
|
||||
5,
|
||||
),
|
||||
"4\\/5": (
|
||||
4,
|
||||
5,
|
||||
),
|
||||
"1\\/6": (
|
||||
1,
|
||||
6,
|
||||
),
|
||||
"2\\/6": (
|
||||
2,
|
||||
6,
|
||||
),
|
||||
"3\\/6": (
|
||||
3,
|
||||
6,
|
||||
),
|
||||
"4\\/6": (
|
||||
4,
|
||||
6,
|
||||
),
|
||||
"5\\/6": (
|
||||
5,
|
||||
6,
|
||||
),
|
||||
"1\\/12": (
|
||||
1,
|
||||
12,
|
||||
),
|
||||
"2\\/12": (
|
||||
2,
|
||||
12,
|
||||
),
|
||||
"3\\/12": (
|
||||
3,
|
||||
12,
|
||||
),
|
||||
"4\\/12": (
|
||||
4,
|
||||
12,
|
||||
),
|
||||
"5\\/12": (
|
||||
5,
|
||||
12,
|
||||
),
|
||||
"6\\/12": (
|
||||
6,
|
||||
12,
|
||||
),
|
||||
"7\\/12": (
|
||||
7,
|
||||
12,
|
||||
),
|
||||
"8\\/12": (
|
||||
8,
|
||||
12,
|
||||
),
|
||||
"9\\/12": (
|
||||
9,
|
||||
12,
|
||||
),
|
||||
"10\\/12": (
|
||||
10,
|
||||
12,
|
||||
),
|
||||
"11\\/12": (
|
||||
11,
|
||||
12,
|
||||
),
|
||||
);
|
||||
|
||||
@each $prefix, $props in $class-types {
|
||||
@@ -163,13 +253,12 @@ $fractions: (
|
||||
}
|
||||
|
||||
// ─── Container sizes (w- only) ─────────────────────────────────────────────
|
||||
$containers: (
|
||||
"3xs", "2xs", "xs", "sm", "md", "lg", "xl",
|
||||
"2xl", "3xl", "4xl", "5xl", "6xl", "7xl"
|
||||
);
|
||||
$containers: ("3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl");
|
||||
|
||||
@each $name in $containers {
|
||||
.w-#{$name} { width: var(--container-#{$name}); }
|
||||
.w-#{$name} {
|
||||
width: var(--container-#{$name});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Shared keywords (auto, px, full, min, max, fit) ───────────────────────
|
||||
@@ -178,9 +267,9 @@ $shared-keywords: (
|
||||
"auto": auto,
|
||||
"px": 1px,
|
||||
"full": 100%,
|
||||
"min": min-content,
|
||||
"max": max-content,
|
||||
"fit": fit-content,
|
||||
"min": min-content,
|
||||
"max": max-content,
|
||||
"fit": fit-content,
|
||||
);
|
||||
|
||||
@each $prefix, $props in $class-types {
|
||||
@@ -210,5 +299,9 @@ $viewport-keywords: (
|
||||
}
|
||||
}
|
||||
|
||||
.w-screen { width: 100vw; }
|
||||
.h-screen { height: 100vh; }
|
||||
.w-screen {
|
||||
width: 100vw;
|
||||
}
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
<template>
|
||||
<span :class="`icon-text fsb-${props.size||17} ${props.type || 'has-text-primary'}`">
|
||||
<span :class="`icon-text fsb-${props.size || 17} ${props.type || 'has-text-primary'}`">
|
||||
<span>{{ title }}</span>
|
||||
<SvgIcon id="ignore" v-bind="{name: 'right.svg', type: props.type? props.type.replace('has-text-', '') : null,
|
||||
size: (props.size>=30? props.size*0.7 : props.size) || 20, alt: 'Mũi tên chỉ hướng'}"></SvgIcon>
|
||||
<SvgIcon
|
||||
id="ignore"
|
||||
v-bind="{
|
||||
name: 'right.svg',
|
||||
type: props.type ? props.type.replace('has-text-', '') : null,
|
||||
size: (props.size >= 30 ? props.size * 0.7 : props.size) || 20,
|
||||
alt: 'Mũi tên chỉ hướng',
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</template>
|
||||
<script setup>
|
||||
var props = defineProps({
|
||||
type: String,
|
||||
size: Number,
|
||||
title: String
|
||||
})
|
||||
</script>
|
||||
var props = defineProps({
|
||||
type: String,
|
||||
size: Number,
|
||||
title: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<Teleport to="#__nuxt > div">
|
||||
<div class="modal is-active" @click="doClick">
|
||||
<div
|
||||
class="modal is-active"
|
||||
@click="doClick"
|
||||
>
|
||||
<div
|
||||
class="modal-background"
|
||||
:style="`opacity:${count === 0 ? 0.7 : 0.3} !important;`"
|
||||
@@ -10,14 +13,23 @@
|
||||
:id="docid"
|
||||
:style="`width:${vWidth}; border-radius:16px;`"
|
||||
>
|
||||
<header class="modal-card-head my-0 py-2" v-if="title">
|
||||
<header
|
||||
class="modal-card-head my-0 py-2"
|
||||
v-if="title"
|
||||
>
|
||||
<div style="width: 100%">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded has-text-left">
|
||||
<p class="fsb-18 has-text-primary" v-html="title"></p>
|
||||
<p
|
||||
class="fsb-18 has-text-primary"
|
||||
v-html="title"
|
||||
></p>
|
||||
</div>
|
||||
<div class="control has-text-right">
|
||||
<button class="delete is-medium" @click="closeModal()"></button>
|
||||
<button
|
||||
class="delete is-medium"
|
||||
@click="closeModal()"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,14 +61,13 @@ const store = useStore();
|
||||
const { $id } = useNuxtApp();
|
||||
|
||||
const props = defineProps({
|
||||
component: String,
|
||||
component: String,
|
||||
width: String,
|
||||
height: String,
|
||||
vbind: Object,
|
||||
title: String,
|
||||
});
|
||||
|
||||
|
||||
const componentFiles = import.meta.glob("@/components/**/*.vue");
|
||||
|
||||
const resolvedComponent = shallowRef(null);
|
||||
@@ -68,10 +79,8 @@ function loadDynamicComponent() {
|
||||
}
|
||||
|
||||
const fullPath = `/components/${props.component}.vue`;
|
||||
|
||||
const componentPath = Object.keys(componentFiles).find((path) =>
|
||||
path.endsWith(fullPath)
|
||||
);
|
||||
|
||||
const componentPath = Object.keys(componentFiles).find((path) => path.endsWith(fullPath));
|
||||
|
||||
if (componentPath) {
|
||||
resolvedComponent.value = defineAsyncComponent(componentFiles[componentPath]);
|
||||
@@ -118,7 +127,7 @@ const doClick = function (e) {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.documentElement.classList.add('is-clipped');
|
||||
document.documentElement.classList.add("is-clipped");
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") closeModal();
|
||||
});
|
||||
@@ -128,6 +137,6 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
count--;
|
||||
if (count === 0) document.documentElement.classList.remove('is-clipped');
|
||||
})
|
||||
</script>
|
||||
if (count === 0) document.documentElement.classList.remove("is-clipped");
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,49 +1,110 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field has-addons" :id="docid">
|
||||
<div
|
||||
class="field has-addons"
|
||||
:id="docid"
|
||||
>
|
||||
<div class="control has-icons-left is-expanded">
|
||||
<div :class="`dropdown ${ pos || ''} ${focused? 'is-active' : ''}`" style="width: 100%;">
|
||||
<div class="dropdown-trigger" style="width: 100%;">
|
||||
<input
|
||||
:disabled="disabled"
|
||||
:class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-dark' : ''}`"
|
||||
type="text"
|
||||
@focus="setFocus"
|
||||
@blur="lostFocus"
|
||||
@keyup.enter="pressEnter"
|
||||
@keyup="beginSearch"
|
||||
<div
|
||||
:class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`"
|
||||
style="width: 100%"
|
||||
>
|
||||
<div
|
||||
class="dropdown-trigger"
|
||||
style="width: 100%"
|
||||
>
|
||||
<input
|
||||
:disabled="disabled"
|
||||
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-dark' : ''}`"
|
||||
type="text"
|
||||
@focus="setFocus"
|
||||
@blur="lostFocus"
|
||||
@keyup.enter="pressEnter"
|
||||
@keyup="beginSearch"
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown-menu" style="min-width: 100%" role="menu" @click="doClick()">
|
||||
<div class="dropdown-content px-3" style="min-width: 100%;">
|
||||
<p class="has-text-warning" v-if="data.length===0">{{ isVietnamese ? 'Không 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
|
||||
class="dropdown-menu"
|
||||
style="min-width: 100%"
|
||||
role="menu"
|
||||
@click="doClick()"
|
||||
>
|
||||
<div
|
||||
class="dropdown-content px-3"
|
||||
style="min-width: 100%"
|
||||
>
|
||||
<p
|
||||
class="has-text-warning"
|
||||
v-if="data.length === 0"
|
||||
>
|
||||
{{ isVietnamese ? "Không có giá trị thỏa mãn" : "No matching values" }}
|
||||
</p>
|
||||
<ScrollBox
|
||||
v-bind="{
|
||||
data: data,
|
||||
name: field,
|
||||
fontsize: 14,
|
||||
maxheight: '200px',
|
||||
notick: true,
|
||||
}"
|
||||
@selected="choose"
|
||||
v-else
|
||||
></ScrollBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="icon is-left">
|
||||
<SvgIcon v-bind="{name: 'magnify.svg', type: 'gray', size: 22}"></SvgIcon>
|
||||
<SvgIcon v-bind="{ name: 'magnify.svg', type: 'gray', size: 22 }"></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control" v-if="clearable && value">
|
||||
<button class="button is-primary px-2" @click="clearValue" style="height: 100%" type="button">
|
||||
<SvgIcon v-bind="{name: 'close.svg', type: 'white', size: 24}"></SvgIcon>
|
||||
<div
|
||||
class="control"
|
||||
v-if="clearable && value"
|
||||
>
|
||||
<button
|
||||
class="button is-primary px-2"
|
||||
@click="clearValue"
|
||||
style="height: 100%"
|
||||
type="button"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'close.svg', type: 'white', size: 24 }"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control" v-if="viewaddon">
|
||||
<button class="button is-dark px-2" @click="viewInfo()" style="height: 100%" type="button">
|
||||
<SvgIcon v-bind="{name: 'view.svg', type: 'white', size: 24}"></SvgIcon>
|
||||
<div
|
||||
class="control"
|
||||
v-if="viewaddon"
|
||||
>
|
||||
<button
|
||||
class="button is-dark px-2"
|
||||
@click="viewInfo()"
|
||||
style="height: 100%"
|
||||
type="button"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 24 }"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control" v-if="addon">
|
||||
<button class="button is-primary px-2" @click="addNew()" style="height: 100%" type="button">
|
||||
<SvgIcon v-bind="{name: 'add1.png', type: 'white', size: 24}"></SvgIcon>
|
||||
<div
|
||||
class="control"
|
||||
v-if="addon"
|
||||
>
|
||||
<button
|
||||
class="button is-primary px-2"
|
||||
@click="addNew()"
|
||||
style="height: 100%"
|
||||
type="button"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 24 }"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal @dataevent="dataevent" @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@dataevent="dataevent"
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -51,7 +112,22 @@
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
export default {
|
||||
props: ['api', 'field', 'column', 'first', 'optionid', 'filter', 'addon', 'viewaddon', 'position', 'disabled', 'vdata', 'clearable', 'placeholder', 'searchfield'],
|
||||
props: [
|
||||
"api",
|
||||
"field",
|
||||
"column",
|
||||
"first",
|
||||
"optionid",
|
||||
"filter",
|
||||
"addon",
|
||||
"viewaddon",
|
||||
"position",
|
||||
"disabled",
|
||||
"vdata",
|
||||
"clearable",
|
||||
"placeholder",
|
||||
"searchfield",
|
||||
],
|
||||
setup() {
|
||||
const store = useStore();
|
||||
return { store };
|
||||
@@ -64,7 +140,7 @@ export default {
|
||||
value: undefined,
|
||||
selected: undefined,
|
||||
showmodal: undefined,
|
||||
params: this.api? this.$findapi(this.api)['params'] : undefined,
|
||||
params: this.api ? this.$findapi(this.api)["params"] : undefined,
|
||||
orgdata: undefined,
|
||||
error: false,
|
||||
focused: false,
|
||||
@@ -72,142 +148,142 @@ export default {
|
||||
count2: 0,
|
||||
docid: this.$id(),
|
||||
pos: undefined,
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isVietnamese() {
|
||||
return this.store.lang === "vi";
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
this.getPos()
|
||||
if(this.vdata) {
|
||||
this.orgdata = this.$copy(this.vdata)
|
||||
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field]))
|
||||
this.getPos();
|
||||
if (this.vdata) {
|
||||
this.orgdata = this.$copy(this.vdata);
|
||||
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
|
||||
}
|
||||
if(this.first) {
|
||||
this.data = this.orgdata? this.$copy(this.orgdata) : await this.getData()
|
||||
if(this.optionid) {
|
||||
let f = {}
|
||||
f[this.field] = this.optionid
|
||||
if(this.optionid>0) f = {id: this.optionid}
|
||||
this.selected = this.$find(this.data, f)
|
||||
if(this.selected && this.vdata) {
|
||||
return this.value = this.selected[this.field]
|
||||
if (this.first) {
|
||||
this.data = this.orgdata ? this.$copy(this.orgdata) : await this.getData();
|
||||
if (this.optionid) {
|
||||
let f = {};
|
||||
f[this.field] = this.optionid;
|
||||
if (this.optionid > 0) f = { id: this.optionid };
|
||||
this.selected = this.$find(this.data, f);
|
||||
if (this.selected && this.vdata) {
|
||||
return (this.value = this.selected[this.field]);
|
||||
}
|
||||
}
|
||||
} else if(this.optionid) {
|
||||
this.selected = await this.$getdata(this.api, {id: this.optionid}, undefined, true)
|
||||
} else if (this.optionid) {
|
||||
this.selected = await this.$getdata(this.api, { id: this.optionid }, undefined, true);
|
||||
}
|
||||
if(this.selected) this.doSelect(this.selected)
|
||||
if (this.selected) this.doSelect(this.selected);
|
||||
},
|
||||
watch: {
|
||||
optionid: function(newVal) {
|
||||
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid})
|
||||
if(this.selected) this.doSelect(this.selected)
|
||||
else this.value = undefined
|
||||
optionid: function (newVal) {
|
||||
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
|
||||
if (this.selected) this.doSelect(this.selected);
|
||||
else this.value = undefined;
|
||||
},
|
||||
filter: async function(newVal) {
|
||||
this.data = await this.getData()
|
||||
filter: async function (newVal) {
|
||||
this.data = await this.getData();
|
||||
},
|
||||
vdata: function(newval) {
|
||||
if(newval) {
|
||||
this.orgdata = this.$copy(this.vdata)
|
||||
this.orgdata.map(v=>v.search = this.$nonAccent(v[this.field]))
|
||||
this.data = this.$copy(this.orgdata)
|
||||
this.selected = undefined
|
||||
this.value = undefined
|
||||
if(this.optionid) this.selected = this.$find(this.data, {id: this.optionid})
|
||||
if(this.selected) this.doSelect(this.selected)
|
||||
vdata: function (newval) {
|
||||
if (newval) {
|
||||
this.orgdata = this.$copy(this.vdata);
|
||||
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
|
||||
this.data = this.$copy(this.orgdata);
|
||||
this.selected = undefined;
|
||||
this.value = undefined;
|
||||
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
|
||||
if (this.selected) this.doSelect(this.selected);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
choose(v) {
|
||||
this.focused = false
|
||||
this.count1 = 0
|
||||
this.count2 = 0
|
||||
this.doSelect(v)
|
||||
this.focused = false;
|
||||
this.count1 = 0;
|
||||
this.count2 = 0;
|
||||
this.doSelect(v);
|
||||
},
|
||||
setFocus() {
|
||||
this.focused = true
|
||||
this.count1 = 0
|
||||
this.count2 = 0
|
||||
this.focused = true;
|
||||
this.count1 = 0;
|
||||
this.count2 = 0;
|
||||
},
|
||||
lostFocus() {
|
||||
let self = this
|
||||
setTimeout(()=>{
|
||||
if(self.focused && self.count1===0) self.focused = false
|
||||
}, 200)
|
||||
let self = this;
|
||||
setTimeout(() => {
|
||||
if (self.focused && self.count1 === 0) self.focused = false;
|
||||
}, 200);
|
||||
},
|
||||
pressEnter() {
|
||||
if(this.data.length===0) return
|
||||
this.choose(this.data[0])
|
||||
if (this.data.length === 0) return;
|
||||
this.choose(this.data[0]);
|
||||
},
|
||||
doClick() {
|
||||
this.count1 += 1
|
||||
this.count1 += 1;
|
||||
},
|
||||
doSelect(option) {
|
||||
if(this.$empty(option)) return
|
||||
this.$emit('option', option)
|
||||
this.$emit('modalevent', {name: 'option', data: option})
|
||||
this.selected = option
|
||||
this.value = this.selected[this.field]
|
||||
if (this.$empty(option)) return;
|
||||
this.$emit("option", option);
|
||||
this.$emit("modalevent", { name: "option", data: option });
|
||||
this.selected = option;
|
||||
this.value = this.selected[this.field];
|
||||
},
|
||||
clearValue() {
|
||||
this.value = undefined
|
||||
this.selected = undefined
|
||||
this.$emit('option', null)
|
||||
this.$emit('modalevent', {name: 'option', data: null})
|
||||
this.value = undefined;
|
||||
this.selected = undefined;
|
||||
this.$emit("option", null);
|
||||
this.$emit("modalevent", { name: "option", data: null });
|
||||
},
|
||||
findObject(val) {
|
||||
let rows = this.$copy(this.orgdata)
|
||||
if(this.$empty(val)) this.data = rows
|
||||
let rows = this.$copy(this.orgdata);
|
||||
if (this.$empty(val)) this.data = rows;
|
||||
else {
|
||||
let text = this.$nonAccent(val)
|
||||
this.data = rows.filter(v=>v.search.toLowerCase().indexOf(text.toLowerCase())>=0)
|
||||
let text = this.$nonAccent(val);
|
||||
this.data = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
|
||||
}
|
||||
},
|
||||
async getData() {
|
||||
this.params.filter = this.filter
|
||||
let data = await this.$getdata(this.api, undefined, this.params)
|
||||
return data
|
||||
this.params.filter = this.filter;
|
||||
let data = await this.$getdata(this.api, undefined, this.params);
|
||||
return data;
|
||||
},
|
||||
async getApi(val) {
|
||||
if(this.vdata) return this.findObject(val)
|
||||
let text = val? val.toLowerCase() : ''
|
||||
let f = {}
|
||||
|
||||
if (this.vdata) return this.findObject(val);
|
||||
let text = val ? val.toLowerCase() : "";
|
||||
let f = {};
|
||||
|
||||
// Sử dụng searchfield nếu có, nếu không thì dùng column
|
||||
const fieldsToSearch = this.searchfield || this.column;
|
||||
|
||||
fieldsToSearch.map(v=>{
|
||||
f[`${v}__icontains`] = text
|
||||
})
|
||||
this.params.filter_or = f
|
||||
if(this.filter) this.params.filter = this.$copy(this.filter)
|
||||
let arr = await this.$getdata(this.api, undefined, this.params)
|
||||
this.data = this.$copy(arr)
|
||||
|
||||
fieldsToSearch.map((v) => {
|
||||
f[`${v}__icontains`] = text;
|
||||
});
|
||||
this.params.filter_or = f;
|
||||
if (this.filter) this.params.filter = this.$copy(this.filter);
|
||||
let arr = await this.$getdata(this.api, undefined, this.params);
|
||||
this.data = this.$copy(arr);
|
||||
},
|
||||
beginSearch(e) {
|
||||
let val = e.target.value
|
||||
this.search = val
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.getApi(val), 150)
|
||||
let val = e.target.value;
|
||||
this.search = val;
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => this.getApi(val), 150);
|
||||
},
|
||||
addNew() {
|
||||
this.showmodal = this.$copy(this.addon)
|
||||
this.showmodal = this.$copy(this.addon);
|
||||
},
|
||||
dataevent(v) {
|
||||
console.log("SearchBox received dataevent:", v); // Debug log
|
||||
|
||||
|
||||
if (!v || !v.id) {
|
||||
console.error("Invalid data received in SearchBox:", v);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Tìm và cập nhật trong danh sách
|
||||
let idx = this.$findIndex(this.data, {id: v.id})
|
||||
let idx = this.$findIndex(this.data, { id: v.id });
|
||||
if (idx < 0) {
|
||||
// Nếu chưa có trong danh sách, thêm vào đầu
|
||||
this.data.unshift(v);
|
||||
@@ -217,10 +293,10 @@ export default {
|
||||
this.data[idx] = v;
|
||||
console.log("Updated existing item in data:", v);
|
||||
}
|
||||
|
||||
|
||||
// Cập nhật orgdata nếu có
|
||||
if (this.orgdata) {
|
||||
let orgIdx = this.$findIndex(this.orgdata, {id: v.id});
|
||||
let orgIdx = this.$findIndex(this.orgdata, { id: v.id });
|
||||
if (orgIdx < 0) {
|
||||
this.orgdata.unshift(v);
|
||||
// Thêm search field cho orgdata
|
||||
@@ -234,42 +310,46 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// **Tự động select item vừa tạo/cập nhật**
|
||||
this.doSelect(v);
|
||||
|
||||
|
||||
// Đóng modal
|
||||
this.showmodal = undefined;
|
||||
|
||||
|
||||
console.log("SearchBox data after update:", this.data);
|
||||
},
|
||||
viewInfo() {
|
||||
if(!this.selected) return this.$dialog(this.isVietnamese ? 'Vui lòng lựa chọn trước khi xem thông tin.' : 'Please select before viewing', this.isVietnamese ? 'Thông báo' : 'Notice')
|
||||
let copy = this.$copy(this.viewaddon)
|
||||
copy.vbind = {row: this.selected}
|
||||
this.showmodal = copy
|
||||
if (!this.selected)
|
||||
return this.$dialog(
|
||||
this.isVietnamese ? "Vui lòng lựa chọn trước khi xem thông tin." : "Please select before viewing",
|
||||
this.isVietnamese ? "Thông báo" : "Notice",
|
||||
);
|
||||
let copy = this.$copy(this.viewaddon);
|
||||
copy.vbind = { row: this.selected };
|
||||
this.showmodal = copy;
|
||||
},
|
||||
getPos() {
|
||||
switch(this.position) {
|
||||
case 'is-top-left':
|
||||
this.pos = 'is-up is-left'
|
||||
switch (this.position) {
|
||||
case "is-top-left":
|
||||
this.pos = "is-up is-left";
|
||||
break;
|
||||
case 'is-top-right':
|
||||
this.pos = 'is-up is-right'
|
||||
case "is-top-right":
|
||||
this.pos = "is-up is-right";
|
||||
break;
|
||||
case 'is-bottom-left':
|
||||
this.pos = 'is-right'
|
||||
case "is-bottom-left":
|
||||
this.pos = "is-right";
|
||||
break;
|
||||
case 'is-bottom-right':
|
||||
this.pos = 'is-right'
|
||||
case "is-bottom-right":
|
||||
this.pos = "is-right";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.field:not(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,6 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
|
||||
/* primary: $blue-dianne (#204853) */
|
||||
.svg-primary {
|
||||
filter: invert(19%) sepia(18%) saturate(1514%) hue-rotate(151deg) brightness(97%) contrast(85%);
|
||||
@@ -31,7 +30,7 @@ export default {
|
||||
|
||||
/* findata/info/warning: $sirocco (#758385) */
|
||||
/* Cả ba đều dùng chung bộ lọc này */
|
||||
.svg-findata,
|
||||
.svg-findata,
|
||||
.svg-info,
|
||||
.svg-warning {
|
||||
filter: invert(56%) sepia(10%) saturate(301%) hue-rotate(167deg) brightness(92%) contrast(82%);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<span class="tooltip">
|
||||
<span v-html="props.html"></span>
|
||||
<span class="tooltiptext" v-html="props.tooltip"></span>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
v-html="props.tooltip"
|
||||
></span>
|
||||
</span>
|
||||
</template>
|
||||
<script setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
var props = defineProps({
|
||||
html: String,
|
||||
tooltip: String,
|
||||
width: Number
|
||||
})
|
||||
</script>
|
||||
import { defineAsyncComponent } from "vue";
|
||||
var props = defineProps({
|
||||
html: String,
|
||||
tooltip: String,
|
||||
width: Number,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
<template>
|
||||
<nav class="navbar has-shadow sticky px-3" style="top: 0" role="navigation">
|
||||
<nav
|
||||
class="navbar has-shadow sticky px-3"
|
||||
style="top: 0"
|
||||
role="navigation"
|
||||
>
|
||||
<div class="navbar-brand mr-5">
|
||||
<span class="navbar-item is-gap-1">
|
||||
<div style="width: 16px; height: 16px" class="has-background-primary rounded-full"></div>
|
||||
<span class="fs-17 font-semibold has-text-primary">{{ $dayjs().format('DD/MM') }}</span>
|
||||
</span>
|
||||
<div class="size-4 has-background-primary rounded-full"></div>
|
||||
<span class="fs-17 font-semibold has-text-primary">{{ $dayjs().format("DD/MM") }}</span>
|
||||
</span>
|
||||
<a
|
||||
class="navbar-item p-0 has-text-primary"
|
||||
@click="changeTab(leftmenu[0])"
|
||||
>
|
||||
<svg style="max-height: none; width: 44px" width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M40.5 6C59.0015 6 74 20.9985 74 39.5C74 58.0015 59.0015 73 40.5 73C21.9985 73 7 58.0015 7 39.5C7 20.9985 21.9985 6 40.5 6ZM17.9834 29.0654V48.2373H30.9395V44.8955H22.0371V40.3174H30.2373V36.9756H22.0371V32.4072H30.9014V29.0654H17.9834ZM33.8604 48.2373H37.9141V41.4404H40.873L44.5039 48.2373H48.9785L44.9092 40.7852C44.9677 40.7598 45.0277 40.7378 45.085 40.7109C46.1269 40.2242 46.9225 39.5252 47.4717 38.6143C48.0209 37.6969 48.2959 36.6012 48.2959 35.3281C48.2959 34.0613 48.0244 32.9595 47.4814 32.0234C46.9448 31.0812 46.1613 30.3545 45.1318 29.8428C44.1085 29.3248 42.8725 29.0655 41.4248 29.0654H33.8604V48.2373ZM50.8965 48.2373H54.9492V42.0215H58.3574C59.83 42.0214 61.0843 41.7499 62.1201 41.207C63.1623 40.6641 63.9577 39.9052 64.5068 38.9316C65.0559 37.9582 65.331 36.8354 65.3311 35.5625C65.3311 34.2895 65.0595 33.1659 64.5166 32.1924C63.9799 31.2127 63.2001 30.4476 62.1768 29.8984C61.1533 29.343 59.914 29.0654 58.46 29.0654H50.8965V48.2373ZM57.6826 32.3789C58.4689 32.3789 59.1182 32.5139 59.6299 32.7822C60.1416 33.0443 60.5228 33.4151 60.7725 33.8955C61.0283 34.3698 61.1562 34.9259 61.1562 35.5625C61.1562 36.1925 61.0281 36.7507 60.7725 37.2373C60.5228 37.7178 60.1416 38.0955 59.6299 38.3701C59.1245 38.6384 58.482 38.7734 57.7021 38.7734H54.9492V32.3789H57.6826ZM40.6475 32.3789C41.4274 32.3789 42.0733 32.4948 42.585 32.7256C43.1028 32.9502 43.4867 33.2811 43.7363 33.7178C43.9922 34.1546 44.1201 34.6916 44.1201 35.3281C44.1201 35.9584 43.9922 36.4858 43.7363 36.9102C43.4867 37.3344 43.1064 37.6531 42.5947 37.8652C42.083 38.0774 41.4399 38.1836 40.666 38.1836H37.9141V32.3789H40.6475Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<svg
|
||||
style="max-height: none; width: 44px"
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M40.5 6C59.0015 6 74 20.9985 74 39.5C74 58.0015 59.0015 73 40.5 73C21.9985 73 7 58.0015 7 39.5C7 20.9985 21.9985 6 40.5 6ZM17.9834 29.0654V48.2373H30.9395V44.8955H22.0371V40.3174H30.2373V36.9756H22.0371V32.4072H30.9014V29.0654H17.9834ZM33.8604 48.2373H37.9141V41.4404H40.873L44.5039 48.2373H48.9785L44.9092 40.7852C44.9677 40.7598 45.0277 40.7378 45.085 40.7109C46.1269 40.2242 46.9225 39.5252 47.4717 38.6143C48.0209 37.6969 48.2959 36.6012 48.2959 35.3281C48.2959 34.0613 48.0244 32.9595 47.4814 32.0234C46.9448 31.0812 46.1613 30.3545 45.1318 29.8428C44.1085 29.3248 42.8725 29.0655 41.4248 29.0654H33.8604V48.2373ZM50.8965 48.2373H54.9492V42.0215H58.3574C59.83 42.0214 61.0843 41.7499 62.1201 41.207C63.1623 40.6641 63.9577 39.9052 64.5068 38.9316C65.0559 37.9582 65.331 36.8354 65.3311 35.5625C65.3311 34.2895 65.0595 33.1659 64.5166 32.1924C63.9799 31.2127 63.2001 30.4476 62.1768 29.8984C61.1533 29.343 59.914 29.0654 58.46 29.0654H50.8965V48.2373ZM57.6826 32.3789C58.4689 32.3789 59.1182 32.5139 59.6299 32.7822C60.1416 33.0443 60.5228 33.4151 60.7725 33.8955C61.0283 34.3698 61.1562 34.9259 61.1562 35.5625C61.1562 36.1925 61.0281 36.7507 60.7725 37.2373C60.5228 37.7178 60.1416 38.0955 59.6299 38.3701C59.1245 38.6384 58.482 38.7734 57.7021 38.7734H54.9492V32.3789H57.6826ZM40.6475 32.3789C41.4274 32.3789 42.0733 32.4948 42.585 32.7256C43.1028 32.9502 43.4867 33.2811 43.7363 33.7178C43.9922 34.1546 44.1201 34.6916 44.1201 35.3281C44.1201 35.9584 43.9922 36.4858 43.7363 36.9102C43.4867 37.3344 43.1064 37.6531 42.5947 37.8652C42.083 38.0774 41.4399 38.1836 40.666 38.1836H37.9141V32.3789H40.6475Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
role="button"
|
||||
@@ -28,30 +42,45 @@
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu" id="navMenu">
|
||||
<div class="navbar-start is-gap-1 is-align-items-center" >
|
||||
<template v-for="(v, i) in leftmenu" :key="i" :id="v.code">
|
||||
<a class="navbar-item rounded-lg is-clipped p-0" v-if="!v.submenu" @click="changeTab(v)">
|
||||
<span :class="[
|
||||
'px-2 py-2 font-medium',
|
||||
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
|
||||
<div
|
||||
class="navbar-menu"
|
||||
id="navMenu"
|
||||
>
|
||||
<div class="navbar-start is-gap-1 is-align-items-center">
|
||||
<template
|
||||
v-for="(v, i) in leftmenu"
|
||||
:key="i"
|
||||
:id="v.code"
|
||||
>
|
||||
<a
|
||||
v-if="!v.submenu"
|
||||
:class="[
|
||||
'navbar-item rounded-lg is-clipped font-medium',
|
||||
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30',
|
||||
]"
|
||||
style="font-size: 13.5px"
|
||||
@click="changeTab(v)"
|
||||
>
|
||||
{{ v[lang] }}
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
class="navbar-item rounded-lg has-dropdown is-hoverable"
|
||||
>
|
||||
<a
|
||||
:class="[
|
||||
'navbar-link rounded-lg is-arrowless font-medium',
|
||||
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30',
|
||||
]"
|
||||
@click="changeTab(v)"
|
||||
style="font-size: 13.5px"
|
||||
>
|
||||
{{ v[lang] }}
|
||||
</span>
|
||||
</a>
|
||||
<div class="navbar-item rounded-lg has-dropdown is-hoverable" v-else>
|
||||
<a class="navbar-link rounded-lg p-0" @click="changeTab(v)">
|
||||
<p
|
||||
:class="[
|
||||
'px-2 py-2 rounded-lg font-medium is-flex is-align-items-center',
|
||||
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
|
||||
]"
|
||||
style="font-size: 13.5px"
|
||||
>
|
||||
<p class="is-flex is-align-items-center">
|
||||
<span>{{ v[lang] }}</span>
|
||||
<Icon name="material-symbols:keyboard-arrow-down-rounded" :size="20" />
|
||||
<Icon
|
||||
name="material-symbols:keyboard-arrow-down-rounded"
|
||||
:size="20"
|
||||
/>
|
||||
</p>
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
@@ -76,19 +105,23 @@
|
||||
</a>
|
||||
</div> -->
|
||||
<div class="navbar-end">
|
||||
<a class="navbar-item">
|
||||
<a class="navbar-item is-flex is-gap-2 is-justify-content-space-between is-align-items-center">
|
||||
<div>
|
||||
<p class="fs-13">Xin chào,</p>
|
||||
<p class="fs-14 font-bold">Quản lý</p>
|
||||
</div>
|
||||
<Avatarbox text="Q" type="findata" size="two" />
|
||||
<Avatarbox
|
||||
text="Q"
|
||||
type="findata"
|
||||
size="two"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
<script setup>
|
||||
import Avatarbox from '@/components/common/Avatarbox.vue';
|
||||
import Avatarbox from "@/components/common/Avatarbox.vue";
|
||||
import { watch } from "vue";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -99,229 +132,229 @@ const lang = ref($store.lang);
|
||||
const menu = [
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'dashboard',
|
||||
vi: 'Dashboard',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "dashboard",
|
||||
vi: "Dashboard",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Dashboard',
|
||||
component: 'DashboardMaster',
|
||||
base: "Dashboard",
|
||||
component: "DashboardMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'orders',
|
||||
vi: 'Đơn hàng',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "orders",
|
||||
vi: "Đơn hàng",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Orders',
|
||||
component: 'OrdersMaster',
|
||||
base: "Orders",
|
||||
component: "OrdersMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'inventory',
|
||||
vi: 'Tồn kho',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "inventory",
|
||||
vi: "Tồn kho",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Inventory',
|
||||
component: 'InventoryMaster',
|
||||
base: "Inventory",
|
||||
component: "InventoryMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'rights',
|
||||
vi: 'Phân quyền',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "rights",
|
||||
vi: "Phân quyền",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Rights',
|
||||
component: 'RightsMaster',
|
||||
base: "Rights",
|
||||
component: "RightsMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'POS',
|
||||
vi: 'POS',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "POS",
|
||||
vi: "POS",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'POS',
|
||||
component: 'POSMaster',
|
||||
base: "POS",
|
||||
component: "POSMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'receipts',
|
||||
vi: 'Hoá đơn',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "receipts",
|
||||
vi: "Hoá đơn",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Receipts',
|
||||
component: 'ReceiptsMaster',
|
||||
base: "Receipts",
|
||||
component: "ReceiptsMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'imports',
|
||||
vi: 'Nhập hàng',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "imports",
|
||||
vi: "Nhập hàng",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Imports',
|
||||
component: 'ImportsMaster',
|
||||
base: "Imports",
|
||||
component: "ImportsMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'exports',
|
||||
vi: 'Xuất hàng',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "exports",
|
||||
vi: "Xuất hàng",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Exports',
|
||||
component: 'ExportsMaster',
|
||||
base: "Exports",
|
||||
component: "ExportsMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'inventory-transfer',
|
||||
vi: 'Chuyển kho',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "inventory-transfer",
|
||||
vi: "Chuyển kho",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'InventoryTransfer',
|
||||
component: 'InventoryTransferMaster',
|
||||
base: "InventoryTransfer",
|
||||
component: "InventoryTransferMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'inventory-count',
|
||||
vi: 'Kiểm kho',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "inventory-count",
|
||||
vi: "Kiểm kho",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'InventoryCount',
|
||||
component: 'InventoryCountMaster',
|
||||
base: "InventoryCount",
|
||||
component: "InventoryCountMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'cash-book',
|
||||
vi: 'Sổ quỹ',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "cash-book",
|
||||
vi: "Sổ quỹ",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'CashBook',
|
||||
component: 'CashBookMaster',
|
||||
base: "CashBook",
|
||||
component: "CashBookMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'topmenu',
|
||||
classify: 'left',
|
||||
code: 'report',
|
||||
vi: 'Báo cáo',
|
||||
category: "topmenu",
|
||||
classify: "left",
|
||||
code: "report",
|
||||
vi: "Báo cáo",
|
||||
link: null,
|
||||
submenu: [
|
||||
{
|
||||
id: 1,
|
||||
category: 'submenu',
|
||||
classify: 'report',
|
||||
code: 'ncc',
|
||||
vi: 'NCC',
|
||||
category: "submenu",
|
||||
classify: "report",
|
||||
code: "ncc",
|
||||
vi: "NCC",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'NCC',
|
||||
component: 'NCCMaster',
|
||||
base: "NCC",
|
||||
component: "NCCMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'submenu',
|
||||
classify: 'report',
|
||||
code: 'customers',
|
||||
vi: 'Khách hàng',
|
||||
category: "submenu",
|
||||
classify: "report",
|
||||
code: "customers",
|
||||
vi: "Khách hàng",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Customers',
|
||||
component: 'CustomersMaster',
|
||||
base: "Customers",
|
||||
component: "CustomersMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'submenu',
|
||||
classify: 'report',
|
||||
code: 'goods',
|
||||
vi: 'Hàng hoá',
|
||||
category: "submenu",
|
||||
classify: "report",
|
||||
code: "goods",
|
||||
vi: "Hàng hoá",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Goods',
|
||||
component: 'GoodsMaster',
|
||||
base: "Goods",
|
||||
component: "GoodsMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'submenu',
|
||||
classify: 'report',
|
||||
code: 'report-cash-book',
|
||||
vi: 'Sổ quỹ',
|
||||
category: "submenu",
|
||||
classify: "report",
|
||||
code: "report-cash-book",
|
||||
vi: "Sổ quỹ",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'ReportCashBook',
|
||||
component: 'ReportCashBookMaster',
|
||||
base: "ReportCashBook",
|
||||
component: "ReportCashBookMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
category: 'submenu',
|
||||
classify: 'report',
|
||||
code: 'finance',
|
||||
vi: 'Tài chính',
|
||||
category: "submenu",
|
||||
classify: "report",
|
||||
code: "finance",
|
||||
vi: "Tài chính",
|
||||
link: null,
|
||||
detail: {
|
||||
base: 'Finance',
|
||||
component: 'FinanceMaster',
|
||||
base: "Finance",
|
||||
component: "FinanceMaster",
|
||||
},
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
},
|
||||
]
|
||||
];
|
||||
// if($store.rights.length>0) {
|
||||
// menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
|
||||
// }
|
||||
if(menu.length===0) {
|
||||
$snackbar($store.lang==='vi'? 'Bạn không có quyền truy cập' : 'You do not have permission to access.')
|
||||
if (menu.length === 0) {
|
||||
$snackbar($store.lang === "vi" ? "Bạn không có quyền truy cập" : "You do not have permission to access.");
|
||||
}
|
||||
// menu.map(v=>{
|
||||
// let arr = $filter($store.common, {category: 'submenu', classify: v.code})
|
||||
@@ -330,8 +363,8 @@ if(menu.length===0) {
|
||||
// }
|
||||
// v.submenu = arr.length>0? arr : null
|
||||
// })
|
||||
var leftmenu = $filter(menu, {category: 'topmenu', classify: 'left'})
|
||||
var currentTab = ref(leftmenu.length>0? leftmenu[0] : undefined)
|
||||
var leftmenu = $filter(menu, { category: "topmenu", classify: "left" });
|
||||
var currentTab = ref(leftmenu.length > 0 ? leftmenu[0] : undefined);
|
||||
var subTab = ref();
|
||||
var tabConfig = $find(menu, { code: "configuration" });
|
||||
var avatar = ref();
|
||||
@@ -364,11 +397,16 @@ function changeTab(tab, subtab) {
|
||||
router.push({ query: query });
|
||||
}
|
||||
function openProfile() {
|
||||
let modal = { component: "user/Profile", width: "1100px", height: "360px", title: $store.lang==='vi'? 'Thông tin cá nhân' : '"User profile"' };
|
||||
let modal = {
|
||||
component: "user/Profile",
|
||||
width: "1100px",
|
||||
height: "360px",
|
||||
title: $store.lang === "vi" ? "Thông tin cá nhân" : '"User profile"',
|
||||
};
|
||||
$store.commit("showmodal", modal);
|
||||
}
|
||||
let found = route.query.tab? $find(menu, {code: route.query.tab}) : undefined
|
||||
if(found || currentTab.value) changeTab(found || currentTab.value)
|
||||
let found = route.query.tab ? $find(menu, { code: route.query.tab }) : undefined;
|
||||
if (found || currentTab.value) changeTab(found || currentTab.value);
|
||||
onMounted(() => {
|
||||
if (!$store.login) return;
|
||||
avatar.value = {
|
||||
@@ -391,7 +429,7 @@ watch(
|
||||
};
|
||||
isAdmin.value = $store.login.type__code === "admin";
|
||||
lang.value = $store.lang;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style scoped>
|
||||
@@ -413,20 +451,30 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
.navbar-brand {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.navbar-item,
|
||||
a.navbar-link {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.navbar-dropdown {
|
||||
box-shadow: 0 0.2em 0 hsla(var(--bulma-scheme-h), var(--bulma-scheme-s), var(--bulma-scheme-invert-l), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-item:hover {
|
||||
.navbar-link {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
> p {
|
||||
|
||||
> p {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.navbar-item > .navbar-link:after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,48 +1,51 @@
|
||||
<template>
|
||||
<div v-if="record">
|
||||
<div class="columns is-multiline mx-0 mt-1" id="printable">
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('code') }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.code}` }}
|
||||
<div v-if="record">
|
||||
<div
|
||||
class="columns is-multiline mx-0 mt-1"
|
||||
id="printable"
|
||||
>
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("code") }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.code}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-7">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('account-type') }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.type__code} / ${record.type__name}` }}
|
||||
<div class="column is-7">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("account-type") }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.type__code} / ${record.type__name}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('currency') }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.currency__code} / ${record.currency__name}` }}
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("currency") }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.currency__code} / ${record.currency__name}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-7">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('balance') }}:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.balance) }}
|
||||
</div>
|
||||
<!--<p class="help is-findata">{{$vnmoney($formatNumber(record.balance))}}</p>-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('open-date') }}:</label>
|
||||
<div class="control">
|
||||
{{ `${$dayjs(record.create_time).format('DD/MM/YYYY')}` }}
|
||||
<div class="column is-7">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("balance") }}:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.balance) }}
|
||||
</div>
|
||||
<!--<p class="help is-findata">{{$vnmoney($formatNumber(record.balance))}}</p>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="column is-7">
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("open-date") }}:</label>
|
||||
<div class="control">
|
||||
{{ `${$dayjs(record.create_time).format("DD/MM/YYYY")}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="column is-7">
|
||||
<div class="field">
|
||||
<label class="label">Chi nhánh:</label>
|
||||
<div class="control">
|
||||
@@ -50,30 +53,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="border-bottom"></div>
|
||||
<div
|
||||
class="mt-5"
|
||||
id="ignore"
|
||||
>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="$exportpdf('printable', record.code)"
|
||||
>
|
||||
{{ $lang("print") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-bottom"></div>
|
||||
<div class="mt-5" id="ignore">
|
||||
<button class="button is-primary has-text-white" @click="$exportpdf('printable', record.code)">{{$lang('print')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['row'],
|
||||
data() {
|
||||
return {
|
||||
errors: {},
|
||||
record: undefined
|
||||
}
|
||||
export default {
|
||||
props: ["row"],
|
||||
data() {
|
||||
return {
|
||||
errors: {},
|
||||
record: undefined,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.record = await this.$getdata("internalaccount", { id: this.row.account || this.row.id }, undefined, true);
|
||||
},
|
||||
methods: {
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj;
|
||||
if (attr === "_type") this.category = obj.category__code;
|
||||
},
|
||||
async created() {
|
||||
this.record = await this.$getdata('internalaccount', {id: this.row.account || this.row.id}, undefined, true)
|
||||
},
|
||||
methods: {
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj
|
||||
if(attr==='_type') this.category = obj.category__code
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,50 +1,60 @@
|
||||
<!-- components/dialog/ConfirmDeleteEntry.vue -->
|
||||
<template>
|
||||
<div class="has-text-centered">
|
||||
|
||||
<div class=" mb-3 p-3">
|
||||
<p class="is-size-5 has-text-weight-semibold mb-4">
|
||||
Bạn có chắc chắn muốn xóa bút toán này?
|
||||
</p>
|
||||
|
||||
<p class="mt-3 has-text-danger has-text-weight-semibold">
|
||||
Hành động này <strong>không thể hoàn tác</strong>.<br>
|
||||
<div class="mb-3 p-3">
|
||||
<p class="is-size-5 has-text-weight-semibold mb-4">Bạn có chắc chắn muốn xóa bút toán này?</p>
|
||||
<p class="mt-3 has-text-danger has-text-weight-semibold">
|
||||
Hành động này <strong>không thể hoàn tác</strong>.<br />
|
||||
Dữ liệu liên quan (nếu có) sẽ bị xóa vĩnh viễn.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-justify-content-center">
|
||||
<!-- Captcha addon group - shown only when captcha is not confirmed -->
|
||||
<p class="control" v-if="!isConfirmed">
|
||||
<div
|
||||
class="control"
|
||||
v-if="!isConfirmed"
|
||||
>
|
||||
<div class="field has-addons">
|
||||
<p class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Nhập mã xác nhận"
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Nhập mã xác nhận"
|
||||
v-model="userInputCaptcha"
|
||||
@keyup.enter="isConfirmed && confirmDelete()"
|
||||
>
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-static has-text-weight-bold has-background-grey-lighter"
|
||||
style="font-family: 'Courier New', monospace; letter-spacing: 2px;">
|
||||
<a
|
||||
class="button is-static has-text-weight-bold has-background-grey-lighter"
|
||||
style="font-family: "Courier New", monospace; letter-spacing: 2px"
|
||||
>
|
||||
{{ captchaCode }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button" @click="generateCaptcha" title="Tạo mã mới">
|
||||
<button
|
||||
class="button"
|
||||
@click="generateCaptcha"
|
||||
title="Tạo mã mới"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon name="refresh.svg" type="primary" :size="23" />
|
||||
<SvgIcon
|
||||
name="refresh.svg"
|
||||
type="primary"
|
||||
:size="23"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<!-- Confirm button - shown only when captcha IS confirmed -->
|
||||
<p class="control" v-if="isConfirmed">
|
||||
<p
|
||||
class="control"
|
||||
v-if="isConfirmed"
|
||||
>
|
||||
<button
|
||||
class="button is-danger"
|
||||
:class="{ 'is-loading': isDeleting }"
|
||||
@@ -69,75 +79,70 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useNuxtApp } from '#app'
|
||||
import { ref, computed } from "vue";
|
||||
import { useNuxtApp } from "#app";
|
||||
|
||||
const props = defineProps({
|
||||
entryId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
})
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'deleted'])
|
||||
const emit = defineEmits(["close", "deleted"]);
|
||||
|
||||
const { $snackbar ,$insertapi} = useNuxtApp()
|
||||
const isDeleting = ref(false)
|
||||
const captchaCode = ref('')
|
||||
const userInputCaptcha = ref('')
|
||||
const { $snackbar, $insertapi } = useNuxtApp();
|
||||
const isDeleting = ref(false);
|
||||
const captchaCode = ref("");
|
||||
const userInputCaptcha = ref("");
|
||||
|
||||
const isConfirmed = computed(() => {
|
||||
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== ''
|
||||
})
|
||||
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase() && userInputCaptcha.value !== "";
|
||||
});
|
||||
|
||||
const generateCaptcha = () => {
|
||||
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase()
|
||||
userInputCaptcha.value = ''
|
||||
}
|
||||
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase();
|
||||
userInputCaptcha.value = "";
|
||||
};
|
||||
|
||||
// Initial generation
|
||||
generateCaptcha()
|
||||
generateCaptcha();
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (isDeleting.value || !isConfirmed.value) return
|
||||
isDeleting.value = true
|
||||
if (isDeleting.value || !isConfirmed.value) return;
|
||||
isDeleting.value = true;
|
||||
|
||||
try {
|
||||
// Gọi API xóa theo đúng endpoint delete-entry/{id}
|
||||
const result = await $insertapi('deleteentry', {id: props.entryId})
|
||||
const result = await $insertapi("deleteentry", { id: props.entryId });
|
||||
|
||||
if (result === 'error' || !result) {
|
||||
throw new Error('API xóa trả về lỗi')
|
||||
if (result === "error" || !result) {
|
||||
throw new Error("API xóa trả về lỗi");
|
||||
}
|
||||
|
||||
$snackbar(
|
||||
`Đã xóa bút toán ID ${props.entryId} thành công`,
|
||||
'Thành công',
|
||||
'Success'
|
||||
)
|
||||
|
||||
emit('deleted', props.entryId)
|
||||
emit('close')
|
||||
$snackbar(`Đã xóa bút toán ID ${props.entryId} thành công`, "Thành công", "Success");
|
||||
|
||||
emit("deleted", props.entryId);
|
||||
emit("close");
|
||||
} catch (err) {
|
||||
console.error('Xóa bút toán thất bại:', err)
|
||||
console.error("Xóa bút toán thất bại:", err);
|
||||
|
||||
let errorMsg = "Không thể xóa bút toán. Vui lòng thử lại.";
|
||||
|
||||
let errorMsg = 'Không thể xóa bút toán. Vui lòng thử lại.'
|
||||
|
||||
// Nếu backend trả về thông báo cụ thể
|
||||
if (err?.response?.data?.detail) {
|
||||
errorMsg = err.response.data.detail
|
||||
errorMsg = err.response.data.detail;
|
||||
} else if (err?.response?.data?.non_field_errors) {
|
||||
errorMsg = err.response.data.non_field_errors.join(' ')
|
||||
errorMsg = err.response.data.non_field_errors.join(" ");
|
||||
}
|
||||
|
||||
$snackbar(errorMsg, 'Lỗi', 'Danger')
|
||||
$snackbar(errorMsg, "Lỗi", "Danger");
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
isDeleting.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('close')
|
||||
}
|
||||
emit("close");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
@date="selected('fdate', $event)"
|
||||
></Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_date"
|
||||
>
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
@@ -21,11 +26,19 @@
|
||||
@date="selected('tdate', $event)"
|
||||
></Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_date"
|
||||
>
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DataView v-bind="vbind" v-if="vbind" />
|
||||
<DataView
|
||||
v-bind="vbind"
|
||||
v-if="vbind"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
const { $dayjs, $id } = useNuxtApp();
|
||||
@@ -37,9 +50,9 @@ const vbind = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
})
|
||||
});
|
||||
|
||||
function selected(attr, value) {
|
||||
function selected(attr, value) {
|
||||
if (attr === "fdate") fdate.value = value;
|
||||
else tdate.value = value;
|
||||
loadData();
|
||||
@@ -56,18 +69,27 @@ function loadData() {
|
||||
values:
|
||||
"customer__code,customer__fullname,customer__type__name,customer__legal_type__name,customer__legal_code",
|
||||
distinct_values: {
|
||||
sum_sale_price: { type: "Sum", field: "product__prdbk__transaction__sale_price" },
|
||||
sum_received: { type: "Sum", field: "product__prdbk__transaction__amount_received" },
|
||||
sum_remain: { type: "Sum", field: "product__prdbk__transaction__amount_remain" },
|
||||
sum_sale_price: {
|
||||
type: "Sum",
|
||||
field: "product__prdbk__transaction__sale_price",
|
||||
},
|
||||
sum_received: {
|
||||
type: "Sum",
|
||||
field: "product__prdbk__transaction__amount_received",
|
||||
},
|
||||
sum_remain: {
|
||||
type: "Sum",
|
||||
field: "product__prdbk__transaction__amount_remain",
|
||||
},
|
||||
},
|
||||
summary: "annotate",
|
||||
filter: {
|
||||
date__gte: fdate.value,
|
||||
date__lte: tdate.value
|
||||
filter: {
|
||||
date__gte: fdate.value,
|
||||
date__lte: tdate.value,
|
||||
},
|
||||
sort: "-sum_remain",
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
@date="selected('fdate', $event)"
|
||||
></Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_date"
|
||||
>
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
@@ -21,11 +26,19 @@
|
||||
@date="selected('tdate', $event)"
|
||||
></Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_date"
|
||||
>
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DataView v-bind="vbind" v-if="vbind" />
|
||||
<DataView
|
||||
v-bind="vbind"
|
||||
v-if="vbind"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
@@ -60,13 +73,21 @@ export default {
|
||||
values:
|
||||
"product,product__prdbk__transaction__amount_received,product__trade_code,product__prdbk__transaction__sale_price,product__zone_type__name,customer,customer__code,customer__fullname",
|
||||
distinct_values: {
|
||||
sumCR: { type: "Sum", filter: { type__code: "CR" }, field: "amount" },
|
||||
sumDR: { type: "Sum", filter: { type__code: "DR" }, field: "amount" },
|
||||
sumCR: {
|
||||
type: "Sum",
|
||||
filter: { type__code: "CR" },
|
||||
field: "amount",
|
||||
},
|
||||
sumDR: {
|
||||
type: "Sum",
|
||||
filter: { type__code: "DR" },
|
||||
field: "amount",
|
||||
},
|
||||
},
|
||||
summary: "annotate",
|
||||
filter: { date__gte: this.fdate, date__lte: this.tdate },
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
pagename: 'debt_report',
|
||||
api: 'transaction',
|
||||
timeopt: { time: 36000, disable: ['add'] },
|
||||
filter: { phase: 3 }
|
||||
filter: { phase: 3 },
|
||||
}"
|
||||
@option="handleTimeOption"
|
||||
@excel="exportExcel"
|
||||
@@ -15,17 +15,26 @@
|
||||
/>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="has-text-centered py-6">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="has-text-centered py-6"
|
||||
>
|
||||
<p class="has-text-grey mb-3">Đang tải dữ liệu...</p>
|
||||
<progress class="progress is-small is-primary" max="100"></progress>
|
||||
<progress
|
||||
class="progress is-small is-primary"
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<div class="" v-else>
|
||||
<div
|
||||
class=""
|
||||
v-else
|
||||
>
|
||||
<!-- Table -->
|
||||
<div
|
||||
v-if="filteredRows.length > 0"
|
||||
class="table-container"
|
||||
style="overflow-x: auto; max-width: 100%;"
|
||||
style="overflow-x: auto; max-width: 100%"
|
||||
>
|
||||
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth debt-table">
|
||||
<thead>
|
||||
@@ -37,22 +46,40 @@
|
||||
>
|
||||
STT
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Mã KH
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Mã Căn
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Ngày ký HĐ
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Giá trị HĐMB
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Tiền nộp theo HĐV/TTTHNV
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Tỷ lệ
|
||||
</th>
|
||||
|
||||
@@ -66,13 +93,19 @@
|
||||
{{ sch.label }}
|
||||
</th>
|
||||
|
||||
<th rowspan="2" class="has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="has-background-primary has-text-white"
|
||||
>
|
||||
Số tiền quá hạn
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
||||
<template
|
||||
v-for="(sch, si) in scheduleHeaders"
|
||||
:key="si"
|
||||
>
|
||||
<th class="has-background-primary has-text-white sub-header">Ngày</th>
|
||||
<th class="has-background-primary has-text-white sub-header">Số tiền</th>
|
||||
<th class="has-background-primary has-text-white sub-header">Lũy kế sang đợt</th>
|
||||
@@ -84,13 +117,20 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(row, ri) in filteredRows" :key="ri">
|
||||
<tr
|
||||
v-for="(row, ri) in filteredRows"
|
||||
:key="ri"
|
||||
>
|
||||
<!-- Fixed columns -->
|
||||
<td class="fixed-col has-text-centered">{{ ri + 1 }}</td>
|
||||
<td class="fixed-col">{{ row.customer_code }}</td>
|
||||
<td class="fixed-col has-text-weight-semibold has-text-primary">{{ row.trade_code }}</td>
|
||||
<td class="fixed-col has-text-weight-semibold has-text-primary">
|
||||
{{ row.trade_code }}
|
||||
</td>
|
||||
<td class="fixed-col">{{ row.contract_date }}</td>
|
||||
<td class="fixed-col has-text-right">{{ fmt(row.sale_price) }}</td>
|
||||
<td class="fixed-col has-text-right">
|
||||
{{ fmt(row.sale_price) }}
|
||||
</td>
|
||||
<td class="fixed-col has-text-right has-text-weight-semibold has-background-warning-light">
|
||||
{{ fmt(row.ttthnv_paid) }}
|
||||
</td>
|
||||
@@ -99,11 +139,18 @@
|
||||
</td>
|
||||
|
||||
<!-- Scrollable columns -->
|
||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
||||
<template
|
||||
v-for="(sch, si) in scheduleHeaders"
|
||||
:key="si"
|
||||
>
|
||||
<template v-if="row.schedules[si]">
|
||||
<td>{{ row.schedules[si].to_date }}</td>
|
||||
<td class="has-text-right">{{ fmt(row.schedules[si].amount) }}</td>
|
||||
<td class="has-text-right has-text-info">{{ fmt(row.schedules[si].luy_ke_sang_dot) }}</td>
|
||||
<td class="has-text-right">
|
||||
{{ fmt(row.schedules[si].amount) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-info">
|
||||
{{ fmt(row.schedules[si].luy_ke_sang_dot) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-weight-semibold has-text-success">
|
||||
{{ fmt(row.schedules[si].thuc_thanh_toan) }}
|
||||
</td>
|
||||
@@ -121,7 +168,13 @@
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td colspan="6" class="has-text-centered has-text-grey-light" style="font-style: italic">—</td>
|
||||
<td
|
||||
colspan="6"
|
||||
class="has-text-centered has-text-grey-light"
|
||||
style="font-style: italic"
|
||||
>
|
||||
—
|
||||
</td>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -129,25 +182,35 @@
|
||||
class="has-text-right has-text-weight-bold"
|
||||
:class="Number(row.overdue) > 0 ? 'has-background-danger-light' : ''"
|
||||
>
|
||||
{{ Number(row.overdue) > 0 ? fmt(row.overdue) : '—' }}
|
||||
{{ Number(row.overdue) > 0 ? fmt(row.overdue) : "—" }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr class="has-background-light">
|
||||
<td :colspan="7" class="fixed-col has-text-right has-text-weight-bold">TỔNG CỘNG:</td>
|
||||
<td
|
||||
:colspan="7"
|
||||
class="fixed-col has-text-right has-text-weight-bold"
|
||||
>
|
||||
TỔNG CỘNG:
|
||||
</td>
|
||||
|
||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
||||
<template
|
||||
v-for="(sch, si) in scheduleHeaders"
|
||||
:key="si"
|
||||
>
|
||||
<td></td>
|
||||
<td class="has-text-right has-text-weight-semibold">{{ fmt(colSum(si, 'amount')) }}</td>
|
||||
<td class="has-text-right has-text-weight-semibold">
|
||||
{{ fmt(colSum(si, "amount")) }}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="has-text-right has-text-weight-semibold has-text-success">
|
||||
{{ fmt(colSum(si, 'thuc_thanh_toan')) }}
|
||||
{{ fmt(colSum(si, "thuc_thanh_toan")) }}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="has-text-right has-text-weight-semibold has-text-danger">
|
||||
{{ fmt(colSum(si, 'amount_remain')) }}
|
||||
{{ fmt(colSum(si, "amount_remain")) }}
|
||||
</td>
|
||||
</template>
|
||||
|
||||
@@ -160,7 +223,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div v-else class="has-text-centered py-6">
|
||||
<div
|
||||
v-else
|
||||
class="has-text-centered py-6"
|
||||
>
|
||||
<p class="has-text-grey">Không có dữ liệu</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,283 +234,279 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import TimeOption from '~/components/datatable/TimeOption'
|
||||
const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp()
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import TimeOption from "~/components/datatable/TimeOption";
|
||||
const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp();
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const filteredRows = ref([])
|
||||
const scheduleHeaders = ref([])
|
||||
const loading = ref(false);
|
||||
const rows = ref([]);
|
||||
const filteredRows = ref([]);
|
||||
const scheduleHeaders = ref([]);
|
||||
|
||||
const currentFilter = ref(null)
|
||||
const currentSearch = ref(null)
|
||||
const currentFilter = ref(null);
|
||||
const currentSearch = ref(null);
|
||||
|
||||
function handleTimeOption(option) {
|
||||
if (!option) {
|
||||
currentFilter.value = null
|
||||
currentSearch.value = null
|
||||
applyFilters()
|
||||
return
|
||||
currentFilter.value = null;
|
||||
currentSearch.value = null;
|
||||
applyFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.filter) {
|
||||
currentFilter.value = option.filter
|
||||
currentSearch.value = null
|
||||
applyFilters()
|
||||
currentFilter.value = option.filter;
|
||||
currentSearch.value = null;
|
||||
applyFilters();
|
||||
} else if (option.filter_or) {
|
||||
currentFilter.value = null
|
||||
currentSearch.value = option.filter_or
|
||||
applyFilters()
|
||||
currentFilter.value = null;
|
||||
currentSearch.value = option.filter_or;
|
||||
applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
let filtered = [...rows.value]
|
||||
let filtered = [...rows.value];
|
||||
|
||||
if (currentFilter.value && currentFilter.value.create_time__date__gte) {
|
||||
const filterDate = new Date(currentFilter.value.create_time__date__gte)
|
||||
filtered = filtered.filter(row => {
|
||||
const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null
|
||||
return contractDate && contractDate >= filterDate
|
||||
})
|
||||
const filterDate = new Date(currentFilter.value.create_time__date__gte);
|
||||
filtered = filtered.filter((row) => {
|
||||
const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null;
|
||||
return contractDate && contractDate >= filterDate;
|
||||
});
|
||||
}
|
||||
|
||||
if (currentSearch.value) {
|
||||
const searchTerms = Object.values(currentSearch.value).map(v =>
|
||||
String(v).toLowerCase().replace('__icontains', '')
|
||||
)
|
||||
filtered = filtered.filter(row => {
|
||||
const searchTerms = Object.values(currentSearch.value).map((v) =>
|
||||
String(v).toLowerCase().replace("__icontains", ""),
|
||||
);
|
||||
filtered = filtered.filter((row) => {
|
||||
const searchableText = [
|
||||
row.customer_code,
|
||||
row.customer_name,
|
||||
row.trade_code,
|
||||
row.contract_date,
|
||||
String(row.sale_price)
|
||||
].join(' ').toLowerCase()
|
||||
return searchTerms.some(term => searchableText.includes(term))
|
||||
})
|
||||
String(row.sale_price),
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return searchTerms.some((term) => searchableText.includes(term));
|
||||
});
|
||||
}
|
||||
|
||||
filteredRows.value = filtered
|
||||
filteredRows.value = filtered;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
rows.value = []
|
||||
filteredRows.value = []
|
||||
scheduleHeaders.value = []
|
||||
loading.value = true;
|
||||
rows.value = [];
|
||||
filteredRows.value = [];
|
||||
scheduleHeaders.value = [];
|
||||
|
||||
try {
|
||||
const txnConn = $copy($findapi('transaction'))
|
||||
const txnConn = $copy($findapi("transaction"));
|
||||
txnConn.params = {
|
||||
filter: { phase: 3 },
|
||||
values: 'id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase',
|
||||
sort: 'id'
|
||||
}
|
||||
values:
|
||||
"id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase",
|
||||
sort: "id",
|
||||
};
|
||||
|
||||
const detailConn = $copy($findapi('reservation'))
|
||||
const detailConn = $copy($findapi("reservation"));
|
||||
detailConn.params = {
|
||||
filter: { transaction__phase: 3, phase: 3 },
|
||||
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status',
|
||||
sort: 'transaction'
|
||||
}
|
||||
values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
|
||||
sort: "transaction",
|
||||
};
|
||||
|
||||
const schConn = $copy($findapi('payment_schedule'))
|
||||
const schConn = $copy($findapi("payment_schedule"));
|
||||
schConn.params = {
|
||||
filter: { txn_detail__phase: 3 },
|
||||
values:
|
||||
'id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received',
|
||||
sort: 'txn_detail__transaction,cycle'
|
||||
}
|
||||
"id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received",
|
||||
sort: "txn_detail__transaction,cycle",
|
||||
};
|
||||
|
||||
const ttthnvConn = $copy($findapi('reservation'))
|
||||
const ttthnvConn = $copy($findapi("reservation"));
|
||||
ttthnvConn.params = {
|
||||
filter: { transaction__phase: 3, phase: 4 },
|
||||
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status',
|
||||
sort: 'transaction'
|
||||
}
|
||||
values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
|
||||
sort: "transaction",
|
||||
};
|
||||
|
||||
const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([
|
||||
txnConn,
|
||||
detailConn,
|
||||
schConn,
|
||||
ttthnvConn
|
||||
])
|
||||
const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([txnConn, detailConn, schConn, ttthnvConn]);
|
||||
|
||||
const transactions = txnRs?.data?.rows || []
|
||||
const details = detailRs?.data?.rows || []
|
||||
const schedules = schRs?.data?.rows || []
|
||||
const ttthnvList = ttthnvRs?.data?.rows || []
|
||||
const transactions = txnRs?.data?.rows || [];
|
||||
const details = detailRs?.data?.rows || [];
|
||||
const schedules = schRs?.data?.rows || [];
|
||||
const ttthnvList = ttthnvRs?.data?.rows || [];
|
||||
|
||||
if (!transactions.length) {
|
||||
loading.value = false
|
||||
return
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// TTTHNV map
|
||||
const ttthnvMap = {}
|
||||
ttthnvList.forEach(t => {
|
||||
const tid = t.transaction
|
||||
ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0)
|
||||
})
|
||||
const ttthnvMap = {};
|
||||
ttthnvList.forEach((t) => {
|
||||
const tid = t.transaction;
|
||||
ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0);
|
||||
});
|
||||
|
||||
// Group schedules by transaction
|
||||
const schByTxn = {}
|
||||
schedules.forEach(s => {
|
||||
const tid = s.txn_detail__transaction
|
||||
if (!schByTxn[tid]) schByTxn[tid] = []
|
||||
schByTxn[tid].push(s)
|
||||
})
|
||||
const schByTxn = {};
|
||||
schedules.forEach((s) => {
|
||||
const tid = s.txn_detail__transaction;
|
||||
if (!schByTxn[tid]) schByTxn[tid] = [];
|
||||
schByTxn[tid].push(s);
|
||||
});
|
||||
|
||||
// Tìm số đợt tối đa
|
||||
let maxCycles = 0
|
||||
Object.values(schByTxn).forEach(list => {
|
||||
const paymentList = list.filter(s => Number(s.cycle) > 0)
|
||||
if (paymentList.length > maxCycles) maxCycles = paymentList.length
|
||||
})
|
||||
let maxCycles = 0;
|
||||
Object.values(schByTxn).forEach((list) => {
|
||||
const paymentList = list.filter((s) => Number(s.cycle) > 0);
|
||||
if (paymentList.length > maxCycles) maxCycles = paymentList.length;
|
||||
});
|
||||
|
||||
scheduleHeaders.value = Array.from({ length: maxCycles }, (_, i) => ({
|
||||
label: `L0${i + 1}`,
|
||||
index: i
|
||||
}))
|
||||
index: i,
|
||||
}));
|
||||
|
||||
rows.value = transactions.map(txn => {
|
||||
rows.value = transactions.map((txn) => {
|
||||
const txnSchedules = (schByTxn[txn.id] || [])
|
||||
.filter(s => Number(s.cycle) > 0)
|
||||
.sort((a, b) => Number(a.cycle) - Number(b.cycle))
|
||||
.filter((s) => Number(s.cycle) > 0)
|
||||
.sort((a, b) => Number(a.cycle) - Number(b.cycle));
|
||||
|
||||
const ttthnvPaid = ttthnvMap[txn.id] || 0
|
||||
const salePriceNum = Number(txn.sale_price || 0)
|
||||
const ttthnvPaid = ttthnvMap[txn.id] || 0;
|
||||
const salePriceNum = Number(txn.sale_price || 0);
|
||||
|
||||
// ───────────────────────────────────────────────
|
||||
// QUAN TRỌNG: Theo yêu cầu mới nhất của bạn
|
||||
// Lũy kế HĐCN = TTTHNV
|
||||
const luyKe = ttthnvPaid
|
||||
const luyKe = ttthnvPaid;
|
||||
// ───────────────────────────────────────────────
|
||||
|
||||
// Phân bổ TTTHNV dần vào từng đợt → tính lũy kế sang đợt
|
||||
let remainingTTTHNV = ttthnvPaid
|
||||
let remainingTTTHNV = ttthnvPaid;
|
||||
|
||||
const schedulesWithCalc = txnSchedules.map(sch => {
|
||||
const scheduleAmount = Number(sch.amount || 0)
|
||||
const schedulesWithCalc = txnSchedules.map((sch) => {
|
||||
const scheduleAmount = Number(sch.amount || 0);
|
||||
|
||||
// Lũy kế sang đợt = min(remaining TTTHNV, số tiền đợt)
|
||||
const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount)
|
||||
const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount);
|
||||
|
||||
// Số tiền đã thực thanh toán = paid_amount - lũy kế sang đợt
|
||||
const paidAmountFromSchedule = Number(sch.paid_amount || 0)
|
||||
const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot)
|
||||
const paidAmountFromSchedule = Number(sch.paid_amount || 0);
|
||||
const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot);
|
||||
|
||||
// Dư nợ = số tiền đợt - lũy kế sang đợt - thực thanh toán
|
||||
const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan)
|
||||
const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan);
|
||||
|
||||
remainingTTTHNV -= luyKeSangDot
|
||||
remainingTTTHNV = Math.max(0, remainingTTTHNV)
|
||||
remainingTTTHNV -= luyKeSangDot;
|
||||
remainingTTTHNV = Math.max(0, remainingTTTHNV);
|
||||
|
||||
return {
|
||||
to_date: sch.to_date ? $dayjs(sch.to_date).format('DD/MM/YYYY') : '—',
|
||||
to_date: sch.to_date ? $dayjs(sch.to_date).format("DD/MM/YYYY") : "—",
|
||||
amount: scheduleAmount,
|
||||
luy_ke_sang_dot: luyKeSangDot,
|
||||
thuc_thanh_toan: thucThanhToan,
|
||||
amount_remain: amountRemain,
|
||||
status: sch.status
|
||||
}
|
||||
})
|
||||
status: sch.status,
|
||||
};
|
||||
});
|
||||
|
||||
// Tính quá hạn
|
||||
const todayDate = new Date()
|
||||
const todayDate = new Date();
|
||||
const overdue = txnSchedules.reduce((sum, sch, idx) => {
|
||||
const toDate = sch.to_date ? new Date(sch.to_date) : null
|
||||
const remain = schedulesWithCalc[idx]?.amount_remain || 0
|
||||
const toDate = sch.to_date ? new Date(sch.to_date) : null;
|
||||
const remain = schedulesWithCalc[idx]?.amount_remain || 0;
|
||||
if (toDate && toDate < todayDate && remain > 0) {
|
||||
return sum + remain
|
||||
return sum + remain;
|
||||
}
|
||||
return sum
|
||||
}, 0)
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null)
|
||||
const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null);
|
||||
|
||||
return {
|
||||
customer_code: txn.customer__code || '',
|
||||
customer_name: txn.customer__fullname || '',
|
||||
trade_code: txn.product__trade_code || txn.code || '',
|
||||
contract_date: txn.date ? $dayjs(txn.date).format('DD/MM/YYYY') : '—',
|
||||
customer_code: txn.customer__code || "",
|
||||
customer_name: txn.customer__fullname || "",
|
||||
trade_code: txn.product__trade_code || txn.code || "",
|
||||
contract_date: txn.date ? $dayjs(txn.date).format("DD/MM/YYYY") : "—",
|
||||
contract_date_raw: txn.date,
|
||||
sale_price: salePriceNum,
|
||||
ttthnv_paid: ttthnvPaid,
|
||||
luy_ke: luyKe, // ← chính là TTTHNV
|
||||
luy_ke: luyKe, // ← chính là TTTHNV
|
||||
schedules: paddedSchedules,
|
||||
overdue: overdue
|
||||
}
|
||||
})
|
||||
overdue: overdue,
|
||||
};
|
||||
});
|
||||
|
||||
filteredRows.value = rows.value
|
||||
filteredRows.value = rows.value;
|
||||
} catch (e) {
|
||||
console.error('BaoCaoCongNo error:', e)
|
||||
console.error("BaoCaoCongNo error:", e);
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(val) {
|
||||
const n = Number(val)
|
||||
if (isNaN(n) || (!n && n !== 0)) return '—'
|
||||
return n.toLocaleString('vi-VN')
|
||||
const n = Number(val);
|
||||
if (isNaN(n) || (!n && n !== 0)) return "—";
|
||||
return n.toLocaleString("vi-VN");
|
||||
}
|
||||
|
||||
function pct(num, denom) {
|
||||
const n = Number(num)
|
||||
const d = Number(denom)
|
||||
if (!d || isNaN(n)) return '—'
|
||||
return (n / d * 100).toFixed(1) + '%'
|
||||
const n = Number(num);
|
||||
const d = Number(denom);
|
||||
if (!d || isNaN(n)) return "—";
|
||||
return ((n / d) * 100).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
function pctClass(paid, amount) {
|
||||
const p = Number(paid)
|
||||
const a = Number(amount)
|
||||
if (isNaN(p) || isNaN(a) || !a) return ''
|
||||
const ratio = p / a
|
||||
if (ratio >= 1) return 'has-text-success'
|
||||
if (ratio >= 0.5) return 'has-text-info'
|
||||
return 'has-text-danger'
|
||||
const p = Number(paid);
|
||||
const a = Number(amount);
|
||||
if (isNaN(p) || isNaN(a) || !a) return "";
|
||||
const ratio = p / a;
|
||||
if (ratio >= 1) return "has-text-success";
|
||||
if (ratio >= 0.5) return "has-text-info";
|
||||
return "has-text-danger";
|
||||
}
|
||||
|
||||
function colSum(scheduleIndex, field) {
|
||||
return filteredRows.value.reduce((sum, row) => {
|
||||
const sch = row.schedules[scheduleIndex]
|
||||
return sum + (sch ? Number(sch[field] || 0) : 0)
|
||||
}, 0)
|
||||
const sch = row.schedules[scheduleIndex];
|
||||
return sum + (sch ? Number(sch[field] || 0) : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
const totalOverdue = computed(() =>
|
||||
filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0)
|
||||
)
|
||||
const totalOverdue = computed(() => filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0));
|
||||
|
||||
function exportExcel() {
|
||||
const headers = [
|
||||
'STT',
|
||||
'Mã KH',
|
||||
'Mã Căn',
|
||||
'Ngày ký HĐ',
|
||||
'Giá trị HĐMB',
|
||||
'Tiền nộp TTTHNV',
|
||||
'Lũy kế tiền về HĐCN',
|
||||
'Tỷ lệ HĐCN'
|
||||
]
|
||||
"STT",
|
||||
"Mã KH",
|
||||
"Mã Căn",
|
||||
"Ngày ký HĐ",
|
||||
"Giá trị HĐMB",
|
||||
"Tiền nộp TTTHNV",
|
||||
"Lũy kế tiền về HĐCN",
|
||||
"Tỷ lệ HĐCN",
|
||||
];
|
||||
|
||||
scheduleHeaders.value.forEach(h => {
|
||||
scheduleHeaders.value.forEach((h) => {
|
||||
headers.push(
|
||||
`${h.label} - Ngày`,
|
||||
`${h.label} - Số tiền`,
|
||||
`${h.label} - Lũy kế sang`,
|
||||
`${h.label} - Số tiền đã thực thanh toán`,
|
||||
`${h.label} - Tỷ lệ`,
|
||||
`${h.label} - Dư nợ`
|
||||
)
|
||||
})
|
||||
`${h.label} - Dư nợ`,
|
||||
);
|
||||
});
|
||||
|
||||
headers.push('Số tiền quá hạn')
|
||||
headers.push("Số tiền quá hạn");
|
||||
|
||||
const data = filteredRows.value.map((row, i) => {
|
||||
const base = [
|
||||
@@ -455,11 +517,11 @@ function exportExcel() {
|
||||
fmt(row.sale_price),
|
||||
fmt(row.ttthnv_paid),
|
||||
fmt(row.luy_ke),
|
||||
pct(row.luy_ke, row.sale_price)
|
||||
]
|
||||
pct(row.luy_ke, row.sale_price),
|
||||
];
|
||||
|
||||
scheduleHeaders.value.forEach((_, si) => {
|
||||
const sch = row.schedules[si]
|
||||
const sch = row.schedules[si];
|
||||
if (sch) {
|
||||
base.push(
|
||||
sch.to_date,
|
||||
@@ -467,31 +529,31 @@ function exportExcel() {
|
||||
fmt(sch.luy_ke_sang_dot),
|
||||
fmt(sch.thuc_thanh_toan),
|
||||
pct(sch.thuc_thanh_toan, sch.amount),
|
||||
fmt(sch.amount_remain)
|
||||
)
|
||||
fmt(sch.amount_remain),
|
||||
);
|
||||
} else {
|
||||
base.push('', '', '', '', '', '')
|
||||
base.push("", "", "", "", "", "");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
base.push(fmt(row.overdue))
|
||||
return base
|
||||
})
|
||||
base.push(fmt(row.overdue));
|
||||
return base;
|
||||
});
|
||||
|
||||
const csvRows = [headers, ...data]
|
||||
const csv = csvRows.map(r => r.map(c => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',')).join('\n')
|
||||
const csvRows = [headers, ...data];
|
||||
const csv = csvRows.map((r) => r.map((c) => `"${String(c ?? "").replace(/"/g, '""')}"`).join(",")).join("\n");
|
||||
|
||||
const BOM = '\uFEFF'
|
||||
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `bao-cao-cong-no-${$dayjs().format('YYYYMMDD')}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
const BOM = "\uFEFF";
|
||||
const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `bao-cao-cong-no-${$dayjs().format("YYYYMMDD")}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
onMounted(() => loadData());
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -578,4 +640,4 @@ onMounted(() => loadData())
|
||||
.fixed-col {
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<DataView v-bind="{api: 'internalaccount', setting: store.lang==='en'? 'internal-account-en' : 'internal-account', pagename: pagename,
|
||||
modal: {title: 'Tài khoản', component: 'accounting/AccountView', width: '50%', 'height': '300px'}}" />
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" />
|
||||
</div>
|
||||
<div>
|
||||
<DataView
|
||||
v-bind="{
|
||||
api: 'internalaccount',
|
||||
setting: store.lang === 'en' ? 'internal-account-en' : 'internal-account',
|
||||
pagename: pagename,
|
||||
modal: {
|
||||
title: 'Tài khoản',
|
||||
component: 'accounting/AccountView',
|
||||
width: '50%',
|
||||
height: '300px',
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useStore } from '~/stores/index'
|
||||
import { useStore } from "~/stores/index";
|
||||
export default {
|
||||
setup() {
|
||||
const store = useStore()
|
||||
return {store}
|
||||
const store = useStore();
|
||||
return { store };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showmodal: undefined,
|
||||
pagename: 'pagedata32'
|
||||
}
|
||||
pagename: "pagedata32",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
deposit() {
|
||||
this.showmodal = {component: 'accounting/InternalDeposit', title: 'Nộp tiền tài khoản nội bộ', width: '40%', height: '300px',
|
||||
vbind: {pagename: this.pagename}}
|
||||
this.showmodal = {
|
||||
component: "accounting/InternalDeposit",
|
||||
title: "Nộp tiền tài khoản nội bộ",
|
||||
width: "40%",
|
||||
height: "300px",
|
||||
vbind: { pagename: this.pagename },
|
||||
};
|
||||
},
|
||||
doClick() {
|
||||
this.$approvalcode()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
this.$approvalcode();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,176 +1,288 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div class="column is-8">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('account') }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{api:'internalaccount', field:'label', column:['label'], first: true, optionid: record.account}"
|
||||
:disabled="record.account" @option="selected('_account', $event)" v-if="!record.id"></SearchBox>
|
||||
<span v-else>{{record.account__code}}</span>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors._account">{{ errors._account }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div class="column is-8">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label
|
||||
<label class="label">{{ $lang("account") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'internalaccount',
|
||||
field: 'label',
|
||||
column: ['label'],
|
||||
first: true,
|
||||
optionid: record.account,
|
||||
}"
|
||||
:disabled="record.account"
|
||||
@option="selected('_account', $event)"
|
||||
v-if="!record.id"
|
||||
></SearchBox>
|
||||
<span v-else>{{ record.account__code }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors._account"
|
||||
>
|
||||
{{ errors._account }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Ngày hạch toán<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<Datepicker
|
||||
v-bind="{ record: record, attr: 'date', maxdate: new Date()}"
|
||||
v-bind="{ record: record, attr: 'date', maxdate: new Date() }"
|
||||
@date="selected('date', $event)"
|
||||
></Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_date"
|
||||
>
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Thu / chi<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{api:'entrytype', field:'name', column:['name'], first: true, optionid: record.type}"
|
||||
:disabled="record.type" @option="selected('_type', $event)" v-if="!record.id"></SearchBox>
|
||||
<span v-else>{{record.type__name}}</span>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">{{$lang('amount-only')}}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<InputNumber v-bind="{record: record, attr: 'amount', placeholder: ''}" @number="selected('amount', $event)"></InputNumber>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.amount">{{errors.amount}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Phương thức thanh toán<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{api:'entrycategory', field:'name', column:['name'], first: true, optionid: record.type}"
|
||||
:disabled="record.type" @option="selected('_category', $event)" v-if="!record.id"></SearchBox>
|
||||
<span v-else>{{record.type__name}}</span>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Sản phẩm<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{
|
||||
api: 'product',
|
||||
field: 'label',
|
||||
searchfield: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
||||
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
||||
first: true
|
||||
}" @option="selected('product', $event)" />
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Khách hàng<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{
|
||||
api: 'customer',
|
||||
field: 'label',
|
||||
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
|
||||
column: ['code', 'fullname', 'phone', 'legal_code'],
|
||||
first: true
|
||||
}" @option="selected('customer', $event)" />
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors._type">{{ errors._type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('content') }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" rows="2" v-model="record.content"></textarea>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.content">{{errors.content}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Mã tham chiếu</label>
|
||||
<div class="control">
|
||||
<input class="input has-text-black" type="text" placeholder="Tối đa 30 ký tự" v-model="record.ref">
|
||||
<label class="label">Thu / chi<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'entrytype',
|
||||
field: 'name',
|
||||
column: ['name'],
|
||||
first: true,
|
||||
optionid: record.type,
|
||||
}"
|
||||
:disabled="record.type"
|
||||
@option="selected('_type', $event)"
|
||||
v-if="!record.id"
|
||||
></SearchBox>
|
||||
<span v-else>{{ record.type__name }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors._type"
|
||||
>
|
||||
{{ errors._type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("amount-only") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<InputNumber
|
||||
v-bind="{ record: record, attr: 'amount', placeholder: '' }"
|
||||
@number="selected('amount', $event)"
|
||||
></InputNumber>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.amount"
|
||||
>
|
||||
{{ errors.amount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Phương thức thanh toán<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'entrycategory',
|
||||
field: 'name',
|
||||
column: ['name'],
|
||||
first: true,
|
||||
optionid: record.type,
|
||||
}"
|
||||
:disabled="record.type"
|
||||
@option="selected('_category', $event)"
|
||||
v-if="!record.id"
|
||||
></SearchBox>
|
||||
<span v-else>{{ record.type__name }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors._type"
|
||||
>
|
||||
{{ errors._type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Sản phẩm<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'product',
|
||||
field: 'label',
|
||||
searchfield: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
||||
column: ['code', 'trade_code', 'type__name', 'land_lot_size', 'zone_type__name'],
|
||||
first: true,
|
||||
}"
|
||||
@option="selected('product', $event)"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors._type"
|
||||
>
|
||||
{{ errors._type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Khách hàng<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'customer',
|
||||
field: 'label',
|
||||
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
|
||||
column: ['code', 'fullname', 'phone', 'legal_code'],
|
||||
first: true,
|
||||
}"
|
||||
@option="selected('customer', $event)"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors._type"
|
||||
>
|
||||
{{ errors._type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("content") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
class="textarea"
|
||||
rows="2"
|
||||
v-model="record.content"
|
||||
></textarea>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.content"
|
||||
>
|
||||
{{ errors.content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Mã tham chiếu</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input has-text-black"
|
||||
type="text"
|
||||
placeholder="Tối đa 30 ký tự"
|
||||
v-model="record.ref"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="column is-12"
|
||||
v-if="entry"
|
||||
>
|
||||
<div class="field">
|
||||
<label class="label">Chứng từ đi kèm (nếu có)</label>
|
||||
<div class="control">
|
||||
<FileGallery
|
||||
v-bind="{
|
||||
row: entry,
|
||||
pagename: pagename,
|
||||
api: 'entryfile',
|
||||
info: false,
|
||||
}"
|
||||
></FileGallery>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-5 ml-3"
|
||||
v-if="!entry"
|
||||
>
|
||||
<button
|
||||
:class="['button is-primary has-text-white mr-2', isUpdating && 'is-loading']"
|
||||
@click="confirm()"
|
||||
>
|
||||
{{ $lang("confirm") }}
|
||||
</button>
|
||||
</div>
|
||||
<Modal
|
||||
@close="showContractModal = undefined"
|
||||
v-bind="showContractModal"
|
||||
v-if="showContractModal"
|
||||
></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
@confirm="update()"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12" v-if="entry">
|
||||
<div class="field">
|
||||
<label class="label">Chứng từ đi kèm (nếu có)</label>
|
||||
<div class="control">
|
||||
<FileGallery v-bind="{row: entry, pagename: pagename, api: 'entryfile', info: false}"></FileGallery>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 ml-3" v-if="!entry">
|
||||
<button
|
||||
:class="[
|
||||
'button is-primary has-text-white mr-2',
|
||||
isUpdating && 'is-loading'
|
||||
]"
|
||||
@click="confirm()">{{$lang('confirm')}}</button>
|
||||
</div>
|
||||
<Modal @close="showContractModal=undefined" v-bind="showContractModal" v-if="showContractModal"></Modal>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" @confirm="update()" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useStore } from '~/stores/index'
|
||||
import { useStore } from "~/stores/index";
|
||||
export default {
|
||||
setup() {
|
||||
const store = useStore()
|
||||
return {store}
|
||||
const store = useStore();
|
||||
return { store };
|
||||
},
|
||||
props: ['pagename', 'row', 'option'],
|
||||
props: ["pagename", "row", "option"],
|
||||
data() {
|
||||
return {
|
||||
record: {date: this.$dayjs().format('YYYY-MM-DD')},
|
||||
record: { date: this.$dayjs().format("YYYY-MM-DD") },
|
||||
errors: {},
|
||||
isUpdating: false,
|
||||
showmodal: undefined,
|
||||
showContractModal: undefined,
|
||||
entry: undefined
|
||||
}
|
||||
entry: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if(!this.option) return
|
||||
this.record.account = this.option.account
|
||||
this.record.type = this.option.type
|
||||
if (!this.option) return;
|
||||
this.record.account = this.option.account;
|
||||
this.record.type = this.option.type;
|
||||
},
|
||||
methods: {
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj
|
||||
this.record = this.$copy(this.record)
|
||||
this.record[attr] = obj;
|
||||
this.record = this.$copy(this.record);
|
||||
},
|
||||
checkError() {
|
||||
this.errors = {}
|
||||
if(this.$empty(this.record._account)) this.errors._account = 'Chưa chọn tài khoản'
|
||||
if(this.$empty(this.record._type)) this.errors._type = 'Chưa chọn loại hạch toán'
|
||||
if(this.$empty(this.record.amount)) this.errors.amount = 'Chưa nhập số tiền'
|
||||
else if(this.$formatNumber(this.record.amount)<=0) this.errors.amount = 'Số tiền phải > 0'
|
||||
if(this.$empty(this.record.content)) this.errors.content = 'Chưa nhập nội dung'
|
||||
if(Object.keys(this.errors).length>0) return true
|
||||
if(this.record._type.code==='DR' && (this.record._account.balance<this.$formatNumber(this.record.amount))) {
|
||||
this.errors._account = 'Số dư tài khoản không đủ để trích nợ'
|
||||
this.errors = {};
|
||||
if (this.$empty(this.record._account)) this.errors._account = "Chưa chọn tài khoản";
|
||||
if (this.$empty(this.record._type)) this.errors._type = "Chưa chọn loại hạch toán";
|
||||
if (this.$empty(this.record.amount)) this.errors.amount = "Chưa nhập số tiền";
|
||||
else if (this.$formatNumber(this.record.amount) <= 0) this.errors.amount = "Số tiền phải > 0";
|
||||
if (this.$empty(this.record.content)) this.errors.content = "Chưa nhập nội dung";
|
||||
if (Object.keys(this.errors).length > 0) return true;
|
||||
if (this.record._type.code === "DR" && this.record._account.balance < this.$formatNumber(this.record.amount)) {
|
||||
this.errors._account = "Số 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() {
|
||||
if(this.checkError()) return
|
||||
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10},
|
||||
title: this.$lang('confirm'), width: '500px', height: '100px'}
|
||||
if (this.checkError()) return;
|
||||
this.showmodal = {
|
||||
component: `dialog/Confirm`,
|
||||
vbind: { content: this.$lang("confirm-action"), duration: 10 },
|
||||
title: this.$lang("confirm"),
|
||||
width: "500px",
|
||||
height: "100px",
|
||||
};
|
||||
},
|
||||
async update() {
|
||||
this.isUpdating = true;
|
||||
@@ -179,28 +291,29 @@ export default {
|
||||
amount: this.record.amount,
|
||||
content: this.record.content,
|
||||
type: this.record._type.code,
|
||||
category: this.record._category ? this.record._category.id : 1, user: this.store.login.id,
|
||||
ref: this.row ? this.row.code : (!this.$empty(this.record.ref) ? this.record.ref.trim() : null),
|
||||
category: this.record._category ? this.record._category.id : 1,
|
||||
user: this.store.login.id,
|
||||
ref: this.row ? this.row.code : !this.$empty(this.record.ref) ? this.record.ref.trim() : null,
|
||||
customer: this.record.customer ? this.record.customer.id : null,
|
||||
product: this.record.product ? this.record.product.id : null,
|
||||
date: this.$empty(this.record.date) ? null : this.record.date
|
||||
}
|
||||
let rs1 = await this.$insertapi('accountentry', obj1, undefined, false)
|
||||
if(rs1==='error') return
|
||||
date: this.$empty(this.record.date) ? null : this.record.date,
|
||||
};
|
||||
let rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
|
||||
if (rs1 === "error") return;
|
||||
|
||||
if (this.record._category.id === 2) {
|
||||
const genDoc = await this.$generateDocument({
|
||||
doc_code: 'PHIEU_THU_TIEN_MAT',
|
||||
doc_code: "PHIEU_THU_TIEN_MAT",
|
||||
entry_id: rs1.id,
|
||||
output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}`
|
||||
output_filename: `PHIEU_THU_TIEN_MAT-${rs1.code}`,
|
||||
});
|
||||
|
||||
await this.$insertapi('file', {
|
||||
name: genDoc.data.pdf,
|
||||
user: this.store.login.id,
|
||||
type: 4,
|
||||
size: 1000,
|
||||
file: genDoc.data.pdf // or genDoc.data.pdf
|
||||
await this.$insertapi("file", {
|
||||
name: genDoc.data.pdf,
|
||||
user: this.store.login.id,
|
||||
type: 4,
|
||||
size: 1000,
|
||||
file: genDoc.data.pdf, // or genDoc.data.pdf
|
||||
});
|
||||
|
||||
this.showContractModal = {
|
||||
@@ -209,20 +322,27 @@ export default {
|
||||
width: "95%",
|
||||
height: "95vh",
|
||||
vbind: {
|
||||
directDocument: genDoc.data
|
||||
directDocument: genDoc.data,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.entry = rs1
|
||||
if(this.pagename) {
|
||||
let data = await this.$getdata('internalaccount', {code__in: [this.record._account.code]})
|
||||
this.$updatepage(this.pagename, data)
|
||||
|
||||
this.entry = rs1;
|
||||
if (this.pagename) {
|
||||
let data = await this.$getdata("internalaccount", {
|
||||
code__in: [this.record._account.code],
|
||||
});
|
||||
this.$updatepage(this.pagename, data);
|
||||
}
|
||||
this.isUpdating = false;
|
||||
this.$emit('modalevent', {name: 'entry', data: rs1})
|
||||
this.$dialog(`Hạch toán <b>${this.record._type.name}</b> số tiền <b>${this.$numtoString(this.record.amount)}</b> vào tài khoản <b>${this.record._account.code}</b> thành công.`, 'Thành công', 'Success', 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
this.$emit("modalevent", { name: "entry", data: rs1 });
|
||||
this.$dialog(
|
||||
`Hạch toán <b>${this.record._type.name}</b> số tiền <b>${this.$numtoString(this.record.amount)}</b> vào tài khoản <b>${this.record._account.code}</b> thành công.`,
|
||||
"Thành công",
|
||||
"Success",
|
||||
10,
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,247 +1,326 @@
|
||||
<template>
|
||||
<div v-if="record" id="printable">
|
||||
<Caption v-bind="{ title: $lang('info') }" />
|
||||
<div class="columns is-multiline is-2 m-0">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('code') }}:</label>
|
||||
<div class="control">
|
||||
<span>{{ record.code }}</span>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Tài khoản:</label>
|
||||
<div class="control">
|
||||
<span>{{ record.account__code }}</span>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.type">{{ errors.type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Ngày hạch toán:</label>
|
||||
<div class="control">
|
||||
{{ $dayjs(record.date).format('DD/MM/YYYY') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('amount-only') }}:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.amount) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Thu / chi:</label>
|
||||
<div class="control">
|
||||
{{ record.type__name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Dư trước:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.balance_before) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Dư sau:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.balance_after) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<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
|
||||
v-if="record"
|
||||
id="printable"
|
||||
>
|
||||
<Caption v-bind="{ title: $lang('info') }" />
|
||||
<div class="columns is-multiline is-2 m-0">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("code") }}:</label>
|
||||
<div class="control">
|
||||
<span>{{ record.code }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.type"
|
||||
>
|
||||
{{ errors.type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Tài khoản:</label>
|
||||
<div class="control">
|
||||
<span>{{ record.account__code }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.type"
|
||||
>
|
||||
{{ errors.type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Ngày hạch toán:</label>
|
||||
<div class="control">
|
||||
{{ $dayjs(record.date).format("DD/MM/YYYY") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("amount-only") }}:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.amount) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Thu / chi:</label>
|
||||
<div class="control">
|
||||
{{ record.type__name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Dư trước:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.balance_before) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Dư sau:</label>
|
||||
<div class="control">
|
||||
{{ $numtoString(record.balance_after) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<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="field">
|
||||
<label class="label">Người hạch toán:</label>
|
||||
<div class="control">
|
||||
{{ `${record.inputer__fullname}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('time') }}:</label>
|
||||
<div class="control">
|
||||
{{ `${$dayjs(record.create_time).format('DD/MM/YYYY')}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Ref:</label>
|
||||
<div class="control">
|
||||
{{ `${record.ref || '/'}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('content') }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.content}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Người hạch toán:</label>
|
||||
<div class="control">
|
||||
{{ `${record.inputer__fullname}` }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHẦN THÔNG TIN PHÂN BỔ -->
|
||||
<Caption v-bind="{ title: 'Thông tin phân bổ' }" />
|
||||
<!-- BẢNG CHI TIẾT PHÂN BỔ -->
|
||||
<div v-if="record.allocation_detail && record.allocation_detail.length > 0" class="mt-4">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
|
||||
<thead>
|
||||
<tr class="">
|
||||
<th class="has-background-primary has-text-white has-text-centered">STT</th>
|
||||
<th class="has-background-primary has-text-white has-text-centered">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 class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("time") }}:</label>
|
||||
<div class="control">
|
||||
{{ `${$dayjs(record.create_time).format("DD/MM/YYYY")}` }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="notification is-info is-light mt-4">
|
||||
<p class="has-text-centered">Chưa có dữ liệu phân bổ cho bút toán này.</p>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Ref:</label>
|
||||
<div class="control">
|
||||
{{ `${record.ref || "/"}` }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Caption class="mt-5 " v-bind="{ title: 'Chứng từ' }"></Caption>
|
||||
<FileGallery v-bind="{ row: record, api: 'entryfile' }"></FileGallery>
|
||||
<div class="mt-5" id="ignore">
|
||||
<button class="button is-primary has-text-white mr-2" @click="$exportpdf('printable', record.code, 'a4', 'landscape')">{{ $lang('print') }}</button>
|
||||
<button v-if="record.category === 2" class="button is-light" @click="viewPhieuThuTienMat">Xem phiếu thu</button>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("content") }}:</label>
|
||||
<div class="control">
|
||||
{{ `${record.content}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHẦN THÔNG TIN PHÂN BỔ -->
|
||||
<Caption v-bind="{ title: 'Thông tin phân bổ' }" />
|
||||
<!-- BẢNG CHI TIẾT PHÂN BỔ -->
|
||||
<div
|
||||
v-if="record.allocation_detail && record.allocation_detail.length > 0"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-striped is-hoverable is-bordered">
|
||||
<thead>
|
||||
<tr class="">
|
||||
<th class="has-background-primary has-text-white has-text-centered">STT</th>
|
||||
<th class="has-background-primary has-text-white has-text-centered">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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['row'],
|
||||
data() {
|
||||
return {
|
||||
errors: {},
|
||||
record: undefined
|
||||
}
|
||||
props: ["row"],
|
||||
data() {
|
||||
return {
|
||||
errors: {},
|
||||
record: undefined,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.record = await this.$getdata("internalentry", { code: this.row.code }, undefined, true);
|
||||
},
|
||||
computed: {
|
||||
// Tính tổng số tiền đã phân bổ
|
||||
totalAllocated() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0;
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.amount || 0), 0);
|
||||
},
|
||||
async created() {
|
||||
this.record = await this.$getdata('internalentry', { code: this.row.code }, undefined, true)
|
||||
// Tính tổng gốc
|
||||
totalPrincipal() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0;
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.principal || 0), 0);
|
||||
},
|
||||
computed: {
|
||||
|
||||
// Tính tổng số tiền đã phân bổ
|
||||
totalAllocated() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.amount || 0), 0)
|
||||
},
|
||||
// Tính tổng gốc
|
||||
totalPrincipal() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.principal || 0), 0)
|
||||
},
|
||||
// Tính tổng phạt
|
||||
totalPenalty() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty || 0), 0)
|
||||
},
|
||||
// Tính tổng phạt đã giảm
|
||||
totalPenaltyReduce() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty_reduce || 0), 0)
|
||||
}
|
||||
// Tính tổng phạt
|
||||
totalPenalty() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0;
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty || 0), 0);
|
||||
},
|
||||
methods: {
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj
|
||||
this.record = this.$copy(this.record)
|
||||
if (attr === '_type') this.category = obj.category__code
|
||||
},
|
||||
viewPhieuThuTienMat() {
|
||||
const url = `${this.$getpath()}static/contract/PHIEU_THU_TIEN_MAT-${this.record.code}.pdf`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tính tổng phạt đã giảm
|
||||
totalPenaltyReduce() {
|
||||
if (!this.record || !this.record.allocation_detail) return 0;
|
||||
return this.record.allocation_detail.reduce((sum, item) => sum + (item.penalty_reduce || 0), 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj;
|
||||
this.record = this.$copy(this.record);
|
||||
if (attr === "_type") this.category = obj.category__code;
|
||||
},
|
||||
viewPhieuThuTienMat() {
|
||||
const url = `${this.$getpath()}static/contract/PHIEU_THU_TIEN_MAT-${this.record.code}.pdf`;
|
||||
window.open(url, "_blank");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.column {
|
||||
padding-inline: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,104 +1,191 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{$lang('source-account')}}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{api:'internalaccount', field:'label', column:['label'], first: true, optionid: row.id}"
|
||||
@option="selected('_source', $event)" v-if="!record.id"></SearchBox>
|
||||
<span v-else>{{record.account__code}}</span>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.source">{{ errors.source }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{$lang('dest-account')}}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="vbind" @option="selected('_target', $event)" v-if="vbind"></SearchBox>
|
||||
<span v-else>{{record.account__code}}</span>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.target">{{ errors.target }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('amount-only') }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<InputNumber v-bind="{record: record, attr: 'amount', placeholder: ''}" @number="selected('amount', $event)"></InputNumber>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("source-account") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'internalaccount',
|
||||
field: 'label',
|
||||
column: ['label'],
|
||||
first: true,
|
||||
optionid: row.id,
|
||||
}"
|
||||
@option="selected('_source', $event)"
|
||||
v-if="!record.id"
|
||||
></SearchBox>
|
||||
<span v-else>{{ record.account__code }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.source"
|
||||
>
|
||||
{{ errors.source }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("dest-account") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="vbind"
|
||||
@option="selected('_target', $event)"
|
||||
v-if="vbind"
|
||||
></SearchBox>
|
||||
<span v-else>{{ record.account__code }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.target"
|
||||
>
|
||||
{{ errors.target }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("amount-only") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<InputNumber
|
||||
v-bind="{ record: record, attr: 'amount', placeholder: '' }"
|
||||
@number="selected('amount', $event)"
|
||||
></InputNumber>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.amount"
|
||||
>
|
||||
{{ errors.amount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang("content") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
class="textarea"
|
||||
rows="2"
|
||||
v-model="record.content"
|
||||
></textarea>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.content"
|
||||
>
|
||||
{{ errors.content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.amount">{{errors.amount}}</p>
|
||||
<div class="mt-5">
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="confirm()"
|
||||
>
|
||||
{{ $lang("confirm") }}
|
||||
</button>
|
||||
</div>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
@confirm="update()"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ $lang('content') }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" rows="2" v-model="record.content"></textarea>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.content">{{errors.content}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<button class="button is-primary has-text-white" @click="confirm()">{{ $lang('confirm') }}</button>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" @confirm="update()" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['pagename', 'row'],
|
||||
props: ["pagename", "row"],
|
||||
data() {
|
||||
return {
|
||||
record: {},
|
||||
errors: {},
|
||||
showmodal: undefined,
|
||||
vbind: undefined
|
||||
}
|
||||
vbind: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj
|
||||
this.record = this.$copy(this.record)
|
||||
if(attr==='_source') {
|
||||
let currency = obj? obj.currency : undefined
|
||||
this.vbind = undefined
|
||||
setTimeout(()=>this.vbind = {api:'internalaccount', field:'label', column:['label'], first: true, filter: {currency: currency}})
|
||||
this.record[attr] = obj;
|
||||
this.record = this.$copy(this.record);
|
||||
if (attr === "_source") {
|
||||
let currency = obj ? obj.currency : undefined;
|
||||
this.vbind = undefined;
|
||||
setTimeout(
|
||||
() =>
|
||||
(this.vbind = {
|
||||
api: "internalaccount",
|
||||
field: "label",
|
||||
column: ["label"],
|
||||
first: true,
|
||||
filter: { currency: currency },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
checkError() {
|
||||
this.errors = {}
|
||||
if(this.$empty(this.record._source)) this.errors.source = 'Chưa chọn tài khoản nguồn'
|
||||
if(this.$empty(this.record._target)) this.errors.target = 'Chưa chọn tài khoản đích'
|
||||
if(Object.keys(this.errors).length===0) {
|
||||
if(this.record._source.id===this.record._target.id) this.errors.target = 'Tài khoản nguồn phải khác tài khoản đích'
|
||||
this.errors = {};
|
||||
if (this.$empty(this.record._source)) this.errors.source = "Chưa chọn tài khoản nguồn";
|
||||
if (this.$empty(this.record._target)) this.errors.target = "Chưa chọn tài khoản đích";
|
||||
if (Object.keys(this.errors).length === 0) {
|
||||
if (this.record._source.id === this.record._target.id)
|
||||
this.errors.target = "Tài khoản nguồn phải khác tài khoản đích";
|
||||
}
|
||||
if(this.$empty(this.record.amount)) this.errors.amount = 'Chưa nhập số tiền'
|
||||
else if(this.$formatNumber(this.record.amount)<=0) this.errors.amount = 'Số tiền phải > 0'
|
||||
else if(this.record._source.balance<this.$formatNumber(this.record.amount)) this.errors.source = 'Tài khoản nguồn không đủ số dư để điều chuyển'
|
||||
if(this.$empty(this.record.content)) this.errors.content = 'Chưa nhập nội dung'
|
||||
return Object.keys(this.errors).length>0
|
||||
if (this.$empty(this.record.amount)) this.errors.amount = "Chưa nhập số tiền";
|
||||
else if (this.$formatNumber(this.record.amount) <= 0) this.errors.amount = "Số tiền phải > 0";
|
||||
else if (this.record._source.balance < this.$formatNumber(this.record.amount))
|
||||
this.errors.source = "Tài khoản nguồn không đủ số dư để điều chuyển";
|
||||
if (this.$empty(this.record.content)) this.errors.content = "Chưa nhập nội dung";
|
||||
return Object.keys(this.errors).length > 0;
|
||||
},
|
||||
confirm() {
|
||||
if(this.checkError()) return
|
||||
this.showmodal = {component: `dialog/Confirm`,vbind: {content: this.$lang('confirm-action'), duration: 10},
|
||||
title: this.$lang('confirm'), width: '500px', height: '100px'}
|
||||
if (this.checkError()) return;
|
||||
this.showmodal = {
|
||||
component: `dialog/Confirm`,
|
||||
vbind: { content: this.$lang("confirm-action"), duration: 10 },
|
||||
title: this.$lang("confirm"),
|
||||
width: "500px",
|
||||
height: "100px",
|
||||
};
|
||||
},
|
||||
async update() {
|
||||
let content = `${this.record.content} (${this.record._source.code} -> ${this.record._target.code})`
|
||||
let obj1 = {code: this.record._source.code, amount: this.record.amount, content: content, type: 'DR', category: 2, user: this.$store.login.id}
|
||||
let rs1 = await this.$insertapi('accountentry', obj1, undefined, false)
|
||||
if(rs1==='error') return
|
||||
let obj2 = {code: this.record._target.code, amount: this.record.amount, content: content, type: 'CR', category: 2, user: this.$store.login.id}
|
||||
let rs2 = await this.$insertapi('accountentry', obj2, undefined, false)
|
||||
if(rs2==='error') return
|
||||
let data = await this.$getdata('internalaccount', {code__in: [this.record._source.code, this.record._target.code]})
|
||||
this.$updatepage(this.pagename, data)
|
||||
this.$dialog(`Điều chuyển vốn <b>${this.$numtoString(this.record.amount)}</b> từ <b>${this.record._source.code}</b> tới <b>${this.record._target.code}</b> thành công.`, 'Thành công', 'Success', 10)
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
let content = `${this.record.content} (${this.record._source.code} -> ${this.record._target.code})`;
|
||||
let obj1 = {
|
||||
code: this.record._source.code,
|
||||
amount: this.record.amount,
|
||||
content: content,
|
||||
type: "DR",
|
||||
category: 2,
|
||||
user: this.$store.login.id,
|
||||
};
|
||||
let rs1 = await this.$insertapi("accountentry", obj1, undefined, false);
|
||||
if (rs1 === "error") return;
|
||||
let obj2 = {
|
||||
code: this.record._target.code,
|
||||
amount: this.record.amount,
|
||||
content: content,
|
||||
type: "CR",
|
||||
category: 2,
|
||||
user: this.$store.login.id,
|
||||
};
|
||||
let rs2 = await this.$insertapi("accountentry", obj2, undefined, false);
|
||||
if (rs2 === "error") return;
|
||||
let data = await this.$getdata("internalaccount", {
|
||||
code__in: [this.record._source.code, this.record._target.code],
|
||||
});
|
||||
this.$updatepage(this.pagename, data);
|
||||
this.$dialog(
|
||||
`Điều chuyển vốn <b>${this.$numtoString(this.record.amount)}</b> từ <b>${this.record._source.code}</b> tới <b>${this.record._target.code}</b> thành công.`,
|
||||
"Thành công",
|
||||
"Success",
|
||||
10,
|
||||
);
|
||||
this.$emit("close");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
</div>
|
||||
<div class="column is-1"></div>
|
||||
</div>
|
||||
<div class="columns" v-for="(invoice, index) in invoices">
|
||||
<div
|
||||
class="columns"
|
||||
v-for="(invoice, index) in invoices"
|
||||
>
|
||||
<div class="column">
|
||||
<input
|
||||
class="input has-text-centered has-text-weight-bold has-text-left"
|
||||
@@ -32,7 +35,12 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<p v-if="invoice.errorLink" class="help is-danger">Link phải bắt đầu bằng https</p>
|
||||
<p
|
||||
v-if="invoice.errorLink"
|
||||
class="help is-danger"
|
||||
>
|
||||
Link phải bắt đầu bằng https
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<input
|
||||
@@ -49,7 +57,12 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<p v-if="invoice.errorCode" class="help is-danger">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 class="column is-2">
|
||||
<input
|
||||
@@ -66,7 +79,12 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
<p v-if="invoice.errorAmount" class="help is-danger">Số tiền không được bỏ trống</p>
|
||||
<p
|
||||
v-if="invoice.errorAmount"
|
||||
class="help is-danger"
|
||||
>
|
||||
Số tiền không được bỏ trống
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<select
|
||||
@@ -84,22 +102,44 @@
|
||||
<option value="principal">Tiền gốc</option>
|
||||
<option value="interest">Tiền lãi</option>
|
||||
</select>
|
||||
<p v-if="invoice.errorType" class="help is-danger">Loại tiền không được bỏ trống</p>
|
||||
<p
|
||||
v-if="invoice.errorType"
|
||||
class="help is-danger"
|
||||
>
|
||||
Loại tiền không được bỏ trống
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow is-1">
|
||||
<label class="label" v-if="i === 0"> </label>
|
||||
<div class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small" style="height: 40px">
|
||||
<button class="button is-dark" @click="handlerRemove(index)">
|
||||
<label
|
||||
class="label"
|
||||
v-if="i === 0"
|
||||
> </label
|
||||
>
|
||||
<div
|
||||
class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small"
|
||||
style="height: 40px"
|
||||
>
|
||||
<button
|
||||
class="button is-dark"
|
||||
@click="handlerRemove(index)"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-dark" @click="add()">
|
||||
<button
|
||||
class="button is-dark"
|
||||
@click="add()"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<a class="button is-dark" :href="invoice.link" target="_blank">
|
||||
<a
|
||||
class="button is-dark"
|
||||
:href="invoice.link"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||
</span>
|
||||
@@ -108,8 +148,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 buttons is-right">
|
||||
<button class="button" @click="emit('close')">{{ isVietnamese ? "Hủy" : "Cancel" }}</button>
|
||||
<button class="button is-primary" @click="handlerUpdate">{{ isVietnamese ? "Lưu lại" : "Save" }}</button>
|
||||
<button
|
||||
class="button"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ isVietnamese ? "Hủy" : "Cancel" }}
|
||||
</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
@click="handlerUpdate"
|
||||
>
|
||||
{{ isVietnamese ? "Lưu lại" : "Save" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,7 +207,11 @@ if (resInvoice.length) {
|
||||
errorAmount: false,
|
||||
errorType: false,
|
||||
};
|
||||
const formatData = resInvoice.map((invoice) => ({ ...invoice, amount: $formatNumber(invoice.amount), ...error }));
|
||||
const formatData = resInvoice.map((invoice) => ({
|
||||
...invoice,
|
||||
amount: $formatNumber(invoice.amount),
|
||||
...error,
|
||||
}));
|
||||
|
||||
invoices.value = formatData;
|
||||
}
|
||||
|
||||
@@ -17,5 +17,4 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(["remove"]);
|
||||
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,22 @@
|
||||
<template>
|
||||
<div :id="docid">
|
||||
<div :id="docid1">
|
||||
<Caption v-bind="{ title: isVietnamese? 'Thanh toán' : 'Payment', type: 'has-text-warning' }"></Caption>
|
||||
<Caption
|
||||
v-bind="{
|
||||
title: isVietnamese ? 'Thanh toán' : 'Payment',
|
||||
type: 'has-text-warning',
|
||||
}"
|
||||
></Caption>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div class="column is-4 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("loan_code")[lang] }}</label>
|
||||
<div class="control">
|
||||
<span class="hyperlink" @click="$copyToClipboard(record.code)">{{ record?.code || "/" }}</span>
|
||||
<span
|
||||
class="hyperlink"
|
||||
@click="$copyToClipboard(record.code)"
|
||||
>{{ record?.code || "/" }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +63,7 @@
|
||||
<div class="control">
|
||||
<span>{{ record?.commission ? $numtoString(record.commission) : "/" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-5 pb-1 px-0">
|
||||
@@ -76,14 +85,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-2 border-bottom"></div> -->
|
||||
<div class="buttons mt-5" id="ignore">
|
||||
<button class="button is-primary has-text-white mt-2" @click="handleUpdate()">
|
||||
<div
|
||||
class="buttons mt-5"
|
||||
id="ignore"
|
||||
>
|
||||
<button
|
||||
class="button is-primary has-text-white mt-2"
|
||||
@click="handleUpdate()"
|
||||
>
|
||||
{{ dataLang && findFieldName("update")[lang] }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
<template>
|
||||
<div :id="docid">
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="has-text-centered mt-5 mb-5" style="min-height: 500px">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="has-text-centered mt-5 mb-5"
|
||||
style="min-height: 500px"
|
||||
>
|
||||
<button class="button is-primary is-loading is-large"></button>
|
||||
<p class="mt-4 has-text-primary has-text-weight-semibold">
|
||||
{{ isVietnamese ? 'Đang tải hợp đồng...' : 'Loading contracts...' }}
|
||||
{{ isVietnamese ? "Đang tải hợp đồng..." : "Loading contracts..." }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- No contract state -->
|
||||
<div v-else-if="!hasContracts" class="has-text-centered mt-5 mb-5" style="min-height: 500px">
|
||||
<div
|
||||
v-else-if="!hasContracts"
|
||||
class="has-text-centered mt-5 mb-5"
|
||||
style="min-height: 500px"
|
||||
>
|
||||
<article class="message is-primary">
|
||||
<div class="message-body" style="font-size: 17px; text-align: left; color: black">
|
||||
<div
|
||||
class="message-body"
|
||||
style="font-size: 17px; text-align: left; color: black"
|
||||
>
|
||||
{{
|
||||
isVietnamese
|
||||
? "Chưa có hợp đồng. Vui lòng tạo giao dịch và hợp đồng trước."
|
||||
@@ -24,10 +35,22 @@
|
||||
<!-- Contracts list -->
|
||||
<template v-else>
|
||||
<!-- Tabs khi 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">
|
||||
<li class="tabs-item" style="border: none" v-for="(contract, index) in contractsList" :key="index"
|
||||
:class="{ 'bg-primary has-text-white': activeContractIndex === index }" @click="switchContract(index)">
|
||||
<li
|
||||
class="tabs-item"
|
||||
style="border: none"
|
||||
v-for="(contract, index) in contractsList"
|
||||
:key="index"
|
||||
:class="{
|
||||
'bg-primary has-text-white': activeContractIndex === index,
|
||||
}"
|
||||
@click="switchContract(index)"
|
||||
>
|
||||
<a class="tabs-link">
|
||||
<span>{{ contract.document[0]?.name || contract.document[0]?.en || `Contract ${index + 1}` }}</span>
|
||||
</a>
|
||||
@@ -38,23 +61,42 @@
|
||||
<!-- Contract content -->
|
||||
<div v-if="currentContract && pdfFileUrl && hasValidDocument">
|
||||
<div class="contract-content mt-2">
|
||||
<iframe :src="`https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfFileUrl}`" width="100%"
|
||||
height="90vh" scrolling="no" style="border: none; height: 75vh; top: 0; left: 0; right: 0; bottom: 0">
|
||||
<iframe
|
||||
:src="`https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfFileUrl}`"
|
||||
width="100%"
|
||||
height="90vh"
|
||||
scrolling="no"
|
||||
style="border: none; height: 75vh; top: 0; left: 0; right: 0; bottom: 0"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download buttons -->
|
||||
<div class="mt-4" id="ignore">
|
||||
<button v-if="hasValidDocument" class="button is-primary has-text-white mr-4" @click="downloadDocx">
|
||||
<div
|
||||
class="mt-4"
|
||||
id="ignore"
|
||||
>
|
||||
<button
|
||||
v-if="hasValidDocument"
|
||||
class="button is-primary has-text-white mr-4"
|
||||
@click="downloadDocx"
|
||||
>
|
||||
{{ isVietnamese ? "Tải file docx" : "Download contract as docx" }}
|
||||
</button>
|
||||
|
||||
<button v-if="hasValidDocument" class="button is-primary has-text-white mr-4" @click="downloadPdf">
|
||||
<button
|
||||
v-if="hasValidDocument"
|
||||
class="button is-primary has-text-white mr-4"
|
||||
@click="downloadPdf"
|
||||
>
|
||||
{{ isVietnamese ? "Tải file pdf" : "Download contract as pdf" }}
|
||||
</button>
|
||||
|
||||
<p v-if="contractError" class="has-text-danger mt-2">
|
||||
<p
|
||||
v-if="contractError"
|
||||
class="has-text-danger mt-2"
|
||||
>
|
||||
{{ contractError }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -73,16 +115,16 @@ export default {
|
||||
props: {
|
||||
contractId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
directDocument: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["contractCreated", "update", "close", "dataevent"],
|
||||
data() {
|
||||
@@ -107,10 +149,12 @@ export default {
|
||||
},
|
||||
hasValidDocument() {
|
||||
if (!this.currentContract) return false;
|
||||
return this.currentContract.document &&
|
||||
return (
|
||||
this.currentContract.document &&
|
||||
this.currentContract.document.length > 0 &&
|
||||
this.currentContract.document[0]?.pdf;
|
||||
}
|
||||
this.currentContract.document[0]?.pdf
|
||||
);
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
@@ -118,11 +162,9 @@ export default {
|
||||
this.contractError = null;
|
||||
|
||||
if (this.directDocument) {
|
||||
this.contractsList = [
|
||||
{ document: [this.directDocument] }
|
||||
];
|
||||
this.contractsList = [{ document: [this.directDocument] }];
|
||||
this.updatePdfUrl(0);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
let contracts = [];
|
||||
@@ -130,31 +172,22 @@ export default {
|
||||
|
||||
if (this.contractId) {
|
||||
fetchParams = { id: this.contractId };
|
||||
}
|
||||
else if (this.row?.id) {
|
||||
} else if (this.row?.id) {
|
||||
fetchParams = { transaction: this.row.id };
|
||||
}
|
||||
|
||||
if (!fetchParams) {
|
||||
throw new Error(
|
||||
this.isVietnamese
|
||||
? 'Không có ID hợp đồng hoặc transaction để tải.'
|
||||
: 'No contract ID or transaction provided to load.'
|
||||
? "Không có ID hợp đồng hoặc transaction để tải."
|
||||
: "No contract ID or transaction provided to load.",
|
||||
);
|
||||
}
|
||||
|
||||
contracts = await this.$getdata(
|
||||
'contract',
|
||||
fetchParams,
|
||||
undefined
|
||||
);
|
||||
contracts = await this.$getdata("contract", fetchParams, undefined);
|
||||
|
||||
if (!contracts || contracts.length === 0) {
|
||||
throw new Error(
|
||||
this.isVietnamese
|
||||
? 'Không tìm thấy hợp đồng.'
|
||||
: 'Contract not found.'
|
||||
);
|
||||
throw new Error(this.isVietnamese ? "Không tìm thấy hợp đồng." : "Contract not found.");
|
||||
}
|
||||
|
||||
this.contractsList = contracts;
|
||||
@@ -164,12 +197,9 @@ export default {
|
||||
this.updatePdfUrl(this.activeContractIndex);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading contracts:', error);
|
||||
this.contractError = error.message || (
|
||||
this.isVietnamese
|
||||
? 'Lỗi khi tải danh sách hợp đồng.'
|
||||
: 'Error loading contracts list.'
|
||||
);
|
||||
console.error("Error loading contracts:", error);
|
||||
this.contractError =
|
||||
error.message || (this.isVietnamese ? "Lỗi khi tải danh sách hợp đồng." : "Error loading contracts list.");
|
||||
this.contractsList = [];
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
@@ -192,10 +222,7 @@ export default {
|
||||
|
||||
downloadDocx() {
|
||||
if (!this.hasValidDocument) {
|
||||
this.$snackbar(
|
||||
this.isVietnamese ? "Không có file để tải" : "No file to download",
|
||||
{ type: 'is-warning' }
|
||||
);
|
||||
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,10 +233,7 @@ export default {
|
||||
|
||||
downloadPdf() {
|
||||
if (!this.hasValidDocument) {
|
||||
this.$snackbar(
|
||||
this.isVietnamese ? "Không có file để tải" : "No file to download",
|
||||
{ type: 'is-warning' }
|
||||
);
|
||||
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -259,4 +283,4 @@ export default {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Loading Overlay -->
|
||||
<div v-if="isLoading" class="loading-overlay">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-overlay"
|
||||
>
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<!-- View Mode Toggle -->
|
||||
<div class="mb-5 pb-3" style="border-bottom: 2px solid #e8e8e8;">
|
||||
<div class="buttons has-addons ">
|
||||
<button @click="viewMode = 'list'" :class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']">
|
||||
<div
|
||||
class="mb-5 pb-3"
|
||||
style="border-bottom: 2px solid #e8e8e8"
|
||||
>
|
||||
<div class="buttons has-addons">
|
||||
<button
|
||||
@click="viewMode = 'list'"
|
||||
:class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']"
|
||||
>
|
||||
Danh sách
|
||||
</button>
|
||||
<button @click="viewMode = 'gallery'" :class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']">
|
||||
<button
|
||||
@click="viewMode = 'gallery'"
|
||||
:class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']"
|
||||
>
|
||||
Thư viện
|
||||
</button>
|
||||
</div>
|
||||
@@ -19,8 +31,11 @@
|
||||
|
||||
<!-- Phase Document Types List -->
|
||||
<div v-if="phasedoctypes && phasedoctypes.length > 0">
|
||||
<div v-for="doctype in phasedoctypes" :key="doctype.id" class="mb-6">
|
||||
|
||||
<div
|
||||
v-for="doctype in phasedoctypes"
|
||||
:key="doctype.id"
|
||||
class="mb-6"
|
||||
>
|
||||
<!-- Document Type Header with Upload Button -->
|
||||
<div class="level is-mobile mb-4">
|
||||
<div class="level-left">
|
||||
@@ -34,9 +49,9 @@
|
||||
<div class="level-item">
|
||||
<FileUpload
|
||||
v-if="$getEditRights()"
|
||||
:type="['file', 'image', 'pdf']"
|
||||
@files="(files) => handleUpload(files, doctype.doctype)"
|
||||
position="right"
|
||||
:type="['file', 'image', 'pdf']"
|
||||
@files="(files) => handleUpload(files, doctype.doctype)"
|
||||
position="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,112 +60,195 @@
|
||||
<!-- List View -->
|
||||
<div v-if="viewMode === 'list'">
|
||||
<div v-if="getFilesByDocType(doctype.doctype).length > 0">
|
||||
<div v-for="file in getFilesByDocType(doctype.doctype)" :key="file.id"
|
||||
<div
|
||||
v-for="file in getFilesByDocType(doctype.doctype)"
|
||||
:key="file.id"
|
||||
class="is-flex is-justify-content-space-between is-align-items-center py-3 px-4 has-background-warning has-text-white"
|
||||
style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer;"
|
||||
style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer"
|
||||
@mouseenter="$event.currentTarget.style.opacity = '1'"
|
||||
@mouseleave="$event.currentTarget.style.opacity = '0.95'">
|
||||
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<p class="is-size-7 has-text-weight-semibold has-text-white mb-1" style="word-break: break-word;">
|
||||
@mouseleave="$event.currentTarget.style.opacity = '0.95'"
|
||||
>
|
||||
<div style="flex: 1; min-width: 0">
|
||||
<p
|
||||
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
||||
style="word-break: break-word"
|
||||
>
|
||||
{{ file.name || file.file__name }}
|
||||
</p>
|
||||
<p class="is-size-7 has-text-white-bis">
|
||||
{{ $formatFileSize(file.file__size) }} • {{ $dayjs(file.create_time).format("DD/MM/YYYY HH:mm") }}
|
||||
{{ $formatFileSize(file.file__size) }} •
|
||||
{{ $dayjs(file.create_time).format("DD/MM/YYYY HH:mm") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="buttons are-small ml-3">
|
||||
<button @click="viewFile(file)" class="button has-background-white has-text-primary ">
|
||||
<button
|
||||
@click="viewFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{
|
||||
name: 'view.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"></SvgIcon>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'view.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button @click="downloadFile(file)" class="button has-background-white has-text-primary ">
|
||||
<button
|
||||
@click="downloadFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{
|
||||
name: 'download.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"></SvgIcon>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'download.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button @click="deleteFile(file.id)" class="button has-background-white has-text-danger ">
|
||||
<button
|
||||
@click="deleteFile(file.id)"
|
||||
class="button has-background-white has-text-danger"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{
|
||||
name: 'bin.svg',
|
||||
type: 'danger',
|
||||
size: 18,
|
||||
}"></SvgIcon>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'bin.svg',
|
||||
type: 'danger',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="has-text-grey-light is-size-7 has-text-centered py-5">
|
||||
<div
|
||||
v-else
|
||||
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
||||
>
|
||||
Chưa có file nào
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery View -->
|
||||
<div v-if="viewMode === 'gallery'">
|
||||
<div v-if="getFilesByDocType(doctype.doctype).length > 0" class="columns is-multiline is-variable is-2">
|
||||
<div v-for="file in getFilesByDocType(doctype.doctype)" :key="file.id"
|
||||
class="column is-half-tablet is-one-third-desktop">
|
||||
<div class="has-background-warning has-text-white"
|
||||
style="border-radius: 6px; overflow: hidden; height: 100%; display: flex; flex-direction: column; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);"
|
||||
@mouseenter="$event.currentTarget.style.transform = 'translateY(-4px)'; $event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)'"
|
||||
@mouseleave="$event.currentTarget.style.transform = 'translateY(0)'; $event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)'">
|
||||
|
||||
<div
|
||||
v-if="getFilesByDocType(doctype.doctype).length > 0"
|
||||
class="columns is-multiline is-variable is-2"
|
||||
>
|
||||
<div
|
||||
v-for="file in getFilesByDocType(doctype.doctype)"
|
||||
:key="file.id"
|
||||
class="column is-half-tablet is-one-third-desktop"
|
||||
>
|
||||
<div
|
||||
class="has-background-warning has-text-white"
|
||||
style="
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);
|
||||
"
|
||||
@mouseenter="
|
||||
$event.currentTarget.style.transform = 'translateY(-4px)';
|
||||
$event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)';
|
||||
"
|
||||
@mouseleave="
|
||||
$event.currentTarget.style.transform = 'translateY(0)';
|
||||
$event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)';
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="flex: 1; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(255, 255, 255, 0.1); min-height: 140px;">
|
||||
<div v-if="isImage(file.file__name)"
|
||||
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
|
||||
<img :src="`${$getpath()}static/files/${file.file__file}`" :alt="file.file__name"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
min-height: 140px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="isImage(file.file__name)"
|
||||
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center"
|
||||
>
|
||||
<img
|
||||
:src="`${$getpath()}static/files/${file.file__file}`"
|
||||
:alt="file.file__name"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="has-text-white-ter" style="font-size: 48px; line-height: 1;">
|
||||
<div
|
||||
v-else
|
||||
class="has-text-white-ter"
|
||||
style="font-size: 48px; line-height: 1"
|
||||
>
|
||||
FILE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px 16px;">
|
||||
<p class="is-size-7 has-text-weight-semibold has-text-white mb-1" :title="file.file__name"
|
||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<div style="padding: 12px 16px">
|
||||
<p
|
||||
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
||||
:title="file.file__name"
|
||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
|
||||
>
|
||||
{{ file.file__name }}
|
||||
</p>
|
||||
<p class="is-size-7 has-text-white-bis mb-3">{{ $formatFileSize(file.file__size) }}</p>
|
||||
<p class="is-size-7 has-text-white-bis mb-3">
|
||||
{{ $formatFileSize(file.file__size) }}
|
||||
</p>
|
||||
|
||||
<div class="buttons are-small is-centered">
|
||||
<button @click="viewFile(file)" class="button has-background-white has-text-primary ">
|
||||
<button
|
||||
@click="viewFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{
|
||||
name: 'view.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"></SvgIcon>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'view.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button @click="downloadFile(file)" class="button has-background-white has-text-primary ">
|
||||
<button
|
||||
@click="downloadFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{
|
||||
name: 'download.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"></SvgIcon>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'download.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button @click="deleteFile(file.id)" class="button has-background-white has-text-danger ">
|
||||
<button
|
||||
@click="deleteFile(file.id)"
|
||||
class="button has-background-white has-text-danger"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{
|
||||
name: 'bin.svg',
|
||||
type: 'danger',
|
||||
size: 18,
|
||||
}"></SvgIcon>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'bin.svg',
|
||||
type: 'danger',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -158,7 +256,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="has-text-grey-light is-size-7 has-text-centered py-5">
|
||||
<div
|
||||
v-else
|
||||
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
||||
>
|
||||
Chưa có file nào
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,11 +267,19 @@
|
||||
</div>
|
||||
|
||||
<!-- If no phase doctypes -->
|
||||
<div v-else-if="!isLoading" class="has-text-centered py-6">
|
||||
<div
|
||||
v-else-if="!isLoading"
|
||||
class="has-text-centered py-6"
|
||||
>
|
||||
<p class="has-text-grey-light is-size-7">Chưa có loại tài liệu được định nghĩa cho giai đoạn này.</p>
|
||||
</div>
|
||||
|
||||
<Modal @close="showmodal = undefined" @modalevent="handleModalEvent" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
@modalevent="handleModalEvent"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -184,7 +293,7 @@ export default {
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
@@ -194,7 +303,7 @@ export default {
|
||||
isLoading: false,
|
||||
showmodal: undefined,
|
||||
phasedoctypes: [],
|
||||
viewMode: 'list',
|
||||
viewMode: "list",
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
@@ -221,9 +330,14 @@ export default {
|
||||
if (!this.transaction?.phase) return;
|
||||
|
||||
try {
|
||||
const phasedoctypesData = await $getdata('phasedoctype', {
|
||||
phase: this.transaction.phase,
|
||||
}, undefined, false);
|
||||
const phasedoctypesData = await $getdata(
|
||||
"phasedoctype",
|
||||
{
|
||||
phase: this.transaction.phase,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
if (phasedoctypesData) {
|
||||
this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData];
|
||||
@@ -240,15 +354,27 @@ export default {
|
||||
if (!this.row.id) return;
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const detail = await $getdata('reservation', {
|
||||
id: this.transaction.txncurrent__detail
|
||||
}, undefined, true)
|
||||
const filesArray = await $getdata('transactionfile', {
|
||||
txn_detail: detail.id,
|
||||
}, undefined, false);
|
||||
const detail = await $getdata(
|
||||
"reservation",
|
||||
{
|
||||
id: this.transaction.txncurrent__detail,
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
const filesArray = await $getdata(
|
||||
"transactionfile",
|
||||
{
|
||||
txn_detail: detail.id,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
if (filesArray) {
|
||||
this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort((a, b) => new Date(b.create_time) - new Date(a.create_time));
|
||||
this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort(
|
||||
(a, b) => new Date(b.create_time) - new Date(a.create_time),
|
||||
);
|
||||
} else {
|
||||
this.files = [];
|
||||
}
|
||||
@@ -260,18 +386,20 @@ export default {
|
||||
}
|
||||
},
|
||||
getFilesByDocType(docTypeId) {
|
||||
return this.files.filter(file => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null));
|
||||
return this.files.filter(
|
||||
(file) => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null),
|
||||
);
|
||||
},
|
||||
getFileExtension(fileName) {
|
||||
return fileName ? fileName.split('.').pop().toLowerCase() : 'file';
|
||||
return fileName ? fileName.split(".").pop().toLowerCase() : "file";
|
||||
},
|
||||
isImage(fileName) {
|
||||
const ext = this.getFileExtension(fileName);
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext);
|
||||
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
|
||||
},
|
||||
isViewableDocument(fileName) {
|
||||
const ext = this.getFileExtension(fileName);
|
||||
return ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext);
|
||||
return ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(ext);
|
||||
},
|
||||
async handleUpload(uploadedFiles, docTypeId) {
|
||||
if (!uploadedFiles || uploadedFiles.length === 0) return;
|
||||
@@ -282,15 +410,20 @@ export default {
|
||||
try {
|
||||
for (const fileRecord of uploadedFiles) {
|
||||
if (docTypeId) {
|
||||
await $patchapi('file', {
|
||||
await $patchapi("file", {
|
||||
id: fileRecord.id,
|
||||
doc_type: docTypeId
|
||||
doc_type: docTypeId,
|
||||
});
|
||||
}
|
||||
|
||||
const detail = await $getdata('reservation', {
|
||||
id: this.transaction.txncurrent__detail,
|
||||
}, undefined, true)
|
||||
const detail = await $getdata(
|
||||
"reservation",
|
||||
{
|
||||
id: this.transaction.txncurrent__detail,
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const payload = {
|
||||
txn_detail: detail.id,
|
||||
@@ -305,7 +438,7 @@ export default {
|
||||
}
|
||||
|
||||
await this.fetchFiles();
|
||||
this.$emit('upload-completed');
|
||||
this.$emit("upload-completed");
|
||||
} catch (error) {
|
||||
console.error("Lỗi khi lưu file:", error);
|
||||
alert("Đã xảy ra lỗi khi tải file lên. Vui lòng thử lại.");
|
||||
@@ -318,21 +451,21 @@ export default {
|
||||
const filePath = file.file__file || file.file;
|
||||
if (!filePath) return;
|
||||
|
||||
const link = document.createElement('a');
|
||||
const link = document.createElement("a");
|
||||
link.href = `${$getpath()}static/files/${filePath}`;
|
||||
link.download = file.file__name || 'download';
|
||||
link.download = file.file__name || "download";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
deleteFile(fileId) {
|
||||
this.showmodal = {
|
||||
component: 'dialog/Confirm',
|
||||
title: 'Xác nhận xóa',
|
||||
height: '10vh',
|
||||
width: '40%',
|
||||
component: "dialog/Confirm",
|
||||
title: "Xác nhận xóa",
|
||||
height: "10vh",
|
||||
width: "40%",
|
||||
vbind: {
|
||||
content: 'Bạn có chắc chắn muốn xóa file này không?'
|
||||
content: "Bạn có chắc chắn muốn xóa file này không?",
|
||||
},
|
||||
onConfirm: async () => {
|
||||
this.isLoading = true;
|
||||
@@ -350,17 +483,17 @@ export default {
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
handleModalEvent(event) {
|
||||
if (event.name === 'confirm' && typeof this.showmodal?.onConfirm === 'function') {
|
||||
if (event.name === "confirm" && typeof this.showmodal?.onConfirm === "function") {
|
||||
this.showmodal.onConfirm();
|
||||
}
|
||||
},
|
||||
viewFile(file) {
|
||||
const { $getpath } = useNuxtApp();
|
||||
const fileName = file.file__name || '';
|
||||
const fileName = file.file__name || "";
|
||||
const filePath = file.file__file || file.file;
|
||||
if (!filePath) return;
|
||||
|
||||
@@ -371,23 +504,23 @@ export default {
|
||||
if (isImageFile) {
|
||||
this.showmodal = {
|
||||
title: fileName,
|
||||
component: 'media/ChipImage',
|
||||
component: "media/ChipImage",
|
||||
vbind: {
|
||||
extend: false,
|
||||
file: file,
|
||||
image: fileUrl,
|
||||
show: ['download', 'delete']
|
||||
}
|
||||
show: ["download", "delete"],
|
||||
},
|
||||
};
|
||||
} else if (isViewable) {
|
||||
// Mở Google Viewer trực tiếp trong tab mới
|
||||
const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`;
|
||||
window.open(viewerUrl, '_blank');
|
||||
window.open(viewerUrl, "_blank");
|
||||
} else {
|
||||
this.downloadFile(file);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -420,4 +553,4 @@ export default {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Viewer: display when click tem from another dealer -->
|
||||
<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>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,15 @@
|
||||
<template>
|
||||
<div v-if="productData" class="grid px-3">
|
||||
<div
|
||||
v-if="productData"
|
||||
class="grid px-3"
|
||||
>
|
||||
<div class="cell is-col-span-12">
|
||||
<div id="schedule-content">
|
||||
<div v-if="selectedPolicy" id="print-area" :class="{ 'is-loading': isLoading }">
|
||||
<div
|
||||
v-if="selectedPolicy"
|
||||
id="print-area"
|
||||
:class="{ 'is-loading': isLoading }"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
||||
<h3 class="title is-4 has-text-primary mb-1">
|
||||
@@ -12,101 +19,186 @@
|
||||
<span class="button is-white">
|
||||
<span class="has-text-weight-semibold">Đơn vị: VNĐ</span>
|
||||
</span>
|
||||
<button class="button is-light" @click="$emit('print')" id="ignore-print">
|
||||
<button
|
||||
class="button is-light"
|
||||
@click="$emit('print')"
|
||||
id="ignore-print"
|
||||
>
|
||||
<span class="is-size-6">In</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4" style="background-color: var(--bulma-background)"></hr>
|
||||
<hr
|
||||
class="my-4"
|
||||
style="background-color: var(--bulma-background)"
|
||||
/>
|
||||
|
||||
<!-- Summary Information -->
|
||||
<div class="fixed-grid has-4-cols-mobile has-7-cols-desktop">
|
||||
<div class="grid">
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Sản phẩm</p>
|
||||
<p class="has-text-primary has-text-weight-medium">{{ productData.trade_code || productData.code }} <a
|
||||
class="ml-4" id="ignore" @click="$copyToClipboard(productData.trade_code)">
|
||||
<SvgIcon name="copy.svg" type="primary" :size="18" />
|
||||
<p class="has-text-primary has-text-weight-medium">
|
||||
{{ productData.trade_code || productData.code }}
|
||||
<a
|
||||
class="ml-4"
|
||||
id="ignore"
|
||||
@click="$copyToClipboard(productData.trade_code)"
|
||||
>
|
||||
<SvgIcon
|
||||
name="copy.svg"
|
||||
type="primary"
|
||||
:size="18"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá niêm yết</p>
|
||||
<p class="has-text-primary">{{ $numtoString(calculatorData.originPrice) }}</p>
|
||||
<p class="has-text-primary">
|
||||
{{ $numtoString(calculatorData.originPrice) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Tổng chiết khấu</p>
|
||||
<p class="has-text-danger has-text-weight-bold">{{ $numtoString(calculatorData.totalDiscount) }}</p>
|
||||
<p class="has-text-danger has-text-weight-bold">
|
||||
{{ $numtoString(calculatorData.totalDiscount) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá sau chiết khấu</p>
|
||||
<p class="has-text-black has-text-weight-bold">{{ $numtoString(calculatorData.salePrice) }}</p>
|
||||
<p class="has-text-black has-text-weight-bold">
|
||||
{{ $numtoString(calculatorData.salePrice) }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="selectedPolicy.contract_allocation_percentage < 100"
|
||||
class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<div
|
||||
v-if="selectedPolicy.contract_allocation_percentage < 100"
|
||||
class="cell is-col-span-6-mobile is-col-span-1-desktop"
|
||||
>
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá trị bảo đảm</p>
|
||||
<p class="has-text-primary">{{ $numtoString(calculatorData.allocatedPrice) }}</p>
|
||||
<p class="has-text-primary">
|
||||
{{ $numtoString(calculatorData.allocatedPrice) }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="totalPaid === 0" class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<div
|
||||
v-if="totalPaid === 0"
|
||||
class="cell is-col-span-6-mobile is-col-span-1-desktop"
|
||||
>
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Đặt cọc</p>
|
||||
<p class="has-text-primary">{{ $numtoString(selectedPolicy.deposit) }}</p>
|
||||
<p class="has-text-primary">
|
||||
{{ $numtoString(selectedPolicy.deposit) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Khách hàng</p>
|
||||
<p v-if="selectedCustomer" class="has-text-primary has-text-weight-medium">
|
||||
<p
|
||||
v-if="selectedCustomer"
|
||||
class="has-text-primary has-text-weight-medium"
|
||||
>
|
||||
{{ selectedCustomer.code }} - {{ selectedCustomer.fullname }}
|
||||
</p>
|
||||
<p v-else class="has-text-grey is-italic is-size-6">Chưa chọn</p>
|
||||
<p
|
||||
v-else
|
||||
class="has-text-grey is-italic is-size-6"
|
||||
>
|
||||
Chưa chọn
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" style="background-color: var(--bulma-background)"></hr>
|
||||
<hr
|
||||
class="my-4"
|
||||
style="background-color: var(--bulma-background)"
|
||||
/>
|
||||
|
||||
<!-- Detailed Discounts -->
|
||||
<div v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0" class="mt-4 mb-4">
|
||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
|
||||
CHI TIẾT CHIẾT KHẤU:
|
||||
</p>
|
||||
<div
|
||||
v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0"
|
||||
class="mt-4 mb-4"
|
||||
>
|
||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">CHI TIẾT CHIẾT KHẤU:</p>
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;"
|
||||
colspan="2">Diễn giải chiết khấu</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;" width="15%">Giá trị</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;" width="20%">Thành tiền</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;" width="20%">Còn lại</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
colspan="2"
|
||||
>
|
||||
Diễn giải chiết khấu
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
width="15%"
|
||||
>
|
||||
Giá trị
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
width="20%"
|
||||
>
|
||||
Thành tiền
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
width="20%"
|
||||
>
|
||||
Còn lại
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom: 1px solid #f5f5f5;" class="has-text-grey-light">
|
||||
<td colspan="4" class="has-text-right pt-1 pb-1">Giá gốc</td>
|
||||
<td class="has-text-right has-text-weight-bold pt-1 pb-1">{{
|
||||
$numtoString(calculatorData.originPrice) }}</td>
|
||||
<tr
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
class="has-text-grey-light"
|
||||
>
|
||||
<td
|
||||
colspan="4"
|
||||
class="has-text-right pt-1 pb-1"
|
||||
>
|
||||
Giá gốc
|
||||
</td>
|
||||
<td class="has-text-right has-text-weight-bold pt-1 pb-1">
|
||||
{{ $numtoString(calculatorData.originPrice) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="(item, idx) in calculatorData.detailedDiscounts" :key="`discount-${idx}`"
|
||||
style="border-bottom: 1px solid #f5f5f5;">
|
||||
<td width="5%" class="has-text-centered">{{ idx + 1 }}</td>
|
||||
<tr
|
||||
v-for="(item, idx) in calculatorData.detailedDiscounts"
|
||||
:key="`discount-${idx}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
>
|
||||
<td
|
||||
width="5%"
|
||||
class="has-text-centered"
|
||||
>
|
||||
{{ idx + 1 }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="has-text-weight-semibold">{{ item.name }}</span>
|
||||
<span class="tag is-primary has-text-white is-rounded border ml-1">{{ item.code }}</span>
|
||||
</td>
|
||||
<td class="has-text-right">{{ item.customType === 1 ? item.customValue + '%' :
|
||||
$numtoString(item.customValue) }}</td>
|
||||
<td class="has-text-right">
|
||||
{{ item.customType === 1 ? item.customValue + "%" : $numtoString(item.customValue) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-danger">-{{ $numtoString(item.amount) }}</td>
|
||||
<td class="has-text-right has-text-primary">{{ $numtoString(item.remaining) }}</td>
|
||||
<td class="has-text-right has-text-primary">
|
||||
{{ $numtoString(item.remaining) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Early Payment Details -->
|
||||
<div v-if="isEarlyPaymentActive" class="mt-4 mb-4">
|
||||
<div
|
||||
v-if="isEarlyPaymentActive"
|
||||
class="mt-4 mb-4"
|
||||
>
|
||||
<!-- Original Schedule -->
|
||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
|
||||
LỊCH THANH TOÁN GỐC (THEO CHÍNH SÁCH)
|
||||
@@ -115,26 +207,57 @@
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Đợt
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Tỷ lệ
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số tiền (VND)
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày bắt đầu
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày đến hạn
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số ngày
|
||||
</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Tỷ lệ</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Số tiền (VND)</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
||||
bắt đầu</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
||||
đến hạn</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Số ngày</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(plan, index) in calculatorData.originalPaymentSchedule" :key="`orig-plan-${index}`"
|
||||
style="border-bottom: 1px solid #f5f5f5;">
|
||||
<tr
|
||||
v-for="(plan, index) in calculatorData.originalPaymentSchedule"
|
||||
:key="`orig-plan-${index}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
>
|
||||
<td class="has-text-weight-semibold">Đợt {{ plan.cycle }}</td>
|
||||
<td class="has-text-right">{{ plan.type === 1 ? `${plan.value}%` : '-' }}</td>
|
||||
<td class="has-text-right">{{ $numtoString(plan.amount) }}</td>
|
||||
<td class="has-text-right">
|
||||
{{ plan.type === 1 ? `${plan.value}%` : "-" }}
|
||||
</td>
|
||||
<td class="has-text-right">
|
||||
{{ $numtoString(plan.amount) }}
|
||||
</td>
|
||||
<td>{{ formatDate(plan.from_date) }}</td>
|
||||
<td>{{ formatDate(plan.to_date) }}</td>
|
||||
<td class="has-text-right">{{ plan.days }}</td>
|
||||
@@ -151,29 +274,62 @@
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Đợt
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Hạn TT Gốc
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày TT Thực Tế
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số tiền gốc
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số ngày TT sớm
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Tỷ lệ CK (%/ngày)
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Tiền chiết khấu
|
||||
</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Hạn
|
||||
TT Gốc</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
||||
TT Thực Tế</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Số tiền gốc</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Số ngày TT sớm</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Tỷ lệ CK (%/ngày)</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Tiền chiết khấu</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, idx) in calculatorData.earlyDiscountDetails" :key="`early-discount-${idx}`"
|
||||
style="border-bottom: 1px solid #f5f5f5;">
|
||||
<tr
|
||||
v-for="(item, idx) in calculatorData.earlyDiscountDetails"
|
||||
:key="`early-discount-${idx}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
>
|
||||
<td>Đợt {{ item.cycle }}</td>
|
||||
<td>{{ formatDate(item.original_payment_date) }}</td>
|
||||
<td>{{ formatDate(item.actual_payment_date) }}</td>
|
||||
<td class="has-text-right">{{ $numtoString(item.original_amount) }}</td>
|
||||
<td class="has-text-right">
|
||||
{{ $numtoString(item.original_amount) }}
|
||||
</td>
|
||||
<td class="has-text-right">{{ item.early_days }}</td>
|
||||
<td class="has-text-right">{{ item.discount_rate }}</td>
|
||||
<td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td>
|
||||
@@ -181,9 +337,15 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="has-background-light">
|
||||
<th colspan="6" class="has-text-right has-text-weight-bold">Tổng chiết khấu thanh toán sớm</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-danger">-{{
|
||||
$numtoString(totalEarlyDiscount) }}</th>
|
||||
<th
|
||||
colspan="6"
|
||||
class="has-text-right has-text-weight-bold"
|
||||
>
|
||||
Tổng chiết khấu thanh toán sớm
|
||||
</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-danger">
|
||||
-{{ $numtoString(totalEarlyDiscount) }}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@@ -191,7 +353,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Payment Schedule Table -->
|
||||
<div v-if="displaySchedule.length > 0" class="mt-4">
|
||||
<div
|
||||
v-if="displaySchedule.length > 0"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="level m-0 mb-2 is-mobile">
|
||||
<div class="level-left">
|
||||
<p class="has-text-weight-bold is-size-5 has-text-primary is-underlined">
|
||||
@@ -199,14 +364,23 @@
|
||||
<span v-else>LỊCH THANH TOÁN</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="level-right" id="ignore-print">
|
||||
<div
|
||||
class="level-right"
|
||||
id="ignore-print"
|
||||
>
|
||||
<div class="buttons are-small has-addons">
|
||||
<button class="button" @click="viewMode = 'table'"
|
||||
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'">
|
||||
<button
|
||||
class="button"
|
||||
@click="viewMode = 'table'"
|
||||
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'"
|
||||
>
|
||||
<span class="is-size-6">Bảng</span>
|
||||
</button>
|
||||
<button class="button" @click="viewMode = 'list'"
|
||||
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'">
|
||||
<button
|
||||
class="button"
|
||||
@click="viewMode = 'list'"
|
||||
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'"
|
||||
>
|
||||
<span class="is-size-6">Thẻ</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -214,64 +388,136 @@
|
||||
</div>
|
||||
|
||||
<!-- Table View -->
|
||||
<div v-if="viewMode === 'table'" class="table-container schedule-container">
|
||||
<div
|
||||
v-if="viewMode === 'table'"
|
||||
class="table-container schedule-container"
|
||||
>
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
|
||||
thanh toán</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Số tiền (VND)</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Đã thanh toán</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none;">Còn phải TT</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
||||
bắt đầu</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
|
||||
đến hạn</th>
|
||||
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Trạng
|
||||
thái</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Đợt thanh toán
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số tiền (VND)
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Đã thanh toán
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Còn phải TT
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày bắt đầu
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày đến hạn
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Trạng thái
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(plan, index) in displaySchedule" :key="`plan-${index}`"
|
||||
style="border-bottom: 1px solid #f5f5f5;"
|
||||
:class="plan.is_merged ? 'has-background-warning-light' : ''">
|
||||
<td class="has-text-weight-semibold" :class="plan.is_merged ? 'has-text-warning' : ''">
|
||||
<tr
|
||||
v-for="(plan, index) in displaySchedule"
|
||||
:key="`plan-${index}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
:class="plan.is_merged ? 'has-background-warning-light' : ''"
|
||||
>
|
||||
<td
|
||||
class="has-text-weight-semibold"
|
||||
:class="plan.is_merged ? 'has-text-warning' : ''"
|
||||
>
|
||||
Đợt {{ plan.cycle }}
|
||||
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span>
|
||||
<span
|
||||
v-if="plan.is_merged"
|
||||
class="tag is-warning is-light ml-1 is-size-7"
|
||||
>GỘP SỚM</span
|
||||
>
|
||||
</td>
|
||||
<td class="has-text-right">
|
||||
<div v-if="plan.is_merged" class="has-text-right">
|
||||
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount) }}
|
||||
<div
|
||||
v-if="plan.is_merged"
|
||||
class="has-text-right"
|
||||
>
|
||||
<p
|
||||
class="has-text-grey"
|
||||
title="Tổng các đợt gốc"
|
||||
>
|
||||
{{ $numtoString(totalOriginalEarlyAmount) }}
|
||||
</p>
|
||||
<p
|
||||
class="has-text-danger"
|
||||
title="Chiết khấu thanh toán sớm"
|
||||
>
|
||||
- {{ $numtoString(totalEarlyDiscount) }}
|
||||
</p>
|
||||
<hr
|
||||
class="my-1"
|
||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%"
|
||||
/>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $numtoString(plan.amount) }}
|
||||
</p>
|
||||
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{
|
||||
$numtoString(totalEarlyDiscount) }}</p>
|
||||
<hr class="my-1"
|
||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%;">
|
||||
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
|
||||
</div>
|
||||
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
||||
</td>
|
||||
<td class="has-text-right has-text-success">{{ $numtoString(plan.paid_amount) }}</td>
|
||||
<td class="has-text-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</td>
|
||||
<td class="has-text-right has-text-success">
|
||||
{{ $numtoString(plan.paid_amount) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-danger">
|
||||
{{ $numtoString(plan.remain_amount) }}
|
||||
</td>
|
||||
<td>{{ formatDate(plan.from_date) }}</td>
|
||||
<td>{{ formatDate(plan.to_date) }}</td>
|
||||
<td>
|
||||
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span>
|
||||
<span v-else class="tag is-warning">Chờ thanh toán</span>
|
||||
<span
|
||||
v-if="plan.status === 2"
|
||||
class="tag is-success"
|
||||
>Đã thanh toán</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="tag is-warning"
|
||||
>Chờ thanh toán</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="has-background-light">
|
||||
<th class="has-text-right has-text-weight-bold">Tổng cộng</th>
|
||||
<th class="has-text-right has-text-weight-bold">{{ $numtoString(totalAmount) }}</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-success">{{ $numtoString(totalPaid) }}
|
||||
<th class="has-text-right has-text-weight-bold">
|
||||
{{ $numtoString(totalAmount) }}
|
||||
</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-success">
|
||||
{{ $numtoString(totalPaid) }}
|
||||
</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-danger">
|
||||
{{ $numtoString(calculatorData.totalRemaining) }}
|
||||
</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-danger">{{
|
||||
$numtoString(calculatorData.totalRemaining) }}</th>
|
||||
<th colspan="3"></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -279,28 +525,57 @@
|
||||
</div>
|
||||
|
||||
<!-- List View (Card) -->
|
||||
<div v-else-if="viewMode === 'list'" class="schedule-container">
|
||||
<div v-for="(plan, index) in displaySchedule" :key="`card-${index}`" class="card mb-4"
|
||||
:class="plan.is_merged ? 'has-background-warning-light' : ''">
|
||||
<div
|
||||
v-else-if="viewMode === 'list'"
|
||||
class="schedule-container"
|
||||
>
|
||||
<div
|
||||
v-for="(plan, index) in displaySchedule"
|
||||
:key="`card-${index}`"
|
||||
class="card mb-4"
|
||||
:class="plan.is_merged ? 'has-background-warning-light' : ''"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="level is-mobile mb-5">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<span class="tag is-primary" :class="plan.is_merged ? 'is-warning' : ''">Đợt {{ plan.cycle
|
||||
}}</span>
|
||||
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span>
|
||||
<span
|
||||
class="tag is-primary"
|
||||
:class="plan.is_merged ? 'is-warning' : ''"
|
||||
>Đợt {{ plan.cycle }}</span
|
||||
>
|
||||
<span
|
||||
v-if="plan.is_merged"
|
||||
class="tag is-warning is-light ml-1 is-size-7"
|
||||
>GỘP SỚM</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item has-text-weight-bold">
|
||||
<div v-if="plan.is_merged" class="has-text-right">
|
||||
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount)
|
||||
}}</p>
|
||||
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{
|
||||
$numtoString(totalEarlyDiscount) }}</p>
|
||||
<hr class="my-1"
|
||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto">
|
||||
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
|
||||
<div
|
||||
v-if="plan.is_merged"
|
||||
class="has-text-right"
|
||||
>
|
||||
<p
|
||||
class="has-text-grey"
|
||||
title="Tổng các đợt gốc"
|
||||
>
|
||||
{{ $numtoString(totalOriginalEarlyAmount) }}
|
||||
</p>
|
||||
<p
|
||||
class="has-text-danger"
|
||||
title="Chiết khấu thanh toán sớm"
|
||||
>
|
||||
- {{ $numtoString(totalEarlyDiscount) }}
|
||||
</p>
|
||||
<hr
|
||||
class="my-1"
|
||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto"
|
||||
/>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $numtoString(plan.amount) }}
|
||||
</p>
|
||||
</div>
|
||||
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
||||
</div>
|
||||
@@ -308,25 +583,41 @@
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Đã thanh toán:</div>
|
||||
<div class="level-right has-text-success">{{ $numtoString(plan.paid_amount) }}</div>
|
||||
<div class="level-right has-text-success">
|
||||
{{ $numtoString(plan.paid_amount) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Còn phải TT:</div>
|
||||
<div class="level-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</div>
|
||||
<div class="level-right has-text-danger">
|
||||
{{ $numtoString(plan.remain_amount) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Từ ngày:</div>
|
||||
<div class="level-right">{{ formatDate(plan.from_date) }}</div>
|
||||
<div class="level-right">
|
||||
{{ formatDate(plan.from_date) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Đến hạn:</div>
|
||||
<div class="level-right">{{ formatDate(plan.to_date) }}</div>
|
||||
<div class="level-right">
|
||||
{{ formatDate(plan.to_date) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">Trạng thái:</div>
|
||||
<div class="level-right">
|
||||
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span>
|
||||
<span v-else class="tag is-warning">Chờ thanh toán</span>
|
||||
<span
|
||||
v-if="plan.status === 2"
|
||||
class="tag is-success"
|
||||
>Đã thanh toán</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="tag is-warning"
|
||||
>Chờ thanh toán</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,7 +626,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Summary Footer -->
|
||||
<div class="" style="border-top: 1px solid #eee;">
|
||||
<div
|
||||
class=""
|
||||
style="border-top: 1px solid #eee"
|
||||
>
|
||||
<div class="level is-mobile is-size-6 my-4">
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
@@ -354,22 +648,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { ref, computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN
|
||||
const props = defineProps({
|
||||
productData: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
selectedPolicy: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
selectedCustomer: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
calculatorData: {
|
||||
type: Object,
|
||||
@@ -390,15 +684,15 @@ const props = defineProps({
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['print']);
|
||||
const emit = defineEmits(["print"]);
|
||||
|
||||
// Local state
|
||||
const viewMode = ref('table');
|
||||
const viewMode = ref("table");
|
||||
|
||||
// Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN
|
||||
const displaySchedule = computed(() => {
|
||||
@@ -426,8 +720,8 @@ const totalPaid = computed(() => {
|
||||
});
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('DD/MM/YYYY');
|
||||
if (!date) return "-";
|
||||
return dayjs(date).format("DD/MM/YYYY");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -501,4 +795,4 @@ th,
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
<template v-if="option === 'your'">
|
||||
<template v-if="tab === 'message'">
|
||||
<div class="field is-grouped" v-for="(v, i) in message">
|
||||
<div
|
||||
class="field is-grouped"
|
||||
v-for="(v, i) in message"
|
||||
>
|
||||
<div class="control is-expanded">
|
||||
<textarea class="textarea" placeholder="" rows="3" v-model="v.text"></textarea>
|
||||
<textarea
|
||||
class="textarea"
|
||||
placeholder=""
|
||||
rows="3"
|
||||
v-model="v.text"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="mr-3" @click="add()">
|
||||
<a
|
||||
class="mr-3"
|
||||
@click="add()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 20 }"></SvgIcon>
|
||||
</a>
|
||||
<a @click="remove(v, i)">
|
||||
@@ -44,22 +55,40 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="update()"
|
||||
>
|
||||
Cập nhật
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="tab === 'image'">
|
||||
<div class="field is-grouped mb-0">
|
||||
<div class="control is-expanded"></div>
|
||||
<div class="control">
|
||||
<FileUpload v-bind="{ position: 'left' }" @files="getImages"></FileUpload>
|
||||
<FileUpload
|
||||
v-bind="{ position: 'left' }"
|
||||
@files="getImages"
|
||||
></FileUpload>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0">
|
||||
<div class="control mb-2" v-for="(v, i) in image">
|
||||
<div
|
||||
class="field is-grouped is-grouped-multiline"
|
||||
v-if="image.length > 0"
|
||||
>
|
||||
<div
|
||||
class="control mb-2"
|
||||
v-for="(v, i) in image"
|
||||
>
|
||||
<ChipImage
|
||||
style="width: 128px"
|
||||
@remove="removeImage(v, i)"
|
||||
v-bind="{ show: ['copy', 'download', 'delete'], file: v, image: `${$getpath()}static/files/${v.file}` }"
|
||||
v-bind="{
|
||||
show: ['copy', 'download', 'delete'],
|
||||
file: v,
|
||||
image: `${$getpath()}static/files/${v.file}`,
|
||||
}"
|
||||
>
|
||||
</ChipImage>
|
||||
</div>
|
||||
@@ -69,24 +98,47 @@
|
||||
<div class="field is-grouped mb-0">
|
||||
<div class="control is-expanded"></div>
|
||||
<div class="control">
|
||||
<FileUpload v-bind="{ position: 'left', type: 'file' }" @files="getFiles"></FileUpload>
|
||||
<FileUpload
|
||||
v-bind="{ position: 'left', type: 'file' }"
|
||||
@files="getFiles"
|
||||
></FileUpload>
|
||||
</div>
|
||||
</div>
|
||||
<FileShow @remove="removeFile" v-bind="{ files: file, show: { delete: 1 } }"></FileShow>
|
||||
<FileShow
|
||||
@remove="removeFile"
|
||||
v-bind="{ files: file, show: { delete: 1 } }"
|
||||
></FileShow>
|
||||
</template>
|
||||
<template v-else-if="tab === 'link'">
|
||||
<div class="field is-grouped" v-for="(v, i) in link">
|
||||
<div
|
||||
class="field is-grouped"
|
||||
v-for="(v, i) in link"
|
||||
>
|
||||
<div class="control is-expanded">
|
||||
<input class="input" placeholder="" v-model="v.link" />
|
||||
<input
|
||||
class="input"
|
||||
placeholder=""
|
||||
v-model="v.link"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="mr-3" @click="copyContent(v.link)">
|
||||
<a
|
||||
class="mr-3"
|
||||
@click="copyContent(v.link)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||
</a>
|
||||
<a class="mr-3" :href="v.link" target="_blank">
|
||||
<a
|
||||
class="mr-3"
|
||||
:href="v.link"
|
||||
target="_blank"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'open.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||
</a>
|
||||
<a class="mr-3" @click="addLink()">
|
||||
<a
|
||||
class="mr-3"
|
||||
@click="addLink()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a @click="removeLink(v, i)">
|
||||
@@ -95,14 +147,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="update()"
|
||||
>
|
||||
Cập nhật
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="option === 'system'">
|
||||
<template v-if="tab === 'message'">
|
||||
<div v-if="message">
|
||||
<div class="px-2 py-2 mb-2" style="border: 1px solid #e8e8e8" v-for="(v, i) in message">
|
||||
<div
|
||||
class="px-2 py-2 mb-2"
|
||||
style="border: 1px solid #e8e8e8"
|
||||
v-for="(v, i) in message"
|
||||
>
|
||||
<span class="mr-3">{{ v.text }}</span>
|
||||
<a @click="copyContent(v.text)">
|
||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||
@@ -111,11 +172,21 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="tab === 'image'">
|
||||
<div class="field is-grouped is-grouped-multiline" v-if="image.length > 0">
|
||||
<div class="control mb-2" v-for="(v, i) in image">
|
||||
<div
|
||||
class="field is-grouped is-grouped-multiline"
|
||||
v-if="image.length > 0"
|
||||
>
|
||||
<div
|
||||
class="control mb-2"
|
||||
v-for="(v, i) in image"
|
||||
>
|
||||
<ChipImage
|
||||
style="width: 128px"
|
||||
v-bind="{ show: ['copy', 'download'], file: v, image: `${$getpath()}static/files/${v.file}` }"
|
||||
v-bind="{
|
||||
show: ['copy', 'download'],
|
||||
file: v,
|
||||
image: `${$getpath()}static/files/${v.file}`,
|
||||
}"
|
||||
>
|
||||
</ChipImage>
|
||||
</div>
|
||||
@@ -125,8 +196,17 @@
|
||||
<FileShow v-bind="{ files: file }"></FileShow>
|
||||
</template>
|
||||
<template v-else-if="tab === 'link'">
|
||||
<div class="px-2 py-2 mb-2" style="border: 1px solid #e8e8e8" v-for="(v, i) in link">
|
||||
<a :href="v.link" target="_blank" class="mr-3">{{ v.link }}</a>
|
||||
<div
|
||||
class="px-2 py-2 mb-2"
|
||||
style="border: 1px solid #e8e8e8"
|
||||
v-for="(v, i) in link"
|
||||
>
|
||||
<a
|
||||
:href="v.link"
|
||||
target="_blank"
|
||||
class="mr-3"
|
||||
>{{ v.link }}</a
|
||||
>
|
||||
<a @click="copyContent(v.link)">
|
||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||
</a>
|
||||
|
||||
@@ -3,20 +3,26 @@ const props = defineProps({
|
||||
text: String,
|
||||
image: String,
|
||||
type: String,
|
||||
size: Number
|
||||
size: Number,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<template>
|
||||
<div
|
||||
@click="$emit('justclick')"
|
||||
class="rounded-full mx-0 px-0 size-10 font-bold is-flex is-justify-content-center is-align-items-center"
|
||||
:style="{
|
||||
border: image ? 'none' : '1px solid var(--bulma-grey-70)'
|
||||
border: image ? 'none' : '1px solid var(--bulma-grey-70)',
|
||||
}"
|
||||
>
|
||||
<figure v-if="image" class="image">
|
||||
<img class="is-rounded" :src="`${$path()}download?name=${image}`">
|
||||
<figure
|
||||
v-if="image"
|
||||
class="image"
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="`${$path()}download?name=${image}`"
|
||||
/>
|
||||
</figure>
|
||||
<span v-else>{{text}}</span>
|
||||
<span v-else>{{ text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
<template>
|
||||
<div @click="handleClick()" class="is-clickable">
|
||||
<div
|
||||
@click="handleClick()"
|
||||
class="is-clickable"
|
||||
>
|
||||
<template v-if="count > 0">
|
||||
<span class="dot-primary">
|
||||
{{ count }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="dot-primary">
|
||||
+
|
||||
</span>
|
||||
<span class="dot-primary"> + </span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['row', 'countField', 'modalConfig'],
|
||||
|
||||
props: ["row", "countField", "modalConfig"],
|
||||
|
||||
computed: {
|
||||
count() {
|
||||
return this.row[this.countField] || 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
handleClick() {
|
||||
if (!this.modalConfig) return;
|
||||
|
||||
let config = this.$copy(this.modalConfig);
|
||||
|
||||
this.$emit('open', {
|
||||
name: 'dataevent',
|
||||
|
||||
this.$emit("open", {
|
||||
name: "dataevent",
|
||||
data: {
|
||||
modal: config
|
||||
}
|
||||
modal: config,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,135 +1,141 @@
|
||||
<!-- CountdownTimer.vue -->
|
||||
<template>
|
||||
<div class="countdown-wrapper">
|
||||
<span v-if="isExpired" class="tag is-danger">
|
||||
{{ isVietnamese ? 'Hết giờ' : 'Expired' }}
|
||||
<span
|
||||
v-if="isExpired"
|
||||
class="tag is-danger"
|
||||
>
|
||||
{{ isVietnamese ? "Hết giờ" : "Expired" }}
|
||||
</span>
|
||||
<span v-else class="tag" :class="tagClass">
|
||||
<span
|
||||
v-else
|
||||
class="tag"
|
||||
:class="tagClass"
|
||||
>
|
||||
<span class="countdown-text">{{ formattedTime }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useStore } from '@/stores/index'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
const props = defineProps({
|
||||
dateValue: {
|
||||
type: [String, Date],
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'HH:mm:ss'
|
||||
}
|
||||
})
|
||||
default: "HH:mm:ss",
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore()
|
||||
const { $dayjs } = useNuxtApp()
|
||||
const store = useStore();
|
||||
const { $dayjs } = useNuxtApp();
|
||||
|
||||
const timeRemaining = ref({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0
|
||||
})
|
||||
seconds: 0,
|
||||
});
|
||||
|
||||
const isExpired = ref(false)
|
||||
let intervalId = null
|
||||
const isExpired = ref(false);
|
||||
let intervalId = null;
|
||||
|
||||
const isVietnamese = computed(() => store.lang === 'vi')
|
||||
const isVietnamese = computed(() => store.lang === "vi");
|
||||
|
||||
const tagClass = computed(() => {
|
||||
const totalSeconds = timeRemaining.value.days * 86400 +
|
||||
timeRemaining.value.hours * 3600 +
|
||||
timeRemaining.value.minutes * 60 +
|
||||
timeRemaining.value.seconds
|
||||
const totalSeconds =
|
||||
timeRemaining.value.days * 86400 +
|
||||
timeRemaining.value.hours * 3600 +
|
||||
timeRemaining.value.minutes * 60 +
|
||||
timeRemaining.value.seconds;
|
||||
|
||||
if (totalSeconds <= 0) return 'is-danger'
|
||||
if (totalSeconds <= 3600) return 'is-warning' // <= 1 hour
|
||||
if (totalSeconds <= 86400) return 'is-info' // <= 1 day
|
||||
return 'is-primary' // > 1 day
|
||||
})
|
||||
if (totalSeconds <= 0) return "is-danger";
|
||||
if (totalSeconds <= 3600) return "is-warning"; // <= 1 hour
|
||||
if (totalSeconds <= 86400) return "is-info"; // <= 1 day
|
||||
return "is-primary"; // > 1 day
|
||||
});
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
const { days, hours, minutes, seconds } = timeRemaining.value
|
||||
const { days, hours, minutes, seconds } = timeRemaining.value;
|
||||
|
||||
if (days > 0) {
|
||||
return isVietnamese
|
||||
? `${days}d ${hours}h ${minutes}m`
|
||||
: `${days}d ${hours}h ${minutes}m`
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
return isVietnamese
|
||||
? `${hours}h ${minutes}m ${seconds}s`
|
||||
: `${hours}h ${minutes}m ${seconds}s`
|
||||
return isVietnamese ? `${days}d ${hours}h ${minutes}m` : `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
return isVietnamese
|
||||
? `${minutes}m ${seconds}s`
|
||||
: `${minutes}m ${seconds}s`
|
||||
})
|
||||
if (hours > 0) {
|
||||
return isVietnamese ? `${hours}h ${minutes}m ${seconds}s` : `${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
return isVietnamese ? `${minutes}m ${seconds}s` : `${minutes}m ${seconds}s`;
|
||||
});
|
||||
|
||||
const calculateTimeRemaining = () => {
|
||||
try {
|
||||
const targetDate = $dayjs(props.dateValue)
|
||||
const now = $dayjs()
|
||||
const targetDate = $dayjs(props.dateValue);
|
||||
const now = $dayjs();
|
||||
|
||||
if (now.isAfter(targetDate)) {
|
||||
isExpired.value = true
|
||||
timeRemaining.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
|
||||
return
|
||||
isExpired.value = true;
|
||||
timeRemaining.value = { days: 0, hours: 0, minutes: 0, seconds: 0 };
|
||||
return;
|
||||
}
|
||||
|
||||
isExpired.value = false
|
||||
const diff = targetDate.diff(now, 'second')
|
||||
isExpired.value = false;
|
||||
const diff = targetDate.diff(now, "second");
|
||||
|
||||
const days = Math.floor(diff / 86400)
|
||||
const hours = Math.floor((diff % 86400) / 3600)
|
||||
const minutes = Math.floor((diff % 3600) / 60)
|
||||
const seconds = diff % 60
|
||||
const days = Math.floor(diff / 86400);
|
||||
const hours = Math.floor((diff % 86400) / 3600);
|
||||
const minutes = Math.floor((diff % 3600) / 60);
|
||||
const seconds = diff % 60;
|
||||
|
||||
timeRemaining.value = { days, hours, minutes, seconds }
|
||||
timeRemaining.value = { days, hours, minutes, seconds };
|
||||
} catch (error) {
|
||||
console.error('Error calculating countdown:', error)
|
||||
isExpired.value = true
|
||||
console.error("Error calculating countdown:", error);
|
||||
isExpired.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
calculateTimeRemaining()
|
||||
calculateTimeRemaining();
|
||||
|
||||
if (intervalId) clearInterval(intervalId)
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
|
||||
intervalId = setInterval(() => {
|
||||
calculateTimeRemaining()
|
||||
calculateTimeRemaining();
|
||||
|
||||
if (isExpired.value && intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
watch(() => props.dateValue, () => {
|
||||
startCountdown()
|
||||
}, { deep: true })
|
||||
watch(
|
||||
() => props.dateValue,
|
||||
() => {
|
||||
startCountdown();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
startCountdown()
|
||||
})
|
||||
startCountdown();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.countdown-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,7 @@ const toolbarOptions = [
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
|
||||
// ✍️ Định dạng cơ bản
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
["bold", "italic", "underline", "strike"],
|
||||
|
||||
// 🎨 Màu chữ & nền
|
||||
[{ color: [] }, { background: [] }],
|
||||
@@ -46,14 +46,13 @@ const toolbarOptions = [
|
||||
[{ align: [] }],
|
||||
|
||||
// 📋 Danh sách
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
|
||||
// 🔗 Media
|
||||
['link', 'image', 'video'],
|
||||
|
||||
['clean'], // Xóa định dạng
|
||||
]
|
||||
["link", "image", "video"],
|
||||
|
||||
["clean"], // Xóa định dạng
|
||||
];
|
||||
|
||||
var content = props.text;
|
||||
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
<template>
|
||||
<div class="control has-icons-left">
|
||||
<input :class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-black' : ''}`" type="text"
|
||||
:placeholder="placeholder || ''" v-model="value" @keyup="doCheck" :disabled="disabled || false">
|
||||
<span class="icon is-left">
|
||||
<SvgIcon v-bind="{name: 'email.svg', type: 'gray', size: 21}"></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control has-icons-left">
|
||||
<input
|
||||
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-black' : ''}`"
|
||||
type="text"
|
||||
:placeholder="placeholder || ''"
|
||||
v-model="value"
|
||||
@keyup="doCheck"
|
||||
:disabled="disabled || false"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<SvgIcon v-bind="{ name: 'email.svg', type: 'gray', size: 21 }"></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['record', 'attr', 'placeholder', 'disabled'],
|
||||
props: ["record", "attr", "placeholder", "disabled"],
|
||||
data() {
|
||||
return {
|
||||
value: this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined,
|
||||
error: undefined
|
||||
}
|
||||
value: this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
record: function(newVal) {
|
||||
this.value = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined
|
||||
}
|
||||
record: function (newVal) {
|
||||
this.value = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doCheck() {
|
||||
if(this.$empty(this.value)) {
|
||||
this.value = undefined
|
||||
this.error = false
|
||||
return this.$emit('email', null)
|
||||
if (this.$empty(this.value)) {
|
||||
this.value = undefined;
|
||||
this.error = false;
|
||||
return this.$emit("email", null);
|
||||
}
|
||||
let check = this.$errEmail(this.value)
|
||||
this.error = check? true : false
|
||||
this.$emit('email', this.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
let check = this.$errEmail(this.value);
|
||||
this.error = check ? true : false;
|
||||
this.$emit("email", this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
autocomplete="tel"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<SvgIcon v-bind="{name: 'phone.png', type: 'gray', size: 20}"></SvgIcon>
|
||||
<SvgIcon v-bind="{ name: 'phone.png', type: 'gray', size: 20 }"></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<textarea v-model="record.note" class="textarea" name="note" placeholder="" rows="8"></textarea>
|
||||
<textarea
|
||||
v-model="record.note"
|
||||
class="textarea"
|
||||
name="note"
|
||||
placeholder=""
|
||||
rows="8"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button class="button is-primary has-text-white" @click="save()">
|
||||
{{ $store.lang==='vi'? 'Lưu lại' : 'Save' }}
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="save()"
|
||||
>
|
||||
{{ $store.lang === "vi" ? "Lưu lại" : "Save" }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const emit = defineEmits(["close"])
|
||||
const { $store, $getdata, $updateapi, $updatepage } = useNuxtApp();
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
pagename: String
|
||||
})
|
||||
var record = await $getdata('application', {id: props.row.id}, undefined, true)
|
||||
async function save() {
|
||||
await $updateapi('application', record)
|
||||
record = await $getdata('application', {id: props.row.id}, undefined, true)
|
||||
$updatepage(props.pagename, record)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
const emit = defineEmits(["close"]);
|
||||
const { $store, $getdata, $updateapi, $updatepage } = useNuxtApp();
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
pagename: String,
|
||||
});
|
||||
var record = await $getdata("application", { id: props.row.id }, undefined, true);
|
||||
async function save() {
|
||||
await $updateapi("application", record);
|
||||
record = await $getdata("application", { id: props.row.id }, undefined, true);
|
||||
$updatepage(props.pagename, record);
|
||||
emit("close");
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="data">
|
||||
<article class="message is-findata" v-if="data.length===0">
|
||||
<div class="message-body py-2 fs-16">
|
||||
Chưa có <b>ghi chú</b> nào được lưu
|
||||
</div>
|
||||
<article
|
||||
class="message is-findata"
|
||||
v-if="data.length === 0"
|
||||
>
|
||||
<div class="message-body py-2 fs-16">Chưa có <b>ghi chú</b> nào được lưu</div>
|
||||
</article>
|
||||
<template v-else>
|
||||
<article class="media mt-0 mb-0" v-for="(v,i) in data">
|
||||
<article
|
||||
class="media mt-0 mb-0"
|
||||
v-for="(v, i) in data"
|
||||
>
|
||||
<figure class="media-left">
|
||||
<Avatarbox v-bind="{
|
||||
text: v.user__fullname[0].toUpperCase(),
|
||||
size: 'two',
|
||||
type: 'primary'
|
||||
}" />
|
||||
<Avatarbox
|
||||
v-bind="{
|
||||
text: v.user__fullname[0].toUpperCase(),
|
||||
size: 'two',
|
||||
type: 'primary',
|
||||
}"
|
||||
/>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div>
|
||||
@@ -22,14 +28,20 @@
|
||||
</p>
|
||||
<p class="mt-1 fs-14 has-text-grey">
|
||||
<span class="icon-text">
|
||||
<span>{{v.user__fullname}}</span>
|
||||
<span class="ml-3">{{ $dayjs(v['create_time']).fromNow(true) }}</span>
|
||||
<template v-if="login.id===v.user">
|
||||
<a class="ml-3" @click="edit(v)">
|
||||
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 20}"></SvgIcon>
|
||||
<span>{{ v.user__fullname }}</span>
|
||||
<span class="ml-3">{{ $dayjs(v["create_time"]).fromNow(true) }}</span>
|
||||
<template v-if="login.id === v.user">
|
||||
<a
|
||||
class="ml-3"
|
||||
@click="edit(v)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'gray', size: 20 }"></SvgIcon>
|
||||
</a>
|
||||
<a class="ml-3" @click="askConfirm(v, i)">
|
||||
<SvgIcon v-bind="{name: 'bin.svg', type: 'gray', size: 20}"></SvgIcon>
|
||||
<a
|
||||
class="ml-3"
|
||||
@click="askConfirm(v, i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'bin.svg', type: 'gray', size: 20 }"></SvgIcon>
|
||||
</a>
|
||||
</template>
|
||||
</span>
|
||||
@@ -39,87 +51,126 @@
|
||||
</article>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="$getEditRights()" class="field is-grouped mt-3">
|
||||
<div
|
||||
v-if="$getEditRights()"
|
||||
class="field is-grouped mt-3"
|
||||
>
|
||||
<div class="control is-expanded">
|
||||
<textarea class="textarea" rows="2" placeholder="Viết ghi chú tại đây" v-model="detail"></textarea>
|
||||
<textarea
|
||||
class="textarea"
|
||||
rows="2"
|
||||
placeholder="Viết ghi chú tại đây"
|
||||
v-model="detail"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary has-text-white" @click="save()">Lưu</button>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="save()"
|
||||
>
|
||||
Lưu
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" @confirm="confirm()"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
@confirm="confirm()"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['row', 'api', 'pagename'],
|
||||
props: ["row", "api", "pagename"],
|
||||
data() {
|
||||
return {
|
||||
data: undefined,
|
||||
detail: undefined,
|
||||
vbind2: {image: undefined, text: 'ABC', size: 'two', type: 'findata'},
|
||||
vbind2: { image: undefined, text: "ABC", size: "two", type: "findata" },
|
||||
current: undefined,
|
||||
showmodal: undefined,
|
||||
obj: undefined
|
||||
}
|
||||
obj: undefined,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if(!this.row) return
|
||||
this.data = await this.$getdata(this.api, {ref: this.row.id})
|
||||
if (!this.row) return;
|
||||
this.data = await this.$getdata(this.api, { ref: this.row.id });
|
||||
},
|
||||
computed: {
|
||||
login: {
|
||||
get: function() {return this.$store.login},
|
||||
set: function(val) {this.$store.commit("updateLogin", {login: val})}
|
||||
}
|
||||
get: function () {
|
||||
return this.$store.login;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit("updateLogin", { login: val });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
if(this.$empty(this.detail)) return this.$snackbar('Chưa nhập nội dung ghi chú')
|
||||
let data = {user: this.$store.login.id, detail: this.detail, ref: this.row.id}
|
||||
if(this.current) {
|
||||
data = this.$copy(this.current)
|
||||
data.detail = this.detail
|
||||
if (this.$empty(this.detail)) return this.$snackbar("Chưa nhập nội dung ghi chú");
|
||||
let data = {
|
||||
user: this.$store.login.id,
|
||||
detail: this.detail,
|
||||
ref: this.row.id,
|
||||
};
|
||||
if (this.current) {
|
||||
data = this.$copy(this.current);
|
||||
data.detail = this.detail;
|
||||
}
|
||||
let rs = data.id? await this.$updateapi(this.api, data) : await this.$insertapi(this.api, data)
|
||||
if(!rs) return
|
||||
this.detail = undefined
|
||||
if(this.current) {
|
||||
this.current = undefined
|
||||
let idx = this.$findIndex(this.data, {id: rs.id})
|
||||
this.$set(this.data, idx, rs)
|
||||
let rs = data.id ? await this.$updateapi(this.api, data) : await this.$insertapi(this.api, data);
|
||||
if (!rs) return;
|
||||
this.detail = undefined;
|
||||
if (this.current) {
|
||||
this.current = undefined;
|
||||
let idx = this.$findIndex(this.data, { id: rs.id });
|
||||
this.$set(this.data, idx, rs);
|
||||
} else {
|
||||
this.data.push(rs)
|
||||
let rows = this.$copy(this.$store[this.pagename].data)
|
||||
let idx = this.$findIndex(rows, {id: this.row.id})
|
||||
let copy = this.$copy(this.row)
|
||||
copy.count_note += 1
|
||||
rows[idx] = copy
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}})
|
||||
this.data.push(rs);
|
||||
let rows = this.$copy(this.$store[this.pagename].data);
|
||||
let idx = this.$findIndex(rows, { id: this.row.id });
|
||||
let copy = this.$copy(this.row);
|
||||
copy.count_note += 1;
|
||||
rows[idx] = copy;
|
||||
this.$store.commit("updateState", {
|
||||
name: this.pagename,
|
||||
key: "update",
|
||||
data: { data: rows },
|
||||
});
|
||||
}
|
||||
},
|
||||
edit(v) {
|
||||
this.current = this.$copy(v)
|
||||
this.detail = v.detail
|
||||
this.current = this.$copy(v);
|
||||
this.detail = v.detail;
|
||||
},
|
||||
askConfirm(v, i) {
|
||||
this.obj = {v: v, i: i}
|
||||
this.showmodal = {component: `dialog/Confirm`,vbind: {content: 'Bạn có muốn xóa ghi chú này không?', duration: 10},
|
||||
title: 'Xóa ghi chú', width: '500px', height: '100px'}
|
||||
this.obj = { v: v, i: i };
|
||||
this.showmodal = {
|
||||
component: `dialog/Confirm`,
|
||||
vbind: { content: "Bạn có muốn xóa ghi chú này không?", duration: 10 },
|
||||
title: "Xóa ghi chú",
|
||||
width: "500px",
|
||||
height: "100px",
|
||||
};
|
||||
},
|
||||
async confirm() {
|
||||
let v = this.obj.v
|
||||
let i = this.obj.i
|
||||
let rs = await this.$deleteapi(this.api, v.id)
|
||||
if(rs==='error') return
|
||||
this.$delete(this.data, i)
|
||||
let rows = this.$copy(this.$store[this.pagename].data)
|
||||
let idx = this.$findIndex(rows, {id: this.row.id})
|
||||
let copy = this.$copy(this.row)
|
||||
copy.count_note -= 1
|
||||
rows[idx] = copy
|
||||
this.$store.commit('updateState', {name: this.pagename, key: 'update', data: {data: rows}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
let v = this.obj.v;
|
||||
let i = this.obj.i;
|
||||
let rs = await this.$deleteapi(this.api, v.id);
|
||||
if (rs === "error") return;
|
||||
this.$delete(this.data, i);
|
||||
let rows = this.$copy(this.$store[this.pagename].data);
|
||||
let idx = this.$findIndex(rows, { id: this.row.id });
|
||||
let copy = this.$copy(this.row);
|
||||
copy.count_note -= 1;
|
||||
rows[idx] = copy;
|
||||
this.$store.commit("updateState", {
|
||||
name: this.pagename,
|
||||
key: "update",
|
||||
data: { data: rows },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="row.count_note || $getEditRights()"
|
||||
class="dot-primary"
|
||||
<span
|
||||
v-if="row.count_note || $getEditRights()"
|
||||
class="dot-primary"
|
||||
@click="doClick()"
|
||||
>{{ row.count_note || '+' }}</span>
|
||||
>{{ row.count_note || "+" }}</span
|
||||
>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['row', 'api', 'pagename'],
|
||||
props: ["row", "api", "pagename"],
|
||||
methods: {
|
||||
doClick() {
|
||||
let obj = {component: 'common/NoteInfo', title: 'Ghi chú', width: '50%', vbind: {row: this.row, api: this.api, pagename: this.pagename}}
|
||||
this.$emit('open', {name: 'dataevent', data: {modal: obj}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
let obj = {
|
||||
component: "common/NoteInfo",
|
||||
title: "Ghi chú",
|
||||
width: "50%",
|
||||
vbind: { row: this.row, api: this.api, pagename: this.pagename },
|
||||
};
|
||||
this.$emit("open", { name: "dataevent", data: { modal: obj } });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,54 +1,82 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="fsb-30">{{ text }}
|
||||
<a class="ml-3" @click="copy()">
|
||||
<SvgIcon v-bind="{name: 'copy.svg', type: 'primary', size: 24}"></SvgIcon>
|
||||
<p class="fsb-30">
|
||||
{{ text }}
|
||||
<a
|
||||
class="ml-3"
|
||||
@click="copy()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 24 }"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
<p class="buttons mt-4">
|
||||
<button class="button is-primary" @click="call()">Call</button>
|
||||
<button class="button is-primary" @click="sms()">SMS</button>
|
||||
<button class="button is-primary" @click="openZalo()">Zalo</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
@click="call()"
|
||||
>
|
||||
Call
|
||||
</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
@click="sms()"
|
||||
>
|
||||
SMS
|
||||
</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
@click="openZalo()"
|
||||
>
|
||||
Zalo
|
||||
</button>
|
||||
</p>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['row', 'pagename'],
|
||||
props: ["row", "pagename"],
|
||||
data() {
|
||||
return {
|
||||
text: undefined,
|
||||
phone: this.row.customer__phone || this.row.party__phone || this.row.phone,
|
||||
showmodal: undefined
|
||||
}
|
||||
showmodal: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
var format = function(s) {
|
||||
return `${s.slice(0,3)} ${s.slice(3,6)} ${s.slice(6, 20)}`
|
||||
}
|
||||
this.text = format(this.phone)
|
||||
var format = function (s) {
|
||||
return `${s.slice(0, 3)} ${s.slice(3, 6)} ${s.slice(6, 20)}`;
|
||||
};
|
||||
this.text = format(this.phone);
|
||||
},
|
||||
methods: {
|
||||
call() {
|
||||
window.open(`tel:${this.phone}`)
|
||||
window.open(`tel:${this.phone}`);
|
||||
},
|
||||
sms() {
|
||||
window.open(`sms:${this.phone}`)
|
||||
window.open(`sms:${this.phone}`);
|
||||
},
|
||||
sendSms() {
|
||||
let api = this.row.code.indexOf('CN')>=0? 'customersms' : undefined
|
||||
if(this.row.code.indexOf('LN')>=0) api = 'loansms'
|
||||
else if(this.row.code.indexOf('TS')>=0) api = 'collateralsms'
|
||||
this.showmodal = {component: 'user/Sms', title: 'Nhắn tin SMS', width: '50%', height: '400px',
|
||||
vbind: {row: this.row, pagename: this.pagename, api: api}}
|
||||
let api = this.row.code.indexOf("CN") >= 0 ? "customersms" : undefined;
|
||||
if (this.row.code.indexOf("LN") >= 0) api = "loansms";
|
||||
else if (this.row.code.indexOf("TS") >= 0) api = "collateralsms";
|
||||
this.showmodal = {
|
||||
component: "user/Sms",
|
||||
title: "Nhắn tin SMS",
|
||||
width: "50%",
|
||||
height: "400px",
|
||||
vbind: { row: this.row, pagename: this.pagename, api: api },
|
||||
};
|
||||
},
|
||||
copy() {
|
||||
this.$copyToClipboard(this.phone)
|
||||
this.$copyToClipboard(this.phone);
|
||||
},
|
||||
openZalo() {
|
||||
window.open(`https://zalo.me/${this.phone}`, '_blank')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
window.open(`https://zalo.me/${this.phone}`, "_blank");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
|
||||
<template>
|
||||
<span class="dot-primary" @click="onClick()">{{ row.count_product }}</span>
|
||||
<span
|
||||
class="dot-primary"
|
||||
@click="onClick()"
|
||||
>{{ row.count_product }}</span
|
||||
>
|
||||
</template>
|
||||
<script>
|
||||
// use in Khách hàng -> Giao dịch (<DataView :setting='customer-all-transaction'/>)
|
||||
export default {
|
||||
props: ['row', 'api', 'pagename'],
|
||||
props: ["row", "api", "pagename"],
|
||||
methods: {
|
||||
onClick() {
|
||||
const obj = {
|
||||
component: 'common/ProductInfo',
|
||||
title: 'Sản phẩm',
|
||||
width: '60%',
|
||||
height: '400px',
|
||||
component: "common/ProductInfo",
|
||||
title: "Sản phẩm",
|
||||
width: "60%",
|
||||
height: "400px",
|
||||
vbind: {
|
||||
row: this.row,
|
||||
pagename: this.pagename
|
||||
}
|
||||
}
|
||||
this.$emit('open', {name: 'dataevent', data: { modal: obj }})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
pagename: this.pagename,
|
||||
},
|
||||
};
|
||||
this.$emit("open", { name: "dataevent", data: { modal: obj } });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
pagename: String
|
||||
pagename: String,
|
||||
});
|
||||
const { $id } = useNuxtApp();
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<DataView v-bind="{
|
||||
setting: 'product-info',
|
||||
pagename: $id(),
|
||||
api: 'product',
|
||||
params: {
|
||||
filter: { prdbk__transaction__customer: props.row.id },
|
||||
// copied from 02-connection.js
|
||||
values: 'price_excluding_vat,prdbk__transaction__txncurrent__detail__status__name,locked_until,note,cart,cart__name,cart__code,cart__dealer,cart__dealer__code,cart__dealer__name,direction,type,zone_type,dealer,link,type__name,dealer__code,dealer__name,prdbk,prdbk__transaction__customer,prdbk__transaction,prdbk__transaction__policy__code,prdbk__transaction__sale_price,prdbk__transaction__discount_amount,prdbk__transaction__code,prdbk__transaction__customer__code,prdbk__transaction__customer__phone,prdbk__transaction__customer__fullname,prdbk__transaction__customer__legal_code,id,code,trade_code,land_lot_code,zone_code,zone_type__name,lot_area,building_area,total_built_area,number_of_floors,land_lot_size,origin_price,direction__name,villa_model,product_type,template_name,project,project__name,status,status__code,status__name,status__color,status__sale_status,status__sale_status__color,create_time,prdbk__transaction__amount_received,prdbk__transaction__amount_remain',
|
||||
}
|
||||
}" />
|
||||
<DataView
|
||||
v-bind="{
|
||||
setting: 'product-info',
|
||||
pagename: $id(),
|
||||
api: 'product',
|
||||
params: {
|
||||
filter: { prdbk__transaction__customer: props.row.id },
|
||||
// copied from 02-connection.js
|
||||
values:
|
||||
'price_excluding_vat,prdbk__transaction__txncurrent__detail__status__name,locked_until,note,cart,cart__name,cart__code,cart__dealer,cart__dealer__code,cart__dealer__name,direction,type,zone_type,dealer,link,type__name,dealer__code,dealer__name,prdbk,prdbk__transaction__customer,prdbk__transaction,prdbk__transaction__policy__code,prdbk__transaction__sale_price,prdbk__transaction__discount_amount,prdbk__transaction__code,prdbk__transaction__customer__code,prdbk__transaction__customer__phone,prdbk__transaction__customer__fullname,prdbk__transaction__customer__legal_code,id,code,trade_code,land_lot_code,zone_code,zone_type__name,lot_area,building_area,total_built_area,number_of_floors,land_lot_size,origin_price,direction__name,villa_model,product_type,template_name,project,project__name,status,status__code,status__name,status__color,status__sale_status,status__sale_status__color,create_time,prdbk__transaction__amount_received,prdbk__transaction__amount_remain',
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
<template>
|
||||
<div class="has-text-centered">
|
||||
<div class="mb-4">
|
||||
<p v-if="row && row.fullname"><b>{{row.fullname}}</b></p>
|
||||
<p v-if="row && row.fullname">
|
||||
<b>{{ row.fullname }}</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-2 px-5 is-flex is-justify-content-center">
|
||||
<ClientOnly>
|
||||
<Qrcode
|
||||
v-if="finalLink"
|
||||
:key="finalLink"
|
||||
v-if="finalLink"
|
||||
:key="finalLink"
|
||||
id="qrcode"
|
||||
:value="finalLink"
|
||||
:size="300"
|
||||
/>
|
||||
</ClientOnly>
|
||||
<div v-if="!finalLink" style="width: 300px; height: 300px; border: 1px dashed #ccc; display: flex; align-items: center; justify-content: center; color: #888;">
|
||||
<div
|
||||
v-if="!finalLink"
|
||||
style="
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border: 1px dashed #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #888;
|
||||
"
|
||||
>
|
||||
Không có dữ liệu để tạo QR Code
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-2 is-flex is-justify-content-center is-gap-1">
|
||||
<a
|
||||
@click="openLink()"
|
||||
<a
|
||||
@click="openLink()"
|
||||
class="button is-light is-link is-rounded"
|
||||
:title="isVietnamese ? 'Mở đường dẫn liên kết' : 'Open external link'"
|
||||
v-if="finalLink"
|
||||
@@ -31,8 +44,8 @@
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
@click="download()"
|
||||
<a
|
||||
@click="download()"
|
||||
class="button is-light is-link is-rounded"
|
||||
:title="isVietnamese ? 'Tải Xuống QR Code' : 'Download QR Code'"
|
||||
v-if="finalLink"
|
||||
@@ -45,83 +58,85 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
const store = useStore();
|
||||
const isVietnamese = computed(() => store.lang === "vi")
|
||||
import { computed } from "vue";
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
const { $getpath, $snackbar } = useNuxtApp()
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
link: String
|
||||
})
|
||||
const store = useStore();
|
||||
const isVietnamese = computed(() => store.lang === "vi");
|
||||
|
||||
const finalLink = computed(() => {
|
||||
if (props.link) {
|
||||
return props.link
|
||||
}
|
||||
if (props.row && props.row.code) {
|
||||
const path = $getpath()
|
||||
const baseUrl = path ? path.replace('api.', '') : '';
|
||||
return `${baseUrl}loan/${props.row.code}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
const { $getpath, $snackbar } = useNuxtApp();
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
link: String,
|
||||
});
|
||||
|
||||
function openLink() {
|
||||
if (finalLink.value) {
|
||||
window.open(finalLink.value, "_blank")
|
||||
}
|
||||
const finalLink = computed(() => {
|
||||
if (props.link) {
|
||||
return props.link;
|
||||
}
|
||||
if (props.row && props.row.code) {
|
||||
const path = $getpath();
|
||||
const baseUrl = path ? path.replace("api.", "") : "";
|
||||
return `${baseUrl}loan/${props.row.code}`;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
function openLink() {
|
||||
if (finalLink.value) {
|
||||
window.open(finalLink.value, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function download() {
|
||||
if (!finalLink.value) return;
|
||||
|
||||
let svg = document.getElementById("qrcode");
|
||||
let attempts = 0;
|
||||
|
||||
while (!svg && attempts < 5) {
|
||||
await sleep(100);
|
||||
svg = document.getElementById("qrcode");
|
||||
attempts++;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
if (!svg) {
|
||||
console.error("QR Code SVG element not found after waiting.");
|
||||
$snackbar(isVietnamese.value ? "Không tìm thấy mã QR để tải xuống." : "QR Code not found for download.", {
|
||||
type: "is-danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
async function download() {
|
||||
if (!finalLink.value) return;
|
||||
const serializer = new XMLSerializer();
|
||||
const svgData = serializer.serializeToString(svg);
|
||||
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
let svg = document.getElementById('qrcode');
|
||||
let attempts = 0;
|
||||
|
||||
while (!svg && attempts < 5) {
|
||||
await sleep(100);
|
||||
svg = document.getElementById('qrcode');
|
||||
attempts++;
|
||||
}
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(image, 0, 0, 300, 300);
|
||||
|
||||
if (!svg) {
|
||||
console.error("QR Code SVG element not found after waiting.");
|
||||
$snackbar(isVietnamese.value ? 'Không tìm thấy mã QR để tải xuống.' : 'QR Code not found for download.', { type: 'is-danger' });
|
||||
return;
|
||||
}
|
||||
const pngUrl = canvas.toDataURL("image/png");
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
const svgData = serializer.serializeToString(svg)
|
||||
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
|
||||
const url = URL.createObjectURL(svgBlob)
|
||||
const linkElement = document.createElement("a");
|
||||
linkElement.href = pngUrl;
|
||||
|
||||
const image = new Image()
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 300
|
||||
canvas.height = 300
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(image, 0, 0, 300, 300)
|
||||
const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : "qrcode.png";
|
||||
linkElement.download = filename;
|
||||
linkElement.click();
|
||||
|
||||
const pngUrl = canvas.toDataURL('image/png')
|
||||
|
||||
const linkElement = document.createElement('a')
|
||||
linkElement.href = pngUrl
|
||||
|
||||
const filename = props.row && props.row.code ? `qrcode-${props.row.code}.png` : 'qrcode.png'
|
||||
linkElement.download = filename
|
||||
linkElement.click()
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
$snackbar(isVietnamese.value ? 'Đã tải xuống mã QR!' : 'QR Code downloaded!', { type: 'is-success' });
|
||||
}
|
||||
image.src = url
|
||||
}
|
||||
</script>
|
||||
URL.revokeObjectURL(url);
|
||||
$snackbar(isVietnamese.value ? "Đã tải xuống mã QR!" : "QR Code downloaded!", { type: "is-success" });
|
||||
};
|
||||
image.src = url;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<DataView v-if="vbind" v-bind="vbind"></DataView>
|
||||
<DataView
|
||||
v-if="vbind"
|
||||
v-bind="vbind"
|
||||
></DataView>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['vbind']
|
||||
}
|
||||
</script>
|
||||
props: ["vbind"],
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,175 +1,279 @@
|
||||
<template>
|
||||
<div v-if="record">
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div :class="`column is-3 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{findLang('code')}}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input has-text-black" disabled type="text" placeholder="" v-model="record.code">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{ findLang("code") }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input has-text-black"
|
||||
disabled
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.code"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.code"
|
||||
>
|
||||
{{ errors.code }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="`column is-6 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.fullname">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.fullname">{{errors.fullname}}</p>
|
||||
</div>
|
||||
<div :class="`column is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Tên công ty<b class="ml-1 has-text-danger">*</b></label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.fullname"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.fullname"
|
||||
>
|
||||
{{ errors.fullname }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{ findLang("shortname") }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.shortname"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.shortname"
|
||||
>
|
||||
{{ errors.shortname }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{ findLang("taxcode") }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.legal_code"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.legal_code"
|
||||
>
|
||||
{{ errors.legal_code }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Điện thoại</label>
|
||||
<div class="control">
|
||||
<InputPhone
|
||||
v-bind="{ record: record, attr: 'phone', placeholder: '' }"
|
||||
@phone="selected('phone', $event)"
|
||||
></InputPhone>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.phone"
|
||||
>
|
||||
{{ errors.phone }}
|
||||
<a
|
||||
v-if="existedCustomer"
|
||||
@click="showCustomer()"
|
||||
>Chi tiết</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Email</label>
|
||||
<div class="control">
|
||||
<InputEmail
|
||||
v-bind="{ record: record, attr: 'email', placeholder: '' }"
|
||||
@email="selected('email', $event)"
|
||||
></InputEmail>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.email"
|
||||
>
|
||||
{{ errors.email }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Website</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.website"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{ findLang("country") }}</label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: store.country,
|
||||
field: 'name',
|
||||
column: ['name'],
|
||||
first: true,
|
||||
optionid: record.country,
|
||||
position: 'is-top-left',
|
||||
}"
|
||||
@option="selected('_country', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{ findLang("province") }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.province"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-12 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{ findLang("address") }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.address"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.address"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-3 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{findLang('shortname')}}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.shortname">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.shortname">{{errors.shortname}}</p>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="update()"
|
||||
>
|
||||
{{ findLang("save") }}
|
||||
</button>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{findLang('taxcode')}}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.legal_code">
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.legal_code">{{errors.legal_code}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Điện thoại</label>
|
||||
<div class="control">
|
||||
<InputPhone v-bind="{record: record, attr: 'phone', placeholder: ''}" @phone="selected('phone', $event)"></InputPhone>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.phone">{{ errors.phone }}
|
||||
<a v-if="existedCustomer" @click="showCustomer()">Chi tiết</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Email</label>
|
||||
<div class="control">
|
||||
<InputEmail v-bind="{record: record, attr: 'email', placeholder: ''}" @email="selected('email', $event)"></InputEmail>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Website</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.website">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{findLang('country')}}</label>
|
||||
<div class="control">
|
||||
<SearchBox v-bind="{vdata: store.country, field:'name', column:['name'], first:true, optionid: record.country, position: 'is-top-left'}"
|
||||
@option="selected('_country', $event)"></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{findLang('province')}}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.province">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-12 ${viewport===1? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{findLang('address')}}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="record.address">
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.address"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="button is-primary has-text-white" @click="update()">{{ findLang('save') }}</button>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import InputPhone from '~/components/common/InputPhone'
|
||||
import InputEmail from '~/components/common/InputEmail'
|
||||
import SearchBox from '~/components/SearchBox'
|
||||
import { useStore } from '@/stores/index'
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
row: Object
|
||||
})
|
||||
const store = useStore()
|
||||
const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } = useNuxtApp()
|
||||
const emit = defineEmits(['update', 'dataevent'])
|
||||
var viewport = store.viewport
|
||||
var errors = ref({})
|
||||
var record = ref()
|
||||
var showmodal = undefined
|
||||
var existedCustomer = undefined
|
||||
async function initData() {
|
||||
if(props.row) {
|
||||
let conn = $findapi('company')
|
||||
conn.params.filter = {id: props.row.company || props.row.customer__company || props.row.id}
|
||||
let rs = await $getapi([conn])
|
||||
let found = $find(rs, {name: 'company'})
|
||||
if(found.data.rows.length>0) record.value = found.data.rows[0]
|
||||
} else {
|
||||
record.value = {}
|
||||
import InputPhone from "~/components/common/InputPhone";
|
||||
import InputEmail from "~/components/common/InputEmail";
|
||||
import SearchBox from "~/components/SearchBox";
|
||||
import { useStore } from "@/stores/index";
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
row: Object,
|
||||
});
|
||||
const store = useStore();
|
||||
const { $find, $getdata, $updateapi, $insertapi, $findapi, $getapi, $empty, $errPhone, $resetNull, $snackbar } =
|
||||
useNuxtApp();
|
||||
const emit = defineEmits(["update", "dataevent"]);
|
||||
var viewport = store.viewport;
|
||||
var errors = ref({});
|
||||
var record = ref();
|
||||
var showmodal = undefined;
|
||||
var existedCustomer = undefined;
|
||||
async function initData() {
|
||||
if (props.row) {
|
||||
let conn = $findapi("company");
|
||||
conn.params.filter = {
|
||||
id: props.row.company || props.row.customer__company || props.row.id,
|
||||
};
|
||||
let rs = await $getapi([conn]);
|
||||
let found = $find(rs, { name: "company" });
|
||||
if (found.data.rows.length > 0) record.value = found.data.rows[0];
|
||||
} else {
|
||||
record.value = {};
|
||||
}
|
||||
}
|
||||
function findLang(code) {
|
||||
let found = $find(store.common, { code: code });
|
||||
return found ? found[store.lang] : "";
|
||||
}
|
||||
function showCustomer() {
|
||||
showmodal.value = {
|
||||
component: "customer/CustomerView",
|
||||
width: "60%",
|
||||
height: "600px",
|
||||
title: "Khách hàng",
|
||||
vbind: { row: existedCustomer },
|
||||
};
|
||||
}
|
||||
function selected(attr, obj) {
|
||||
record.value[attr] = obj;
|
||||
}
|
||||
function checkError() {
|
||||
existedCustomer = undefined;
|
||||
errors.value = {};
|
||||
if ($empty(record.value.fullname)) errors.value.fullname = "Họ tên không được bỏ trống";
|
||||
if (record.value.phone) {
|
||||
let text = $errPhone(record.value.phone);
|
||||
if (text) errors.value.phone = text;
|
||||
}
|
||||
return Object.keys(errors.value).length > 0;
|
||||
}
|
||||
async function update() {
|
||||
if (checkError()) return;
|
||||
if (!record.value.id) {
|
||||
if (record.value.phone) record.value.phone = record.value.phone.trim();
|
||||
let obj = await $getdata("company", { phone: record.value.phone }, undefined, true);
|
||||
if (obj) {
|
||||
existedCustomer = obj;
|
||||
errors.phone = "Số điện thoại đã tồn tại trong hệ thống.";
|
||||
}
|
||||
}
|
||||
function findLang(code) {
|
||||
let found = $find(store.common, {code: code})
|
||||
return found? found[store.lang] : ''
|
||||
}
|
||||
function showCustomer() {
|
||||
showmodal.value = {component: 'customer/CustomerView', width: '60%', height: '600px', title: 'Khách hàng', vbind: {row: existedCustomer}}
|
||||
}
|
||||
function selected(attr, obj) {
|
||||
record.value[attr] = obj
|
||||
}
|
||||
function checkError() {
|
||||
existedCustomer = undefined
|
||||
errors.value = {}
|
||||
if($empty(record.value.fullname)) errors.value.fullname = 'Họ tên không được bỏ trống'
|
||||
if(record.value.phone) {
|
||||
let text = $errPhone(record.value.phone)
|
||||
if(text) errors.value.phone = text
|
||||
}
|
||||
return Object.keys(errors.value).length>0
|
||||
}
|
||||
async function update() {
|
||||
if(checkError()) return
|
||||
if(!record.value.id) {
|
||||
if(record.value.phone) record.value.phone = record.value.phone.trim()
|
||||
let obj = await $getdata('company', {phone: record.value.phone}, undefined, true)
|
||||
if(obj) {
|
||||
existedCustomer = obj
|
||||
errors.phone = 'Số điện thoại đã tồn tại trong hệ thống.'
|
||||
}
|
||||
}
|
||||
record.value = $resetNull(record.value)
|
||||
if(record.value._country) record.value.country = record.value._country.id
|
||||
if(!record.value.creator) record.value.creator = store.login.id
|
||||
record.value.updater = store.login.id
|
||||
record.update_time = new Date()
|
||||
let rs = record.value.id? await $updateapi('company', record.value)
|
||||
: await $insertapi('company', record.value)
|
||||
if(rs==='error') return
|
||||
if(!record.value.id) $snackbar(`Khách hàng đã được khởi tạo với mã <b>${rs.code}</b>`, 'Thành công', 'Success')
|
||||
record.value.id = rs.id
|
||||
let ele = await $getdata('company', {id:rs.id}, null, true)
|
||||
emit('update', ele)
|
||||
emit('modalevent', {name: 'dataevent', data: ele})
|
||||
}
|
||||
initData()
|
||||
</script>
|
||||
record.value = $resetNull(record.value);
|
||||
if (record.value._country) record.value.country = record.value._country.id;
|
||||
if (!record.value.creator) record.value.creator = store.login.id;
|
||||
record.value.updater = store.login.id;
|
||||
record.update_time = new Date();
|
||||
let rs = record.value.id ? await $updateapi("company", record.value) : await $insertapi("company", record.value);
|
||||
if (rs === "error") return;
|
||||
if (!record.value.id) $snackbar(`Khách hàng đã được khởi tạo với mã <b>${rs.code}</b>`, "Thành công", "Success");
|
||||
record.value.id = rs.id;
|
||||
let ele = await $getdata("company", { id: rs.id }, null, true);
|
||||
emit("update", ele);
|
||||
emit("modalevent", { name: "dataevent", data: ele });
|
||||
}
|
||||
initData();
|
||||
</script>
|
||||
|
||||
@@ -10,14 +10,21 @@
|
||||
v-for="(v, i) in tabs"
|
||||
:key="i"
|
||||
:class="['is-clickable p-3', i !== 0 && 'mt-2', getStyle(v)]"
|
||||
style="width: 130px; border-radius: 4px;"
|
||||
style="width: 130px; border-radius: 4px"
|
||||
@click="changeTab(v)"
|
||||
>
|
||||
{{ isVietnamese ? v.name : v.en }}
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="field is-grouped is-grouped-multiline">
|
||||
<div class="control" v-for="(v, i) in tabs" @click="changeTab(v)">
|
||||
<div
|
||||
v-else
|
||||
class="field is-grouped is-grouped-multiline"
|
||||
>
|
||||
<div
|
||||
class="control"
|
||||
v-for="(v, i) in tabs"
|
||||
@click="changeTab(v)"
|
||||
>
|
||||
<div style="width: 130px">
|
||||
<div :class="`py-3 px-3 ${getStyle(v)}`">
|
||||
{{ isVietnamese ? v.name : v.en }}
|
||||
@@ -54,7 +61,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal @close="handleModalClose" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="handleModalClose"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
@@ -99,11 +110,7 @@ function getStyle(v) {
|
||||
|
||||
function changeTab(v) {
|
||||
if (tab.value === v.code) return;
|
||||
if (!record)
|
||||
return $dialog(
|
||||
"Vui lòng <b>lưu dữ liệu</b> trước khi chuyển sang mục tiếp theo",
|
||||
"Thông báo"
|
||||
);
|
||||
if (!record) return $dialog("Vui lòng <b>lưu dữ liệu</b> trước khi chuyển sang mục tiếp theo", "Thông báo");
|
||||
tab.value = v.code;
|
||||
}
|
||||
|
||||
@@ -115,7 +122,7 @@ function update(v) {
|
||||
record = {
|
||||
...v,
|
||||
label: `${v.code} / ${v.fullname} / ${v.phone || ""}`,
|
||||
}
|
||||
};
|
||||
emit("modalevent", { name: "dataevent", data: record });
|
||||
if (!props.isEditMode) emit("close");
|
||||
}
|
||||
@@ -123,8 +130,7 @@ function update(v) {
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif !important;
|
||||
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
|
||||
}
|
||||
.button.is-large.is-fullwidth:hover {
|
||||
color: white !important;
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("custcode")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("custcode")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
@@ -15,19 +14,30 @@
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.code"
|
||||
>
|
||||
{{ errors.code }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("name")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("name")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.fullname">
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.fullname"
|
||||
>
|
||||
{{ errors.fullname }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -35,8 +45,7 @@
|
||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("phone_number")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<InputPhone
|
||||
@@ -45,7 +54,10 @@
|
||||
>
|
||||
</InputPhone>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.phone">
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.phone"
|
||||
>
|
||||
{{ errors.phone }}
|
||||
<a
|
||||
class="has-text-primary"
|
||||
@@ -66,14 +78,18 @@
|
||||
>
|
||||
</InputEmail>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.email"
|
||||
>
|
||||
{{ errors.email }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("gender")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("gender")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
@@ -93,8 +109,7 @@
|
||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("birth_date")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("birth_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<Datepicker
|
||||
@@ -103,14 +118,18 @@
|
||||
>
|
||||
</Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.dob">{{ errors.dob }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.dob"
|
||||
>
|
||||
{{ errors.dob }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("country")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("country")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
@@ -130,8 +149,7 @@
|
||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("province")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("province")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
@@ -146,8 +164,7 @@
|
||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("district")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("district")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
@@ -162,8 +179,7 @@
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("address")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("address")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
@@ -173,14 +189,15 @@
|
||||
v-model="record.address"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.address"></p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.address"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-5 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{
|
||||
dataLang && findFieldName("company")[lang]
|
||||
}}</label>
|
||||
<label class="label">{{ dataLang && findFieldName("company")[lang] }}</label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
@@ -201,8 +218,7 @@
|
||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("personal_id")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("personal_id")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
@@ -222,8 +238,7 @@
|
||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("idnum")[lang] }}
|
||||
<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("idnum")[lang] }} <b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
@@ -233,7 +248,10 @@
|
||||
v-model="record.legal_code"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.legal_code">
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.legal_code"
|
||||
>
|
||||
{{ errors.legal_code }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -241,8 +259,7 @@
|
||||
<div :class="`column is-2 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("issued_date")[lang] }}
|
||||
<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("issued_date")[lang] }} <b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<Datepicker
|
||||
@@ -255,7 +272,10 @@
|
||||
@date="selected('issued_date', $event)"
|
||||
></Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_date">
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_date"
|
||||
>
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -263,8 +283,7 @@
|
||||
<div :class="`column is-4 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("issued_place")[lang] }}
|
||||
<b class="ml-1 has-text-danger">*</b></label
|
||||
>{{ dataLang && findFieldName("issued_place")[lang] }} <b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
@@ -274,7 +293,10 @@
|
||||
v-model="record.issued_place"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_place"></p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_place"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-3 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
@@ -306,9 +328,7 @@
|
||||
|
||||
<div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{
|
||||
dataLang && findFieldName("note")[lang]
|
||||
}}</label>
|
||||
<label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
class="textarea"
|
||||
@@ -317,7 +337,12 @@
|
||||
rows="1"
|
||||
></textarea>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.note">{{ errors.note }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.note"
|
||||
>
|
||||
{{ errors.note }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,11 +355,15 @@
|
||||
}"
|
||||
></Caption>
|
||||
</div>
|
||||
<div class="columns mb-0 mx-0" v-for="(v, i) in people">
|
||||
<div
|
||||
class="columns mb-0 mx-0"
|
||||
v-for="(v, i) in people"
|
||||
>
|
||||
<div :class="`column is-7 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<label class="label" v-if="i === 0"
|
||||
>{{ findFieldName("select")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
<label
|
||||
class="label"
|
||||
v-if="i === 0"
|
||||
>{{ findFieldName("select")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
@@ -351,12 +380,18 @@
|
||||
@option="selectPeople($event, v, i)"
|
||||
>
|
||||
</SearchBox>
|
||||
<p class="help is-danger" v-if="v.error">{{ v.error }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="v.error"
|
||||
>
|
||||
{{ v.error }}
|
||||
</p>
|
||||
</div>
|
||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<label class="label" v-if="i === 0"
|
||||
>{{ findFieldName("relationship")[lang]
|
||||
}}<b class="ml-1 has-text-danger">*</b></label
|
||||
<label
|
||||
class="label"
|
||||
v-if="i === 0"
|
||||
>{{ findFieldName("relationship")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
@@ -374,23 +409,30 @@
|
||||
</SearchBox>
|
||||
</div>
|
||||
<div :class="`column ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<label class="label" v-if="i === 0"
|
||||
<label
|
||||
class="label"
|
||||
v-if="i === 0"
|
||||
>{{ findFieldName("addmore")[lang] }}...</label
|
||||
>
|
||||
<button class="button px-2 is-dark mr-3" @click="add()">
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'add1.png', type: 'white', size: 20 }"
|
||||
></SvgIcon>
|
||||
<button
|
||||
class="button px-2 is-dark mr-3"
|
||||
@click="add()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 20 }"></SvgIcon>
|
||||
</button>
|
||||
<button class="button px-2 is-warning" @click="remove(v, i)">
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"
|
||||
></SvgIcon>
|
||||
<button
|
||||
class="button px-2 is-warning"
|
||||
@click="remove(v, i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="button is-primary has-text-white" @click="update()">
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="update()"
|
||||
>
|
||||
{{ store.lang === "en" ? "Save" : "Lưu lại" }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -410,17 +452,7 @@ import Datepicker from "~/components/datepicker/Datepicker";
|
||||
import { useStore } from "@/stores/index";
|
||||
const emit = defineEmits(["close", "update"]);
|
||||
const nuxtApp = useNuxtApp();
|
||||
const {
|
||||
$getdata,
|
||||
$updateapi,
|
||||
$insertapi,
|
||||
$empty,
|
||||
$errPhone,
|
||||
$resetNull,
|
||||
$snackbar,
|
||||
$copy,
|
||||
$dayjs,
|
||||
} = nuxtApp;
|
||||
const { $getdata, $updateapi, $insertapi, $empty, $errPhone, $resetNull, $snackbar, $copy, $dayjs } = nuxtApp;
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
row: Object,
|
||||
@@ -540,10 +572,7 @@ function showCustomer() {
|
||||
}
|
||||
|
||||
const selected = (fieldName, value) => {
|
||||
const finalValue =
|
||||
value !== null && typeof value === "object"
|
||||
? value.id || value.index
|
||||
: value;
|
||||
const finalValue = value !== null && typeof value === "object" ? value.id || value.index : value;
|
||||
record.value[fieldName] = finalValue;
|
||||
|
||||
if (errors.value[fieldName]) {
|
||||
@@ -566,9 +595,7 @@ function checkError() {
|
||||
}
|
||||
|
||||
if ($empty(record.value.dob)) {
|
||||
errors.value.dob = isVietnamese.value
|
||||
? "Ngày sinh không được bỏ trống"
|
||||
: "Date of birth cannot be empty";
|
||||
errors.value.dob = isVietnamese.value ? "Ngày sinh không được bỏ trống" : "Date of birth cannot be empty";
|
||||
} else {
|
||||
if (record.value.dob > new Date()) {
|
||||
errors.value.dob = isVietnamese.value
|
||||
@@ -583,21 +610,15 @@ function checkError() {
|
||||
}
|
||||
|
||||
if ($empty(record.value.legal_code)) {
|
||||
errors.value.legal_code = isVietnamese.value
|
||||
? "Mã số không được bỏ trống"
|
||||
: "Legal code cannot be empty";
|
||||
errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code cannot be empty";
|
||||
}
|
||||
|
||||
if ($empty(record.value.issued_date)) {
|
||||
errors.value.issued_date = isVietnamese.value
|
||||
? "Ngày cấp không được bỏ trống"
|
||||
: "Date of issue cannot be empty";
|
||||
errors.value.issued_date = isVietnamese.value ? "Ngày cấp không được bỏ trống" : "Date of issue cannot be empty";
|
||||
}
|
||||
|
||||
if ($empty(record.value.issued_place)) {
|
||||
errors.value.issued_place = isVietnamese.value
|
||||
? "Nơi cấp không được bỏ trống"
|
||||
: "Place of issue cannot be empty";
|
||||
errors.value.issued_place = isVietnamese.value ? "Nơi cấp không được bỏ trống" : "Place of issue cannot be empty";
|
||||
}
|
||||
return Object.keys(errors.value).length > 0;
|
||||
}
|
||||
@@ -637,45 +658,26 @@ async function update() {
|
||||
try {
|
||||
if (record.value.phone) {
|
||||
record.value.phone = record.value.phone.trim();
|
||||
let phoneCheck = await $getdata(
|
||||
"customer",
|
||||
{ phone: record.value.phone },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
let phoneCheck = await $getdata("customer", { phone: record.value.phone }, undefined, true);
|
||||
if (phoneCheck) {
|
||||
existedCustomer = phoneCheck;
|
||||
errors.value.phone = isVietnamese.value
|
||||
? "Số điện thoại đã tồn tại."
|
||||
: "Phone number already exists.";
|
||||
errors.value.phone = isVietnamese.value ? "Số điện thoại đã tồn tại." : "Phone number already exists.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Kiểm tra email
|
||||
if (record.value.email && record.value.email.trim() !== "") {
|
||||
let emailCheck = await $getdata(
|
||||
"customer",
|
||||
{ email: record.value.email.trim() },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
let emailCheck = await $getdata("customer", { email: record.value.email.trim() }, undefined, true);
|
||||
if (emailCheck) {
|
||||
errors.value.email = isVietnamese.value
|
||||
? "Email đã tồn tại."
|
||||
: "Email already exists.";
|
||||
errors.value.email = isVietnamese.value ? "Email đã tồn tại." : "Email already exists.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Kiểm tra legal_code
|
||||
if (record.value.legal_code) {
|
||||
let legalCheck = await $getdata(
|
||||
"customer",
|
||||
{ legal_code: record.value.legal_code },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
let legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true);
|
||||
if (legalCheck) {
|
||||
errors.value.legal_code = isVietnamese.value
|
||||
? "Số CMND/CCCD đã tồn tại."
|
||||
@@ -714,34 +716,25 @@ async function update() {
|
||||
return;
|
||||
}
|
||||
record.value.id = rs.id;
|
||||
const customerData = await $getdata(
|
||||
"customer",
|
||||
{ id: rs.id },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
const customerData = await $getdata("customer", { id: rs.id }, undefined, true);
|
||||
|
||||
if (isNewCustomer) {
|
||||
// link user=customer
|
||||
await $getdata("linkusercustomer", undefined, { phone: rs.phone });
|
||||
$snackbar(
|
||||
`${
|
||||
isVietnamese.value
|
||||
? "Khách hàng đã được khởi tạo với mã"
|
||||
: "Customer has been created with code"
|
||||
isVietnamese.value ? "Khách hàng đã được khởi tạo với mã" : "Customer has been created with code"
|
||||
} <b>${rs.code}</b>`,
|
||||
"Thành công",
|
||||
"Success"
|
||||
"Success",
|
||||
);
|
||||
} else {
|
||||
$snackbar(
|
||||
`${
|
||||
isVietnamese.value
|
||||
? "Khách hàng đã được cập nhật với mã"
|
||||
: "Customer has been updated with code"
|
||||
isVietnamese.value ? "Khách hàng đã được cập nhật với mã" : "Customer has been updated with code"
|
||||
} <b>${rs.code}</b>`,
|
||||
"Thành công",
|
||||
"Success"
|
||||
"Success",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -760,9 +753,7 @@ async function update() {
|
||||
//Xử lý file uploads nếu có
|
||||
if (record.value.image && record.value.image.length > 0) {
|
||||
let arr = [];
|
||||
record.value.image.map((v) =>
|
||||
arr.push({ ref: record.value.id, file: v })
|
||||
);
|
||||
record.value.image.map((v) => arr.push({ ref: record.value.id, file: v }));
|
||||
await $insertapi("customerfile", arr, undefined, false);
|
||||
}
|
||||
emit("update", customerData);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div v-if="!selectedCustomerType && isNewCustomer && !props.customerType" class="p-5">
|
||||
<div
|
||||
v-if="!selectedCustomerType && isNewCustomer && !props.customerType"
|
||||
class="p-5"
|
||||
>
|
||||
<h3 class="title is-4 mb-5 has-text-centered">
|
||||
{{ isVietnamese ? "Chọn loại khách hàng" : "Select Customer Type" }}
|
||||
</h3>
|
||||
@@ -7,8 +10,8 @@
|
||||
<div class="column is-6">
|
||||
<button
|
||||
:disabled="!$getEditRights('edit', { code: 'individual', category: 'submenu' })"
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 120px"
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 120px"
|
||||
@click="selectCustomerType(1)"
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
@@ -22,10 +25,10 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<button
|
||||
<button
|
||||
:disabled="!$getEditRights('edit', { code: 'org', category: 'submenu' })"
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 120px"
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 120px"
|
||||
@click="selectCustomerType(2)"
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
@@ -46,28 +49,60 @@
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">{{ isIndividual ? "Họ và tên" : "Tên tổ chức" }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<label class="label"
|
||||
>{{ isIndividual ? "Họ và tên" : "Tên tổ chức" }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<div class="control">
|
||||
<input class="input" type="text" v-model="record.fullname" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="record.fullname"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.fullname">{{ errors.fullname }}</p>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.fullname"
|
||||
>
|
||||
{{ errors.fullname }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<InputPhone v-bind="{ record: record, attr: 'phone' }" @phone="selected('phone', $event)"></InputPhone>
|
||||
<p class="help is-danger" v-if="errors.phone">
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("phone_number")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<InputPhone
|
||||
v-bind="{ record: record, attr: 'phone' }"
|
||||
@phone="selected('phone', $event)"
|
||||
></InputPhone>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.phone"
|
||||
>
|
||||
{{ errors.phone }}
|
||||
<a class="has-text-primary" v-if="existedCustomer" @click="showCustomer()">Chi tiết</a>
|
||||
<a
|
||||
class="has-text-primary"
|
||||
v-if="existedCustomer"
|
||||
@click="showCustomer()"
|
||||
>Chi tiết</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Email</label>
|
||||
<InputEmail v-bind="{ record: record, attr: 'email' }" @email="selected('email', $event)"></InputEmail>
|
||||
<p class="help is-danger" v-if="errors.email">{{ errors.email }}</p>
|
||||
<InputEmail
|
||||
v-bind="{ record: record, attr: 'email' }"
|
||||
@email="selected('email', $event)"
|
||||
></InputEmail>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.email"
|
||||
>
|
||||
{{ errors.email }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,28 +110,47 @@
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Địa chỉ liên hệ</label>
|
||||
<input class="input" type="text" v-model="record.contact_address" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="record.contact_address"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">{{ isIndividual ? 'Địa chỉ thường trú' : 'Địa chỉ đăng ký' }}</label>
|
||||
<input class="input" type="text" v-model="record.address" />
|
||||
<label class="label">{{ isIndividual ? "Địa chỉ thường trú" : "Địa chỉ đăng ký" }}</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="record.address"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isOrganization" class="columns is-multiline">
|
||||
<div
|
||||
v-if="isOrganization"
|
||||
class="columns is-multiline"
|
||||
>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Tài khoản ngân hàng</label>
|
||||
<input class="input" type="text" v-model="organizationData.bank_account" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="organizationData.bank_account"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Tên ngân hàng</label>
|
||||
<input class="input" type="text" v-model="organizationData.bank_name" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="organizationData.bank_name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,46 +158,117 @@
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ isIndividual ? 'Giấy tờ tùy thân' : 'Giấy tờ' }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<SearchBox v-bind="{ vdata: filteredLegalTypes, api: 'legaltype', field: isVietnamese ? 'name' : 'en', column: ['name', 'en'], first: true, optionid: record.legal_type }" @option="selected('legal_type', $event)"></SearchBox>
|
||||
<label class="label"
|
||||
>{{ isIndividual ? "Giấy tờ tùy thân" : "Giấy tờ" }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: filteredLegalTypes,
|
||||
api: 'legaltype',
|
||||
field: isVietnamese ? 'name' : 'en',
|
||||
column: ['name', 'en'],
|
||||
first: true,
|
||||
optionid: record.legal_type,
|
||||
}"
|
||||
@option="selected('legal_type', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<input class="input" type="text" v-model="record.legal_code" />
|
||||
<p class="help is-danger" v-if="errors.legal_code">{{ errors.legal_code }}</p>
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("idnum")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="record.legal_code"
|
||||
/>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.legal_code"
|
||||
>
|
||||
{{ errors.legal_code }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label>
|
||||
<Datepicker v-bind="{ record: record, attr: 'issued_date', maxdate: new Date() }" @date="selected('issued_date', $event)"></Datepicker>
|
||||
<p class="help is-danger" v-if="errors.issued_date">{{ errors.issued_date }}</p>
|
||||
<label class="label"
|
||||
>{{ dataLang && findFieldName("issued_date")[lang] }}<b class="ml-1 has-text-danger">*</b></label
|
||||
>
|
||||
<Datepicker
|
||||
v-bind="{
|
||||
record: record,
|
||||
attr: 'issued_date',
|
||||
maxdate: new Date(),
|
||||
}"
|
||||
@date="selected('issued_date', $event)"
|
||||
></Datepicker>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.issued_date"
|
||||
>
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("issued_place")[lang] }}</label>
|
||||
<SearchBox
|
||||
v-bind="{ api: 'issuedplace', field: 'name', column: ['name'], first: true, position: 'is-bottom-right', optionid: record.issued_place,filter: {id__in: isIndividual ? [2, 3] : [4, 5] } }"
|
||||
@option="selected('issued_place', $event)"></SearchBox>
|
||||
v-bind="{
|
||||
api: 'issuedplace',
|
||||
field: 'name',
|
||||
column: ['name'],
|
||||
first: true,
|
||||
position: 'is-bottom-right',
|
||||
optionid: record.issued_place,
|
||||
filter: { id__in: isIndividual ? [2, 3] : [4, 5] },
|
||||
}"
|
||||
@option="selected('issued_place', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline" v-if="isIndividual">
|
||||
<div
|
||||
class="columns is-multiline"
|
||||
v-if="isIndividual"
|
||||
>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("gender")[lang] }}</label>
|
||||
<SearchBox v-bind="{ vdata: store.sex, api: 'sex', field: isVietnamese ? 'name' : 'en', column: ['name', 'en'], first: true, optionid: individualData.sex }" @option="selectedIndividual('sex', $event)"></SearchBox>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: store.sex,
|
||||
api: 'sex',
|
||||
field: isVietnamese ? 'name' : 'en',
|
||||
column: ['name', 'en'],
|
||||
first: true,
|
||||
optionid: individualData.sex,
|
||||
}"
|
||||
@option="selectedIndividual('sex', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("birth_date")[lang] }}</label>
|
||||
<Datepicker v-bind="{ record: individualData, attr: 'dob', maxdate: new Date() }" @date="selectedIndividual('dob', $event)"></Datepicker>
|
||||
<p class="help is-danger" v-if="errors.dob">{{ errors.dob }}</p>
|
||||
<Datepicker
|
||||
v-bind="{
|
||||
record: individualData,
|
||||
attr: 'dob',
|
||||
maxdate: new Date(),
|
||||
}"
|
||||
@date="selectedIndividual('dob', $event)"
|
||||
></Datepicker>
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.dob"
|
||||
>
|
||||
{{ errors.dob }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,19 +277,33 @@
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
|
||||
<textarea class="textarea" v-model="record.note" rows="2"></textarea>
|
||||
<textarea
|
||||
class="textarea"
|
||||
v-model="record.note"
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-4">
|
||||
<h4 class="title is-6 has-text-warning">{{ isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật' }}</h4>
|
||||
<h4 class="title is-6 has-text-warning">
|
||||
{{ isIndividual ? "Người liên quan" : "Người đại diện pháp luật" }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="columns is-multiline mb-0 is-2" v-for="(v, i) in localPeople" :key="i">
|
||||
<div
|
||||
class="columns is-multiline mb-0 is-2"
|
||||
v-for="(v, i) in localPeople"
|
||||
:key="i"
|
||||
>
|
||||
<div class="column">
|
||||
<label class="label" v-if="i === 0">{{ findFieldName("select")[lang] }}</label>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
<label
|
||||
class="label"
|
||||
v-if="i === 0"
|
||||
>{{ findFieldName("select")[lang] }}</label
|
||||
>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'people',
|
||||
field: 'label',
|
||||
column: ['code', 'fullname', 'phone'],
|
||||
@@ -172,34 +311,54 @@
|
||||
optionid: v.people,
|
||||
position: 'is-top-left',
|
||||
addon: peopleAddon,
|
||||
viewaddon: peopleviewAddon
|
||||
}"
|
||||
@option="selectPeople($event, v, i)"></SearchBox>
|
||||
viewaddon: peopleviewAddon,
|
||||
}"
|
||||
@option="selectPeople($event, v, i)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<label class="label" v-if="i === 0">{{ isIndividual ? 'Quan hệ' : 'Chức vụ' }}</label>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
<label
|
||||
class="label"
|
||||
v-if="i === 0"
|
||||
>{{ isIndividual ? "Quan hệ" : "Chức vụ" }}</label
|
||||
>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'relation',
|
||||
field: store.lang === 'en' ? 'en' : 'name',
|
||||
column: ['code', 'name', 'en'],
|
||||
first: true,
|
||||
optionid: v.relation,
|
||||
column: ['code', 'name', 'en'],
|
||||
first: true,
|
||||
optionid: v.relation,
|
||||
position: 'is-top-left',
|
||||
filter:{ id__in: isIndividual ? [1,2,3,4,5,6,7,8] : [9,10,11,12] }
|
||||
}"
|
||||
filter: {
|
||||
id__in: isIndividual ? [1, 2, 3, 4, 5, 6, 7, 8] : [9, 10, 11, 12],
|
||||
},
|
||||
}"
|
||||
@option="selectRelation($event, v, i)"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<label class="label" v-if="i === 0"> </label>
|
||||
<div class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small" style="height: 40px">
|
||||
<button class="button is-dark" @click="add()">
|
||||
<label
|
||||
class="label"
|
||||
v-if="i === 0"
|
||||
> </label
|
||||
>
|
||||
<div
|
||||
class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small"
|
||||
style="height: 40px"
|
||||
>
|
||||
<button
|
||||
class="button is-dark"
|
||||
@click="add()"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-dark" @click="remove(v, i)">
|
||||
<button
|
||||
class="button is-dark"
|
||||
@click="remove(v, i)"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||
</span>
|
||||
@@ -209,11 +368,25 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-5 buttons is-right">
|
||||
<button class="button" @click="emit('close')">{{ isVietnamese ? 'Hủy' : 'Cancel' }}</button>
|
||||
<button class="button is-primary" @click="update()">{{ isVietnamese ? 'Lưu lại' : 'Save' }}</button>
|
||||
<button
|
||||
class="button"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ isVietnamese ? "Hủy" : "Cancel" }}
|
||||
</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
@click="update()"
|
||||
>
|
||||
{{ isVietnamese ? "Lưu lại" : "Save" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -226,7 +399,7 @@ import InputEmail from "~/components/common/InputEmail";
|
||||
import SearchBox from "~/components/SearchBox";
|
||||
import Datepicker from "~/components/datepicker/Datepicker";
|
||||
import { useStore } from "~/stores/index";
|
||||
import { isEqual, pick } from 'es-toolkit';
|
||||
import { isEqual, pick } from "es-toolkit";
|
||||
|
||||
const emit = defineEmits(["close", "update", "modalevent"]);
|
||||
const { $getdata, $patchapi, $insertapi, $deleteapi, $empty, $errPhone, $resetNull, $snackbar, $copy } = useNuxtApp();
|
||||
@@ -260,21 +433,32 @@ const isOrganization = computed(() => selectedCustomerType.value === 2);
|
||||
|
||||
const filteredLegalTypes = computed(() => {
|
||||
if (!store.legaltype) return [];
|
||||
return isOrganization.value ? store.legaltype.filter(lt => lt.id === 4) : store.legaltype.filter(lt => lt.id !== 4);
|
||||
return isOrganization.value
|
||||
? store.legaltype.filter((lt) => lt.id === 4)
|
||||
: store.legaltype.filter((lt) => lt.id !== 4);
|
||||
});
|
||||
|
||||
const peopleAddon = {
|
||||
const peopleAddon = {
|
||||
component: "people/People",
|
||||
width: "65%",
|
||||
height: "500px",
|
||||
title: store.lang === "en" ? "Related person" : "Người liên quan"
|
||||
height: "500px",
|
||||
title: store.lang === "en" ? "Related person" : "Người liên quan",
|
||||
};
|
||||
const peopleviewAddon = { ...peopleAddon };
|
||||
|
||||
function selectCustomerType(type) {
|
||||
const typeNum = Number(type);
|
||||
selectedCustomerType.value = typeNum;
|
||||
record.value = { fullname: "", phone: "", email: "", country: 1, type: typeNum, legal_type: typeNum === 2 ? 4 : null, creator: store.login.id, updater: store.login.id };
|
||||
record.value = {
|
||||
fullname: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
country: 1,
|
||||
type: typeNum,
|
||||
legal_type: typeNum === 2 ? 4 : null,
|
||||
creator: store.login.id,
|
||||
updater: store.login.id,
|
||||
};
|
||||
if (typeNum === 1) {
|
||||
individualData.value = { dob: null, sex: 1 };
|
||||
organizationData.value = {};
|
||||
@@ -287,40 +471,61 @@ function selectCustomerType(type) {
|
||||
}
|
||||
|
||||
function findFieldName(code) {
|
||||
return dataLang.value.find(item => item.code === code) || { vi: code, en: code };
|
||||
return dataLang.value.find((item) => item.code === code) || { vi: code, en: code };
|
||||
}
|
||||
|
||||
function showCustomer() {
|
||||
showmodal.value = { component: "customer/CustomerView", width: "60%", height: "600px", title: "Khách hàng", vbind: { row: existedCustomer.value } };
|
||||
showmodal.value = {
|
||||
component: "customer/CustomerView",
|
||||
width: "60%",
|
||||
height: "600px",
|
||||
title: "Khách hàng",
|
||||
vbind: { row: existedCustomer.value },
|
||||
};
|
||||
}
|
||||
|
||||
const selected = (f, v) => { record.value[f] = (v && typeof v === 'object') ? v.id : v; if (errors.value[f]) delete errors.value[f]; };
|
||||
const selectedIndividual = (f, v) => { individualData.value[f] = (v && typeof v === 'object') ? v.id : v; };
|
||||
const selectedOrg = (f, v) => { organizationData.value[f] = (v && typeof v === 'object') ? v.id : v; };
|
||||
const selectPeople = (opt, _v, i) => {
|
||||
localPeople.value[i].people = opt.id;
|
||||
const selected = (f, v) => {
|
||||
record.value[f] = v && typeof v === "object" ? v.id : v;
|
||||
if (errors.value[f]) delete errors.value[f];
|
||||
};
|
||||
const selectedIndividual = (f, v) => {
|
||||
individualData.value[f] = v && typeof v === "object" ? v.id : v;
|
||||
};
|
||||
const selectedOrg = (f, v) => {
|
||||
organizationData.value[f] = v && typeof v === "object" ? v.id : v;
|
||||
};
|
||||
const selectPeople = (opt, _v, i) => {
|
||||
localPeople.value[i].people = opt.id;
|
||||
};
|
||||
const selectRelation = (opt, _v, i) => {
|
||||
localPeople.value[i].relation = opt ? opt.id : null;
|
||||
};
|
||||
const selectRelation = (opt, _v, i) => { localPeople.value[i].relation = opt ? opt.id : null; };
|
||||
const add = () => localPeople.value.push({});
|
||||
const remove = (_v, i) => {
|
||||
localPeople.value.splice(i, 1);
|
||||
if (localPeople.value.length === 0) localPeople.value = [{}];
|
||||
};
|
||||
|
||||
watch(people, (val) => {
|
||||
localPeople.value = val.map(cp => pick(cp, ['id', 'people', 'relation']));
|
||||
}, { deep: true })
|
||||
watch(
|
||||
people,
|
||||
(val) => {
|
||||
localPeople.value = val.map((cp) => pick(cp, ["id", "people", "relation"]));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
function checkError() {
|
||||
errors.value = {};
|
||||
if ($empty(record.value.fullname)) errors.value.fullname = isVietnamese.value ? "Họ tên không được bỏ trống" : "Full name is required";
|
||||
if ($empty(record.value.fullname))
|
||||
errors.value.fullname = isVietnamese.value ? "Họ tên không được bỏ trống" : "Full name is required";
|
||||
if ($empty(record.value.phone)) {
|
||||
errors.value.phone = isVietnamese.value ? "Số điện thoại không được bỏ trống" : "Phone is required";
|
||||
} else {
|
||||
const text = $errPhone(record.value.phone);
|
||||
if (text) errors.value.phone = text;
|
||||
}
|
||||
if ($empty(record.value.legal_code)) errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code is required";
|
||||
if ($empty(record.value.legal_code))
|
||||
errors.value.legal_code = isVietnamese.value ? "Mã số không được bỏ trống" : "Legal code is required";
|
||||
if ($empty(record.value.issued_date)) errors.value.issued_date = "Ngày cấp không được bỏ trống";
|
||||
|
||||
return Object.keys(errors.value).length > 0;
|
||||
@@ -349,7 +554,10 @@ async function update() {
|
||||
}
|
||||
if (record.value.legal_code) {
|
||||
const legalCheck = await $getdata("customer", { legal_code: record.value.legal_code }, undefined, true);
|
||||
if (legalCheck) { errors.value.legal_code = "Số CMND/CCCD đã tồn tại."; return; }
|
||||
if (legalCheck) {
|
||||
errors.value.legal_code = "Số CMND/CCCD đã tồn tại.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +566,9 @@ async function update() {
|
||||
customerData.updater = store.login.id;
|
||||
customerData.update_time = new Date();
|
||||
|
||||
let res = isNewCustomer.value ? await $insertapi("customer", customerData, undefined, false) : await $patchapi("customer", customerData, undefined, false);
|
||||
let res = isNewCustomer.value
|
||||
? await $insertapi("customer", customerData, undefined, false)
|
||||
: await $patchapi("customer", customerData, undefined, false);
|
||||
if (!res || res === "error") return;
|
||||
|
||||
const customerId = res.id;
|
||||
@@ -367,7 +577,8 @@ async function update() {
|
||||
if (isIndividual.value) {
|
||||
let indPayload = $resetNull({ ...individualData.value });
|
||||
indPayload.customer = customerId;
|
||||
if (individualData.value.id) await $patchapi("individual", { ...indPayload, id: individualData.value.id }, undefined, false);
|
||||
if (individualData.value.id)
|
||||
await $patchapi("individual", { ...indPayload, id: individualData.value.id }, undefined, false);
|
||||
else await $insertapi("individual", indPayload, undefined, false);
|
||||
} else if (isOrganization.value) {
|
||||
let orgPayload = $resetNull({ ...organizationData.value });
|
||||
@@ -393,22 +604,22 @@ async function update() {
|
||||
commonPayload = { organization: organizationId };
|
||||
}
|
||||
|
||||
const validLocalPeople = localPeople.value.filter(lp => lp.people && lp.relation).map(lp => toRaw(lp));
|
||||
const peopleKeys = people.value.map(p => pick(p, ['id', 'people', 'relation']));
|
||||
const validLocalPeople = localPeople.value.filter((lp) => lp.people && lp.relation).map((lp) => toRaw(lp));
|
||||
const peopleKeys = people.value.map((p) => pick(p, ["id", "people", "relation"]));
|
||||
|
||||
// 1. check existing ids, if people or relation changes -> patch
|
||||
const existingLocalPeople = validLocalPeople.filter(cp => Boolean(cp.id));
|
||||
existingLocalPeople.forEach(lp => {
|
||||
const match = peopleKeys.find(p => isEqual(p, lp));
|
||||
const payload = { ...lp, ...commonPayload }
|
||||
|
||||
const existingLocalPeople = validLocalPeople.filter((cp) => Boolean(cp.id));
|
||||
existingLocalPeople.forEach((lp) => {
|
||||
const match = peopleKeys.find((p) => isEqual(p, lp));
|
||||
const payload = { ...lp, ...commonPayload };
|
||||
|
||||
if (!match) {
|
||||
$patchapi(apiName, payload);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 2. if localPeople has and people doesn't -> insert
|
||||
validLocalPeople.forEach(lp => {
|
||||
validLocalPeople.forEach((lp) => {
|
||||
if (!lp.id) {
|
||||
const payload = { ...lp, ...commonPayload };
|
||||
$insertapi(apiName, payload);
|
||||
@@ -417,8 +628,8 @@ async function update() {
|
||||
|
||||
// 3. if people has and localPeople doesn't -> delete
|
||||
if (peopleKeys.length !== 0 && validLocalPeople.length !== 0) {
|
||||
peopleKeys.forEach(cp => {
|
||||
const match = validLocalPeople.find(lp => cp.id === lp.id);
|
||||
peopleKeys.forEach((cp) => {
|
||||
const match = validLocalPeople.find((lp) => cp.id === lp.id);
|
||||
if (!match) {
|
||||
$deleteapi(apiName, cp.id);
|
||||
}
|
||||
@@ -427,17 +638,24 @@ async function update() {
|
||||
|
||||
// Ảnh
|
||||
if (record.value.image && record.value.image.length > 0) {
|
||||
await $insertapi("customerfile", record.value.image.map(v => ({ ref: customerId, file: v })), undefined, false);
|
||||
await $insertapi(
|
||||
"customerfile",
|
||||
record.value.image.map((v) => ({ ref: customerId, file: v })),
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
const completeData = await $getdata("customer", { id: customerId }, undefined, true);
|
||||
|
||||
|
||||
$snackbar(`Khách hàng đã được ${isNewCustomer.value ? "khởi tạo" : "cập nhật"} thành công`, "Thành công");
|
||||
|
||||
emit("modalevent", { name: "dataevent", data: completeData });
|
||||
emit("update", completeData);
|
||||
setTimeout(() => emit("close"), 100);
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function initData() {
|
||||
@@ -465,7 +683,12 @@ async function initData() {
|
||||
const copyData = $copy(props.application);
|
||||
const type = props.customerType || copyData.type || 1;
|
||||
selectCustomerType(type);
|
||||
record.value = { ...record.value, ...copyData, id: undefined, code: undefined };
|
||||
record.value = {
|
||||
...record.value,
|
||||
...copyData,
|
||||
id: undefined,
|
||||
code: undefined,
|
||||
};
|
||||
individualData.value = { ...individualData.value, ...copyData };
|
||||
} else if (props.customerType) {
|
||||
selectCustomerType(props.customerType);
|
||||
@@ -473,4 +696,4 @@ async function initData() {
|
||||
}
|
||||
|
||||
onMounted(() => initData());
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<div class="">
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'building.svg', type: 'black', size: 40 }"
|
||||
></SvgIcon>
|
||||
<SvgIcon v-bind="{ name: 'building.svg', type: 'black', size: 40 }"></SvgIcon>
|
||||
</div>
|
||||
<div class="title is-6 mb-0">
|
||||
{{ isVietnamese ? "Doanh nghiệp" : "Company" }}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div :id="docid" v-if="record">
|
||||
<div
|
||||
:id="docid"
|
||||
v-if="record"
|
||||
>
|
||||
<div>
|
||||
<Caption v-bind="{ title: this.data && findFieldName('info')[this.lang] }"></Caption>
|
||||
<div class="columns is-multiline mx-0">
|
||||
@@ -7,7 +10,11 @@
|
||||
<div class="field">
|
||||
<label class="label">Mã khách hàng</label>
|
||||
<div class="control">
|
||||
<span class="hyperlink" @click="$copyToClipboard(record.code)">{{ record.code }}</span>
|
||||
<span
|
||||
class="hyperlink"
|
||||
@click="$copyToClipboard(record.code)"
|
||||
>{{ record.code }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,14 +30,21 @@
|
||||
<div class="field">
|
||||
<label class="label">Điện thoại</label>
|
||||
<div class="control">
|
||||
<span class="hyperlink" @click="openPhone()">{{ record.phone }}</span>
|
||||
<span
|
||||
class="hyperlink"
|
||||
@click="openPhone()"
|
||||
>{{ record.phone }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Email</label>
|
||||
<div class="control" style="word-break: break-all">
|
||||
<div
|
||||
class="control"
|
||||
style="word-break: break-all"
|
||||
>
|
||||
{{ record.email || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +117,11 @@
|
||||
<div class="field">
|
||||
<label class="label">Người tạo</label>
|
||||
<div class="control">
|
||||
<span class="hyperlink" @click="openUser(record.creator)">{{ record.creator__fullname || "/" }}</span>
|
||||
<span
|
||||
class="hyperlink"
|
||||
@click="openUser(record.creator)"
|
||||
>{{ record.creator__fullname || "/" }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,7 +140,11 @@
|
||||
<div class="field">
|
||||
<label class="label">Người cập nhật</label>
|
||||
<div class="control">
|
||||
<span class="hyperlink" @click="openUser(record.updater)">{{ record.updater__fullname || "/" }}</span>
|
||||
<span
|
||||
class="hyperlink"
|
||||
@click="openUser(record.updater)"
|
||||
>{{ record.updater__fullname || "/" }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +153,7 @@
|
||||
<label class="label">Thời gian cập nhật</label>
|
||||
<div class="control">
|
||||
<span>
|
||||
{{ record.update_time ? $dayjs(record.update_time).format("DD/MM/YYYY HH:mm") : '/' }}
|
||||
{{ record.update_time ? $dayjs(record.update_time).format("DD/MM/YYYY HH:mm") : "/" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,58 +165,137 @@
|
||||
<ImageGallery v-bind="{ row: record, api: 'customerfile', hideopt: true }"></ImageGallery>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Caption v-bind="{ title: this.isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật' }"></Caption>
|
||||
<Caption
|
||||
v-bind="{
|
||||
title: this.isIndividual ? 'Người liên quan' : 'Người đại diện pháp luật',
|
||||
}"
|
||||
></Caption>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
<div
|
||||
v-if="this.relatedPeople && this.relatedPeople.length > 0"
|
||||
v-for="relatedPerson in this.relatedPeople"
|
||||
class="columns is-0 mb-2"
|
||||
>
|
||||
<span class="column is-2">{{ relatedPerson.people__code }}</span>
|
||||
<span class="column is-4 ">
|
||||
<span class="has-text-primary hyperlink"
|
||||
<span class="column is-4">
|
||||
<span
|
||||
class="has-text-primary hyperlink"
|
||||
@click="openRelatedPerson(relatedPerson.people)"
|
||||
>{{ relatedPerson.people__fullname }}</span>
|
||||
>{{ relatedPerson.people__fullname }}</span
|
||||
>
|
||||
<span> ({{ relatedPerson.relation__name }})</span>
|
||||
</span>
|
||||
<span class="column is-4">{{ relatedPerson.people__phone }}</span>
|
||||
</div>
|
||||
<div v-else class="has-text-grey">
|
||||
Chưa có {{ this.isIndividual ? 'người liên quan' : 'người đại diện pháp luật' }}
|
||||
<div
|
||||
v-else
|
||||
class="has-text-grey"
|
||||
>
|
||||
Chưa có
|
||||
{{ this.isIndividual ? "người liên quan" : "người đại diện pháp luật" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="record.count_product >0" class="mt-3">
|
||||
<Caption class="mb-2" v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"></Caption>
|
||||
<DataView v-bind="{
|
||||
setting: 'customer-all-transaction',
|
||||
pagename: this.$id(),
|
||||
api: 'customer',
|
||||
params: {
|
||||
filter: { id: this.row.customer || this.row.id },
|
||||
/* copied from 02-connection.js */
|
||||
values:
|
||||
'id,update_time,creator,creator__fullname,country,country__name,country__en,issued_date,issued_place,issued_place__name,code,email,fullname,legal_code,phone,legal_type,legal_type__name,address,contact_address,note,type,type__name,updater,updater__fullname,create_time,update_time',
|
||||
distinct_values: {
|
||||
label: { type: 'Concat', field: ['code', 'fullname', 'phone', 'legal_code'] },
|
||||
order: { type: 'RowNumber' },
|
||||
image_count: { type: 'Count', field: 'id', subquery: { model: 'Customer_File', column: 'ref' } },
|
||||
count_note: { type: 'Count', field: 'id', subquery: { model: 'Customer_Note', column: 'ref' } },
|
||||
count_product: { type: 'Count', field: 'id', subquery: { model: 'Product_Booked', column: 'transaction__customer' } },
|
||||
sum_product: { type: 'Sum', field: 'transaction__sale_price', subquery: { model: 'Product_Booked', column: 'transaction__customer' } },
|
||||
sum_receiver: { type: 'Sum', field: 'transaction__amount_received', subquery: { model: 'Product_Booked', column: 'transaction__customer' } },
|
||||
sum_remain: { type: 'Sum', field: 'transaction__amount_remain', subquery: { model: 'Product_Booked', column: 'transaction__customer' } }
|
||||
<div
|
||||
v-if="record.count_product > 0"
|
||||
class="mt-3"
|
||||
>
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: this.data && findFieldName('transaction')[this.lang] }"
|
||||
></Caption>
|
||||
<DataView
|
||||
v-bind="{
|
||||
setting: 'customer-all-transaction',
|
||||
pagename: this.$id(),
|
||||
api: 'customer',
|
||||
params: {
|
||||
filter: { id: this.row.customer || this.row.id },
|
||||
/* copied from 02-connection.js */
|
||||
values:
|
||||
'id,update_time,creator,creator__fullname,country,country__name,country__en,issued_date,issued_place,issued_place__name,code,email,fullname,legal_code,phone,legal_type,legal_type__name,address,contact_address,note,type,type__name,updater,updater__fullname,create_time,update_time',
|
||||
distinct_values: {
|
||||
label: {
|
||||
type: 'Concat',
|
||||
field: ['code', 'fullname', 'phone', 'legal_code'],
|
||||
},
|
||||
order: { type: 'RowNumber' },
|
||||
image_count: {
|
||||
type: 'Count',
|
||||
field: 'id',
|
||||
subquery: { model: 'Customer_File', column: 'ref' },
|
||||
},
|
||||
count_note: {
|
||||
type: 'Count',
|
||||
field: 'id',
|
||||
subquery: { model: 'Customer_Note', column: 'ref' },
|
||||
},
|
||||
count_product: {
|
||||
type: 'Count',
|
||||
field: 'id',
|
||||
subquery: {
|
||||
model: 'Product_Booked',
|
||||
column: 'transaction__customer',
|
||||
},
|
||||
},
|
||||
sum_product: {
|
||||
type: 'Sum',
|
||||
field: 'transaction__sale_price',
|
||||
subquery: {
|
||||
model: 'Product_Booked',
|
||||
column: 'transaction__customer',
|
||||
},
|
||||
},
|
||||
sum_receiver: {
|
||||
type: 'Sum',
|
||||
field: 'transaction__amount_received',
|
||||
subquery: {
|
||||
model: 'Product_Booked',
|
||||
column: 'transaction__customer',
|
||||
},
|
||||
},
|
||||
sum_remain: {
|
||||
type: 'Sum',
|
||||
field: 'transaction__amount_remain',
|
||||
subquery: {
|
||||
model: 'Product_Booked',
|
||||
column: 'transaction__customer',
|
||||
},
|
||||
},
|
||||
},
|
||||
summary: 'annotate',
|
||||
},
|
||||
summary: 'annotate',
|
||||
},
|
||||
}" />
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 border-bottom" id="ignore"></div>
|
||||
<div class="buttons mt-2 is-flex is-gap-1" id="ignore">
|
||||
<button v-if="$getEditRights('edit', { code: 'customer', category: 'topmenu' })" class="button is-primary" @click="edit()">Chỉnh sửa</button>
|
||||
<button class="button is-light" @click="$exportpdf(docid, record.code)">In thông tin</button>
|
||||
<div
|
||||
class="mt-4 border-bottom"
|
||||
id="ignore"
|
||||
></div>
|
||||
<div
|
||||
class="buttons mt-2 is-flex is-gap-1"
|
||||
id="ignore"
|
||||
>
|
||||
<button
|
||||
v-if="$getEditRights('edit', { code: 'customer', category: 'topmenu' })"
|
||||
class="button is-primary"
|
||||
@click="edit()"
|
||||
>
|
||||
Chỉnh sửa
|
||||
</button>
|
||||
<button
|
||||
class="button is-light"
|
||||
@click="$exportpdf(docid, record.code)"
|
||||
>
|
||||
In thông tin
|
||||
</button>
|
||||
</div>
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" @dataevent="changeInfo" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
@dataevent="changeInfo"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -225,15 +326,19 @@ export default {
|
||||
},
|
||||
isIndividual() {
|
||||
return this.record.type === 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
this.record = await this.$getdata("customer", { id: this.row.customer || this.row.id }, undefined, true);
|
||||
if (this.isIndividual) {
|
||||
this.relatedPeople = await this.$getdata("customerpeople", { customer: this.row.customer || this.row.id });
|
||||
this.relatedPeople = await this.$getdata("customerpeople", {
|
||||
customer: this.row.customer || this.row.id,
|
||||
});
|
||||
} else {
|
||||
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true);
|
||||
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id });
|
||||
const org = await this.$getdata("organization", { customer: this.row.customer || this.row.id }, undefined, true);
|
||||
this.relatedPeople = await this.$getdata("legalrep", {
|
||||
organization: org.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -270,10 +375,19 @@ export default {
|
||||
this.record = this.$copy(v);
|
||||
// refetch relatedPeople
|
||||
if (this.isIndividual) {
|
||||
this.relatedPeople = await this.$getdata("customerpeople", { customer: this.row.customer || this.row.id });
|
||||
this.relatedPeople = await this.$getdata("customerpeople", {
|
||||
customer: this.row.customer || this.row.id,
|
||||
});
|
||||
} else {
|
||||
const org = await this.$getdata('organization', { customer: this.row.customer || this.row.id }, undefined, true);
|
||||
this.relatedPeople = await this.$getdata("legalrep", { organization: org.id });
|
||||
const org = await this.$getdata(
|
||||
"organization",
|
||||
{ customer: this.row.customer || this.row.id },
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
this.relatedPeople = await this.$getdata("legalrep", {
|
||||
organization: org.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
openUser(userId) {
|
||||
@@ -287,15 +401,15 @@ export default {
|
||||
};
|
||||
},
|
||||
async openRelatedPerson(peopleId) {
|
||||
const peopleRow = await this.$getdata('people', { id: peopleId }, undefined, true);
|
||||
const peopleRow = await this.$getdata("people", { id: peopleId }, undefined, true);
|
||||
this.showmodal = {
|
||||
component: "people/PeopleView",
|
||||
vbind: { row: peopleRow },
|
||||
title: 'Người liên quan',
|
||||
width: '65%',
|
||||
height: '400px',
|
||||
}
|
||||
}
|
||||
title: "Người liên quan",
|
||||
width: "65%",
|
||||
height: "400px",
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
class="fs-13 font-semibold mx-0 px-0 is-flex is-justify-content-center is-align-items-center is-flex-shrink-0 rounded-full size-10"
|
||||
<template>
|
||||
<div
|
||||
class="fs-13 font-semibold mx-0 px-0 is-flex is-justify-content-center is-align-items-center is-flex-shrink-0 rounded-full size-10"
|
||||
style="border: 1px solid var(--bulma-grey-80)"
|
||||
:style="image && 'border: none'">
|
||||
:style="image && 'border: none'"
|
||||
>
|
||||
<div>
|
||||
<span>{{text}}</span>
|
||||
<span>{{ text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['text', 'image', 'type', 'size']
|
||||
}
|
||||
export default {
|
||||
props: ["text", "image", "type", "size"],
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.cbox {
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
<script setup>
|
||||
import DashboardHighlightCard from '@/components/dashboard/DashboardHighlightCard.vue';
|
||||
import Delivery from '@/components/dashboard/Delivery.vue';
|
||||
import OrderStatus from '@/components/dashboard/OrderStatus.vue';
|
||||
import RevenueChart from '@/components/dashboard/RevenueChart.vue';
|
||||
import TopCustomers from '@/components/dashboard/TopCustomers.vue';
|
||||
import TopProducts from '@/components/dashboard/TopProducts.vue';
|
||||
import Warnings from '@/components/dashboard/Warnings.vue';
|
||||
import DashboardHighlightCard from "@/components/dashboard/DashboardHighlightCard.vue";
|
||||
import Delivery from "@/components/dashboard/Delivery.vue";
|
||||
import OrderStatus from "@/components/dashboard/OrderStatus.vue";
|
||||
import RevenueChart from "@/components/dashboard/RevenueChart.vue";
|
||||
import TopCustomers from "@/components/dashboard/TopCustomers.vue";
|
||||
import TopProducts from "@/components/dashboard/TopProducts.vue";
|
||||
import Warnings from "@/components/dashboard/Warnings.vue";
|
||||
|
||||
const highlights = [
|
||||
{
|
||||
name: 'Doanh thu hôm nay',
|
||||
value: '72.5M',
|
||||
color: 'blue',
|
||||
icon: 'material-symbols:attach-money-rounded',
|
||||
subheader: {
|
||||
value: '+12.5%',
|
||||
fluctuation: 'up',
|
||||
}
|
||||
name: "Doanh thu hôm nay",
|
||||
value: "72.5M",
|
||||
color: "blue",
|
||||
icon: "material-symbols:attach-money-rounded",
|
||||
subheader: {
|
||||
value: "+12.5%",
|
||||
fluctuation: "up",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Số đơn hàng',
|
||||
value: '73',
|
||||
color: 'purple',
|
||||
icon: 'material-symbols:shopping-cart-outline-rounded',
|
||||
subheader: {
|
||||
value: '+8 đơn',
|
||||
fluctuation: 'up',
|
||||
}
|
||||
name: "Số đơn hàng",
|
||||
value: "73",
|
||||
color: "purple",
|
||||
icon: "material-symbols:shopping-cart-outline-rounded",
|
||||
subheader: {
|
||||
value: "+8 đơn",
|
||||
fluctuation: "up",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Đơn đang giao',
|
||||
value: '8',
|
||||
color: 'orange',
|
||||
icon: 'material-symbols:delivery-truck-speed-outline-rounded',
|
||||
name: "Đơn đang giao",
|
||||
value: "8",
|
||||
color: "orange",
|
||||
icon: "material-symbols:delivery-truck-speed-outline-rounded",
|
||||
},
|
||||
{
|
||||
name: 'Đơn hoàn thành',
|
||||
value: '38',
|
||||
color: 'green',
|
||||
icon: 'material-symbols:check-circle-outline-rounded',
|
||||
name: "Đơn hoàn thành",
|
||||
value: "38",
|
||||
color: "green",
|
||||
icon: "material-symbols:check-circle-outline-rounded",
|
||||
},
|
||||
{
|
||||
name: 'Công nợ phải thu',
|
||||
value: '245M',
|
||||
color: 'red',
|
||||
icon: 'material-symbols:universal-currency-alt-outline-rounded',
|
||||
subheader: {
|
||||
value: '+15M',
|
||||
fluctuation: 'down',
|
||||
}
|
||||
name: "Công nợ phải thu",
|
||||
value: "245M",
|
||||
color: "red",
|
||||
icon: "material-symbols:universal-currency-alt-outline-rounded",
|
||||
subheader: {
|
||||
value: "+15M",
|
||||
fluctuation: "down",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Tồn kho',
|
||||
value: '1,250',
|
||||
color: 'cyan',
|
||||
icon: 'material-symbols:box-outline-rounded',
|
||||
subheader: {
|
||||
value: '-45 SP',
|
||||
fluctuation: 'down',
|
||||
}
|
||||
name: "Tồn kho",
|
||||
value: "1,250",
|
||||
color: "cyan",
|
||||
icon: "material-symbols:box-outline-rounded",
|
||||
subheader: {
|
||||
value: "-45 SP",
|
||||
fluctuation: "down",
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="fixed-grid has-2-cols has-3-cols-tablet has-6-cols-desktop">
|
||||
<div class="grid">
|
||||
<DashboardHighlightCard
|
||||
<DashboardHighlightCard
|
||||
v-for="highlight in highlights"
|
||||
:key="highlight.name"
|
||||
v-bind="highlight"
|
||||
@@ -88,4 +88,4 @@ const highlights = [
|
||||
<Delivery />
|
||||
<Warnings />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -4,8 +4,8 @@ const props = defineProps({
|
||||
value: String,
|
||||
color: String,
|
||||
icon: String,
|
||||
subheader: Object
|
||||
})
|
||||
subheader: Object,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="cell">
|
||||
@@ -14,14 +14,19 @@ const props = defineProps({
|
||||
<div>
|
||||
<p class="fs-14 has-text-grey mb-1">{{ name }}</p>
|
||||
<p class="fsb-26 mb-1 has-text-black">{{ value }}</p>
|
||||
<div v-if="subheader"
|
||||
<div
|
||||
v-if="subheader"
|
||||
:class="[
|
||||
'is-flex is-gap-0.5 is-align-items-center',
|
||||
subheader.fluctuation === 'up' ? 'has-text-green-40' : 'has-text-red-40'
|
||||
subheader.fluctuation === 'up' ? 'has-text-green-40' : 'has-text-red-40',
|
||||
]"
|
||||
>
|
||||
<Icon
|
||||
:name="subheader.fluctuation === 'up' ? 'material-symbols:arrow-upward-rounded' :'material-symbols:arrow-downward-rounded'"
|
||||
:name="
|
||||
subheader.fluctuation === 'up'
|
||||
? 'material-symbols:arrow-upward-rounded'
|
||||
: 'material-symbols:arrow-downward-rounded'
|
||||
"
|
||||
color="inherit"
|
||||
/>
|
||||
<p class="fs-14">{{ subheader.value }}</p>
|
||||
@@ -30,12 +35,15 @@ const props = defineProps({
|
||||
<div
|
||||
:class="[
|
||||
'rounded-lg size-12 is-flex-shrink-0 is-flex is-justify-content-center is-align-items-center',
|
||||
`has-background-${color}-soft has-text-${color}-40`
|
||||
`has-background-${color}-soft has-text-${color}-40`,
|
||||
]"
|
||||
">
|
||||
<Icon :name="icon" :size="28" />
|
||||
>
|
||||
<Icon
|
||||
:name="icon"
|
||||
:size="28"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<script setup>
|
||||
import DeliveryInteractive from '@/components/dashboard/DeliveryInteractive.vue';
|
||||
import Driver from '@/components/dashboard/Driver.vue';
|
||||
import DeliveryInteractive from "@/components/dashboard/DeliveryInteractive.vue";
|
||||
import Driver from "@/components/dashboard/Driver.vue";
|
||||
|
||||
const drivers = [
|
||||
{
|
||||
name: 'Nguyễn Văn A',
|
||||
status: 'Đang giao',
|
||||
name: "Nguyễn Văn A",
|
||||
status: "Đang giao",
|
||||
deliveries: 3,
|
||||
deliveries_completed: 2,
|
||||
},
|
||||
{
|
||||
name: 'Trần Văn B',
|
||||
status: 'Đang giao',
|
||||
name: "Trần Văn B",
|
||||
status: "Đang giao",
|
||||
deliveries: 2,
|
||||
deliveries_completed: 1,
|
||||
},
|
||||
{
|
||||
name: 'Lê Thị C',
|
||||
status: 'Hoàn thành',
|
||||
name: "Lê Thị C",
|
||||
status: "Hoàn thành",
|
||||
deliveries: 4,
|
||||
deliveries_completed: 4,
|
||||
},
|
||||
{
|
||||
name: 'Phạm Văn D',
|
||||
status: 'Đang giao',
|
||||
name: "Phạm Văn D",
|
||||
status: "Đang giao",
|
||||
deliveries: 1,
|
||||
deliveries_completed: 0,
|
||||
},
|
||||
]
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
@@ -51,4 +51,4 @@ const drivers = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,47 @@
|
||||
class="relative w-full has-background-blue-95 rounded-lg is-clipped"
|
||||
style="height: 360px"
|
||||
>
|
||||
<div class="absolute inset-0 w-full h-full opacity-20"><div class="absolute w-full border-t border-gray-300" style="top: 20%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 40%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 60%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 80%;"></div><div class="absolute w-full border-t border-gray-300" style="top: 100%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 20%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 40%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 60%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 80%;"></div><div class="absolute h-full border-l border-gray-300" style="left: 100%;"></div>
|
||||
<div class="absolute inset-0 w-full h-full opacity-20">
|
||||
<div
|
||||
class="absolute w-full border-t border-gray-300"
|
||||
style="top: 20%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute w-full border-t border-gray-300"
|
||||
style="top: 40%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute w-full border-t border-gray-300"
|
||||
style="top: 60%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute w-full border-t border-gray-300"
|
||||
style="top: 80%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute w-full border-t border-gray-300"
|
||||
style="top: 100%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute h-full border-l border-gray-300"
|
||||
style="left: 20%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute h-full border-l border-gray-300"
|
||||
style="left: 40%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute h-full border-l border-gray-300"
|
||||
style="left: 60%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute h-full border-l border-gray-300"
|
||||
style="left: 80%"
|
||||
></div>
|
||||
<div
|
||||
class="absolute h-full border-l border-gray-300"
|
||||
style="left: 100%"
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
@@ -25,7 +65,11 @@
|
||||
<path
|
||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||
></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="10"
|
||||
r="3"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
@@ -47,7 +91,11 @@
|
||||
<path
|
||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||
></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="10"
|
||||
r="3"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
@@ -69,7 +117,11 @@
|
||||
<path
|
||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||
></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="10"
|
||||
r="3"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
@@ -91,7 +143,11 @@
|
||||
<path
|
||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||
></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="10"
|
||||
r="3"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
@@ -113,7 +169,11 @@
|
||||
<path
|
||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||
></path>
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="10"
|
||||
r="3"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
@@ -139,9 +199,7 @@
|
||||
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
|
||||
></div>
|
||||
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -167,9 +225,7 @@
|
||||
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
|
||||
></div>
|
||||
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -220,9 +276,7 @@
|
||||
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"
|
||||
></div>
|
||||
<div class="absolute -inset-1 rounded-full has-background-blue-60 animate-ping opacity-40"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -244,7 +298,12 @@
|
||||
<path
|
||||
d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"
|
||||
></path>
|
||||
<circle cx="12" cy="10" r="3"></circle></svg>Hà Nội
|
||||
<circle
|
||||
cx="12"
|
||||
cy="10"
|
||||
r="3"
|
||||
></circle></svg
|
||||
>Hà Nội
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,7 +373,7 @@
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.animate-ping {
|
||||
animation: ping 1s cubic-bezier(0,0,0.2,1) infinite;
|
||||
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
@keyframes ping {
|
||||
0% {
|
||||
@@ -339,67 +398,67 @@
|
||||
|
||||
@keyframes move-random-1 {
|
||||
0% {
|
||||
transform: translateX(0) translateY(0)
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
33% {
|
||||
transform: translateX(30px) translateY(24px)
|
||||
transform: translateX(30px) translateY(24px);
|
||||
}
|
||||
|
||||
66% {
|
||||
transform: translateX(60px) translateY(12px)
|
||||
transform: translateX(60px) translateY(12px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0) translateY(0)
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes move-random-2 {
|
||||
0% {
|
||||
transform: translateX(0) translateY(0)
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
23% {
|
||||
transform: translateX(-20px) translateY(-36px)
|
||||
transform: translateX(-20px) translateY(-36px);
|
||||
}
|
||||
|
||||
46% {
|
||||
transform: translateX(0) translateY(22px)
|
||||
transform: translateX(0) translateY(22px);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(30px) translateY(-12px)
|
||||
transform: translateX(30px) translateY(-12px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0) translateY(0)
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes move-random-3 {
|
||||
0% {
|
||||
transform: translateX(0) translateY(0)
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: translateX(-30px) translateY(-15px)
|
||||
transform: translateX(-30px) translateY(-15px);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translateX(-10px) translateY(26px)
|
||||
transform: translateX(-10px) translateY(26px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(48px) translateY(-28px)
|
||||
transform: translateX(48px) translateY(-28px);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translateX(17px) translateY(10px)
|
||||
transform: translateX(17px) translateY(10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0) translateY(0)
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
}
|
||||
.border-2 {
|
||||
@@ -442,13 +501,20 @@
|
||||
opacity: 40%;
|
||||
}
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
--tw-shadow:
|
||||
0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow:
|
||||
var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
|
||||
var(--tw-shadow);
|
||||
}
|
||||
.backdrop-blur {
|
||||
--tw-backdrop-blur: blur(8px);
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,)
|
||||
var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,)
|
||||
var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,)
|
||||
var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,)
|
||||
var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
@@ -474,4 +540,4 @@
|
||||
.border-gray-300 {
|
||||
border-color: var(--bulma-grey-85);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup>
|
||||
import AvatarBox from '@/components/dashboard/AvatarBox.vue';
|
||||
import AvatarBox from "@/components/dashboard/AvatarBox.vue";
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
status: String,
|
||||
deliveries: Number,
|
||||
deliveries_completed: Number,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
<div
|
||||
class="is-flex is-gap-2 fs-14 p-3 rounded-lg"
|
||||
:style="{
|
||||
border: '1px solid var(--bulma-grey-80)',
|
||||
@@ -22,14 +22,13 @@ const props = defineProps({
|
||||
<span :class="['tag', status === 'Đang giao' ? 'is-warning' : 'is-success']">{{ status }}</span>
|
||||
</div>
|
||||
<p class="fs-13 has-text-grey">Đơn: {{ deliveries_completed }}/{{ deliveries }}</p>
|
||||
<progress
|
||||
<progress
|
||||
v-if="deliveries !== deliveries_completed"
|
||||
class="progress is-small is-primary mt-2"
|
||||
style="--bulma-size-small: 0.4rem;"
|
||||
style="--bulma-size-small: 0.4rem"
|
||||
:value="deliveries_completed"
|
||||
:max="deliveries"
|
||||
>
|
||||
</progress>
|
||||
></progress>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<script setup>
|
||||
import OrderStatusCard from '@/components/dashboard/OrderStatusCard.vue';
|
||||
import OrderStatusCard from "@/components/dashboard/OrderStatusCard.vue";
|
||||
|
||||
const statuses = [
|
||||
{
|
||||
id: 1,
|
||||
code: 'pending',
|
||||
name: 'Chờ xử lý',
|
||||
code: "pending",
|
||||
name: "Chờ xử lý",
|
||||
value: 12,
|
||||
color: 'orange',
|
||||
icon: 'material-symbols:clock-loader-40'
|
||||
color: "orange",
|
||||
icon: "material-symbols:clock-loader-40",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: 'delivering',
|
||||
name: 'Đang giao',
|
||||
code: "delivering",
|
||||
name: "Đang giao",
|
||||
value: 8,
|
||||
color: 'blue',
|
||||
icon: 'material-symbols:delivery-truck-speed-outline-rounded'
|
||||
color: "blue",
|
||||
icon: "material-symbols:delivery-truck-speed-outline-rounded",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
code: 'delivered',
|
||||
name: 'Đã giao',
|
||||
code: "delivered",
|
||||
name: "Đã giao",
|
||||
value: 15,
|
||||
color: 'purple',
|
||||
icon: 'material-symbols:bucket-check-outline-rounded'
|
||||
color: "purple",
|
||||
icon: "material-symbols:bucket-check-outline-rounded",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
code: 'completed',
|
||||
name: 'Hoàn thành',
|
||||
code: "completed",
|
||||
name: "Hoàn thành",
|
||||
value: 38,
|
||||
color: 'green',
|
||||
icon: 'material-symbols:check-circle-outline-rounded'
|
||||
color: "green",
|
||||
icon: "material-symbols:check-circle-outline-rounded",
|
||||
},
|
||||
]
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-full">
|
||||
@@ -51,4 +51,4 @@ const statuses = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -6,18 +6,16 @@ const props = defineProps({
|
||||
value: Number,
|
||||
icon: String,
|
||||
color: String,
|
||||
})
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="cell">
|
||||
<div class="card" :style="{ border: `1px solid var(--bulma-${color}-70)` }">
|
||||
<div
|
||||
class="card"
|
||||
:style="{ border: `1px solid var(--bulma-${color}-70)` }"
|
||||
>
|
||||
<div class="card-content is-flex is-flex-direction-column is-align-items-center is-gap-1">
|
||||
<div
|
||||
:class="[
|
||||
'p-3 is-flex rounded-full',
|
||||
`has-background-${color}-90`,
|
||||
`has-text-${color}-40`,
|
||||
]" >
|
||||
<div :class="['p-3 is-flex rounded-full', `has-background-${color}-90`, `has-text-${color}-40`]">
|
||||
<Icon
|
||||
:name="icon"
|
||||
:size="24"
|
||||
@@ -30,4 +28,4 @@ const props = defineProps({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const { $shortenCurrency } = useNuxtApp();
|
||||
const revenueChartOptions = {
|
||||
chart: {
|
||||
type: 'spline'
|
||||
type: "spline",
|
||||
},
|
||||
credits: {
|
||||
enabled: false,
|
||||
@@ -11,38 +11,38 @@ const revenueChartOptions = {
|
||||
text: null,
|
||||
},
|
||||
xAxis: {
|
||||
categories: [
|
||||
'10/4', '11/4', '12/4', '13/4', '14/4', '15/4', '16/4', '17/4', '18/4'
|
||||
],
|
||||
categories: ["10/4", "11/4", "12/4", "13/4", "14/4", "15/4", "16/4", "17/4", "18/4"],
|
||||
accessibility: {
|
||||
description: 'Dates'
|
||||
}
|
||||
description: "Dates",
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
text: 'Doanh thu'
|
||||
text: "Doanh thu",
|
||||
},
|
||||
labels: {
|
||||
format: '{value}'
|
||||
}
|
||||
format: "{value}",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
crosshairs: true,
|
||||
shared: true,
|
||||
valueSuffix: ' VNĐ',
|
||||
valueSuffix: " VNĐ",
|
||||
},
|
||||
plotOptions: {
|
||||
spline: {
|
||||
marker: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
name: 'Doanh thu',
|
||||
data: [45000000, 52000000, 48000000, 51000000, 58000000, 61000000, 67500000, 72000000, 69000000],
|
||||
showInLegend: false,
|
||||
}]
|
||||
series: [
|
||||
{
|
||||
name: "Doanh thu",
|
||||
data: [45000000, 52000000, 48000000, 51000000, 58000000, 61000000, 67500000, 72000000, 69000000],
|
||||
showInLegend: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
@@ -58,4 +58,4 @@ const revenueChartOptions = {
|
||||
<highcharts :options="revenueChartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import AvatarBox from '@/components/dashboard/AvatarBox.vue';
|
||||
import AvatarBox from "@/components/dashboard/AvatarBox.vue";
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
order_count: Number,
|
||||
paid: Number,
|
||||
})
|
||||
});
|
||||
|
||||
const { $shortenCurrency } = useNuxtApp();
|
||||
</script>
|
||||
@@ -20,4 +20,4 @@ const { $shortenCurrency } = useNuxtApp();
|
||||
</div>
|
||||
<p class="font-semibold">{{ $shortenCurrency(paid) }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
<script setup>
|
||||
import TopCustomer from '@/components/dashboard/TopCustomer.vue';
|
||||
|
||||
import TopCustomer from "@/components/dashboard/TopCustomer.vue";
|
||||
|
||||
const customers = [
|
||||
{
|
||||
name: 'Công ty TNHH ABC',
|
||||
name: "Công ty TNHH ABC",
|
||||
order_count: 45,
|
||||
paid: 125000000,
|
||||
},
|
||||
{
|
||||
name: 'Siêu thị XYZ',
|
||||
name: "Siêu thị XYZ",
|
||||
order_count: 38,
|
||||
paid: 98000000,
|
||||
},
|
||||
{
|
||||
name: 'Nhà hàng Đông Dương',
|
||||
name: "Nhà hàng Đông Dương",
|
||||
order_count: 32,
|
||||
paid: 87000000,
|
||||
},
|
||||
{
|
||||
name: 'Khách sạn Mường Thanh',
|
||||
name: "Khách sạn Mường Thanh",
|
||||
order_count: 28,
|
||||
paid: 76000000,
|
||||
},
|
||||
{
|
||||
name: 'Cửa hàng Bách Hoá',
|
||||
name: "Cửa hàng Bách Hoá",
|
||||
order_count: 24,
|
||||
paid: 64000000,
|
||||
},
|
||||
|
||||
]
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
@@ -44,4 +42,4 @@ const customers = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,11 @@ const { $shortenCurrency } = useNuxtApp();
|
||||
</div>
|
||||
<p class="font-semibold">{{ $shortenCurrency(revenue) }}</p>
|
||||
</div>
|
||||
<progress class="progress is-small is-primary" :value="revenue" :max="topRevenue">
|
||||
<progress
|
||||
class="progress is-small is-primary"
|
||||
:value="revenue"
|
||||
:max="topRevenue"
|
||||
>
|
||||
15%
|
||||
</progress>
|
||||
</div>
|
||||
@@ -26,4 +30,4 @@ const { $shortenCurrency } = useNuxtApp();
|
||||
.progress {
|
||||
--bulma-size-small: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<script setup>
|
||||
import TopProduct from '@/components/dashboard/TopProduct.vue';
|
||||
import TopProduct from "@/components/dashboard/TopProduct.vue";
|
||||
|
||||
const products = [
|
||||
{
|
||||
name: 'Gạo ST25 - Bao 5kg',
|
||||
name: "Gạo ST25 - Bao 5kg",
|
||||
sold_count: 1250,
|
||||
revenue: 156000000
|
||||
revenue: 156000000,
|
||||
},
|
||||
{
|
||||
name: 'Nước mắm Phú Quốc - Chai 500ml',
|
||||
name: "Nước mắm Phú Quốc - Chai 500ml",
|
||||
sold_count: 980,
|
||||
revenue: 132000000
|
||||
revenue: 132000000,
|
||||
},
|
||||
{
|
||||
name: 'Đường tinh luyện - Bao 1kg',
|
||||
name: "Đường tinh luyện - Bao 1kg",
|
||||
sold_count: 856,
|
||||
revenue: 98000000
|
||||
revenue: 98000000,
|
||||
},
|
||||
{
|
||||
name: 'Dầu ăn Neptune - Chai 1L',
|
||||
name: "Dầu ăn Neptune - Chai 1L",
|
||||
sold_count: 742,
|
||||
revenue: 87000000
|
||||
revenue: 87000000,
|
||||
},
|
||||
{
|
||||
name: 'Bột mì đa dụng - Bao 1kg',
|
||||
name: "Bột mì đa dụng - Bao 1kg",
|
||||
sold_count: 623,
|
||||
revenue: 72000000
|
||||
revenue: 72000000,
|
||||
},
|
||||
]
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
@@ -35,14 +35,14 @@ const products = [
|
||||
<p class="fs-17 font-semibold mb-4">Top sản phẩm</p>
|
||||
<div class="is-flex is-flex-direction-column is-gap-2">
|
||||
<TopProduct
|
||||
v-for="product in products"
|
||||
v-for="product in products"
|
||||
:key="product.name"
|
||||
v-bind="{
|
||||
...product,
|
||||
topRevenue: products[0].revenue
|
||||
topRevenue: products[0].revenue,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -3,17 +3,16 @@ const props = defineProps({
|
||||
name: String,
|
||||
details: String,
|
||||
level: Number,
|
||||
type: String
|
||||
})
|
||||
type: String,
|
||||
});
|
||||
|
||||
const color = computed(() => {
|
||||
if (props.level === 1) return 'yellow';
|
||||
if (props.level === 2) return 'orange';
|
||||
if (props.level === 3) return 'red';
|
||||
})
|
||||
if (props.level === 1) return "yellow";
|
||||
if (props.level === 2) return "orange";
|
||||
if (props.level === 3) return "red";
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['card m-0', `has-background-${color}-95`]"
|
||||
@@ -22,7 +21,7 @@ const color = computed(() => {
|
||||
}"
|
||||
>
|
||||
<div class="card-content p-4 is-flex is-align-items-center is-gap-2">
|
||||
<Icon
|
||||
<Icon
|
||||
:name="type === 'time' ? 'material-symbols:clock-loader-40' : 'material-symbols:box-outline-rounded'"
|
||||
:size="20"
|
||||
:class="`has-text-${color}-45`"
|
||||
@@ -33,4 +32,4 @@ const color = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script setup>
|
||||
import Warning from '@/components/dashboard/Warning.vue';
|
||||
import Warning from "@/components/dashboard/Warning.vue";
|
||||
|
||||
const warnings = [
|
||||
{
|
||||
name: 'Công nợ sắp đến hạn',
|
||||
details: 'Công ty TNHH ABC - 35.000.000đ - Hạn: 25/03/2026',
|
||||
name: "Công nợ sắp đến hạn",
|
||||
details: "Công ty TNHH ABC - 35.000.000đ - Hạn: 25/03/2026",
|
||||
level: 1,
|
||||
type: 'time',
|
||||
type: "time",
|
||||
},
|
||||
{
|
||||
name: 'Đơn giao trễ',
|
||||
details: 'Đơn hàng #DH-2156 - Đã trễ 2 giờ - Khách: Siêu thị XYZ',
|
||||
name: "Đơn giao trễ",
|
||||
details: "Đơn hàng #DH-2156 - Đã trễ 2 giờ - Khách: Siêu thị XYZ",
|
||||
level: 3,
|
||||
type: 'time',
|
||||
type: "time",
|
||||
},
|
||||
{
|
||||
name: 'Tồn kho thấp',
|
||||
details: 'Gạo ST25 - Chỉ còn 45 bao - Cần nhập thêm',
|
||||
name: "Tồn kho thấp",
|
||||
details: "Gạo ST25 - Chỉ còn 45 bao - Cần nhập thêm",
|
||||
level: 2,
|
||||
type: 'inventory'
|
||||
type: "inventory",
|
||||
},
|
||||
]
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
@@ -35,4 +35,4 @@ const warnings = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'az' })"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }"
|
||||
v-bind="{
|
||||
name: 'az.svg',
|
||||
type: checkFilter() ? 'grey' : 'primary',
|
||||
size: 22,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -20,7 +24,11 @@
|
||||
@click="checkFilter() ? false : $emit('modalevent', { name: 'dosort', data: 'za' })"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'az.svg', type: checkFilter() ? 'grey' : 'primary', size: 22 }"
|
||||
v-bind="{
|
||||
name: 'az.svg',
|
||||
type: checkFilter() ? 'grey' : 'primary',
|
||||
size: 22,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -30,7 +38,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="moveLeft()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="moveLeft()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'left5.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -40,7 +51,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="moveRight()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="moveRight()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'right5.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -50,7 +64,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="resizeWidth()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="resizeWidth()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'thick.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -60,7 +77,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="resizeWidth(true)">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="resizeWidth(true)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'thin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -70,7 +90,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="hideField()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="hideField()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'eye-off.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -81,7 +104,10 @@
|
||||
</span>
|
||||
<!-- <template v-if="store.login ? store.login.is_admin : false"> -->
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="currentField.mandatory ? false : doRemove()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="currentField.mandatory ? false : doRemove()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'bin.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -94,11 +120,7 @@
|
||||
<a
|
||||
class="mr-4"
|
||||
:class="currentField.format === 'number' ? null : 'has-text-grey-light'"
|
||||
@click="
|
||||
currentField.format === 'number'
|
||||
? $emit('modalevent', { name: 'copyfield', data: currentField })
|
||||
: false
|
||||
"
|
||||
@click="currentField.format === 'number' ? $emit('modalevent', { name: 'copyfield', data: currentField }) : false"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
@@ -109,7 +131,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="fieldList()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="fieldList()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'menu4.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -119,7 +144,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="createField()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="createField()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'add.png', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -129,7 +157,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="tableOption()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="tableOption()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'more.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -139,7 +170,10 @@
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-4" @click="saveSetting()">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="saveSetting()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span
|
||||
@@ -156,7 +190,7 @@
|
||||
? currentField.formula
|
||||
? true
|
||||
: x.code !== 'formula'
|
||||
: !['filter', 'formula'].find((y) => y === x.code)
|
||||
: !['filter', 'formula'].find((y) => y === x.code),
|
||||
)"
|
||||
:key="i"
|
||||
:class="selectTab.code === v.code ? 'is-active' : 'has-text-primary'"
|
||||
@@ -193,35 +227,44 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button" @click="editLabel()">
|
||||
<button
|
||||
class="button"
|
||||
@click="editLabel()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'pen.svg', type: 'dark', size: 19 }"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.find((v) => v.name === 'label')">
|
||||
<p
|
||||
class="help is-danger"
|
||||
v-if="errors.find((v) => v.name === 'label')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "label").msg }}
|
||||
</p>
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14"
|
||||
>Kiểu dữ liệu<span class="has-text-danger"> * </span></label
|
||||
>
|
||||
<label class="label fs-14">Kiểu dữ liệu<span class="has-text-danger"> * </span></label>
|
||||
<div class="control fs-14">
|
||||
<span class="mr-4" v-for="(v, i) in datatype">
|
||||
<span class="icon-text" v-if="radioType === v">
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'radio-checked.svg', type: 'gray', size: 22 }"
|
||||
></SvgIcon>
|
||||
<span
|
||||
class="mr-4"
|
||||
v-for="(v, i) in datatype"
|
||||
>
|
||||
<span
|
||||
class="icon-text"
|
||||
v-if="radioType === v"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'radio-checked.svg', type: 'gray', size: 22 }"></SvgIcon>
|
||||
</span>
|
||||
{{ v.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal" v-if="currentField.format === 'number'">
|
||||
<div
|
||||
class="field is-horizontal"
|
||||
v-if="currentField.format === 'number'"
|
||||
>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"
|
||||
>Đơn vị <span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<label class="label fs-14">Đơn vị <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
@@ -234,7 +277,10 @@
|
||||
@option="selected('_account', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find((v) => v.name === 'unit')">
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'unit')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "unit").msg }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -261,7 +307,10 @@
|
||||
class="mr-4"
|
||||
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||
>
|
||||
<a class="icon-text" @click="changeTemplate(v)">
|
||||
<a
|
||||
class="icon-text"
|
||||
@click="changeTemplate(v)"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: `radio-${radioTemplate === v.code ? '' : 'un'}checked.svg`,
|
||||
@@ -277,11 +326,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3" v-if="radioTemplate === 'option'">
|
||||
<button class="button is-primary is-small has-text-white" @click="showSidebar()">
|
||||
<span class="fs-14">{{
|
||||
`${currentField.template ? "Sửa" : "Tạo"} định dạng`
|
||||
}}</span>
|
||||
<p
|
||||
class="mt-3"
|
||||
v-if="radioTemplate === 'option'"
|
||||
>
|
||||
<button
|
||||
class="button is-primary is-small has-text-white"
|
||||
@click="showSidebar()"
|
||||
>
|
||||
<span class="fs-14">{{ `${currentField.template ? "Sửa" : "Tạo"} định dạng` }}</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
@@ -308,14 +361,7 @@
|
||||
import { useStore } from "@/stores/index";
|
||||
import ScrollBox from "~/components/datatable/ScrollBox";
|
||||
const store = useStore();
|
||||
const {
|
||||
$copy,
|
||||
$stripHtml,
|
||||
$clone,
|
||||
$arrayMove,
|
||||
$snackbar,
|
||||
$copyToClipboard,
|
||||
} = useNuxtApp();
|
||||
const { $copy, $stripHtml, $clone, $arrayMove, $snackbar, $copyToClipboard } = useNuxtApp();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object,
|
||||
@@ -337,9 +383,7 @@ const getMenu = function () {
|
||||
let field = currentField;
|
||||
field.disable = "display,tooltip";
|
||||
let arr = field.disable ? field.disable.split(",") : undefined;
|
||||
let array = arr
|
||||
? store.menuchoice.filter((v) => arr.findIndex((x) => x === v.code) < 0)
|
||||
: store.menuchoice;
|
||||
let array = arr ? store.menuchoice.filter((v) => arr.findIndex((x) => x === v.code) < 0) : store.menuchoice;
|
||||
//if (store.login ? !(store.login.is_admin === false) : true) array = [array[0]];
|
||||
return array;
|
||||
};
|
||||
@@ -350,10 +394,7 @@ var value1 = undefined;
|
||||
var value2 = undefined;
|
||||
var moneyunit = store.moneyunit;
|
||||
var radioType = store.datatype.find((v) => v.code === currentField.format);
|
||||
var selectUnit =
|
||||
currentField.format === "number"
|
||||
? moneyunit.find((v) => v.detail === currentField.unit)
|
||||
: undefined;
|
||||
var selectUnit = currentField.format === "number" ? moneyunit.find((v) => v.detail === currentField.unit) : undefined;
|
||||
var bgcolor = undefined;
|
||||
var radioBGcolor = colorchoice.find((v) => v.code === "none");
|
||||
var color = undefined;
|
||||
@@ -366,16 +407,12 @@ var radioMaxWidth = colorchoice.find((v) => v.code === "none");
|
||||
var maxwidth = undefined;
|
||||
var selectAlign = undefined;
|
||||
var radioAlign = colorchoice.find((v) => v.code === "none");
|
||||
var radioTemplate = ref(
|
||||
colorchoice.find((v) => v.code === (currentField.template ? "option" : "none"))["code"]
|
||||
);
|
||||
var radioTemplate = ref(colorchoice.find((v) => v.code === (currentField.template ? "option" : "none"))["code"]);
|
||||
var selectPlacement = store.placement.find((v) => v.code === "is-right");
|
||||
var selectScheme = store.colorscheme.find((v) => v.code === "is-primary");
|
||||
var radioTooltip = store.colorchoice.find((v) => v.code === "none");
|
||||
var selectField = undefined;
|
||||
var tags = currentField.tags
|
||||
? currentField.tags.map((v) => fields.find((x) => x.name === v))
|
||||
: [];
|
||||
var tags = currentField.tags ? currentField.tags.map((v) => fields.find((x) => x.name === v)) : [];
|
||||
var formula = currentField.formula ? currentField.formula : undefined;
|
||||
var decimal = currentField.decimal;
|
||||
let shortmenu = store.menuchoice.filter((x) =>
|
||||
@@ -383,7 +420,7 @@ let shortmenu = store.menuchoice.filter((x) =>
|
||||
? currentField.formula
|
||||
? true
|
||||
: x.code !== "formula"
|
||||
: !["filter", "formula"].find((y) => y === x.code)
|
||||
: !["filter", "formula"].find((y) => y === x.code),
|
||||
);
|
||||
var selectTab = shortmenu.find((v) => selectTab.code === v.code)
|
||||
? selectTab
|
||||
@@ -448,9 +485,7 @@ function tableOption() {
|
||||
}
|
||||
const getFields = function () {
|
||||
fields = pagedata ? $copy(pagedata.fields) : [];
|
||||
fields.map(
|
||||
(v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label)
|
||||
);
|
||||
fields.map((v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label));
|
||||
};
|
||||
const doSelect = function (evt) {
|
||||
emit("modalevent", { name: "selected", data: evt[props.field.name] });
|
||||
@@ -510,15 +545,16 @@ const saveSetting = function () {
|
||||
const showSidebar = function () {
|
||||
let event = { name: "template", field: currentField };
|
||||
let title = "Danh sách cột";
|
||||
if (event.name === "bgcolor")
|
||||
title = `Đổi màu nền: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||
else if (event.name === "color")
|
||||
title = `Đổi màu chữ: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||
else if (event.name === "template")
|
||||
title = `Định dạng nâng cao: ${$stripHtml(event.field.label, 30)}`;
|
||||
if (event.name === "bgcolor") title = `Đổi màu nền: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||
else if (event.name === "color") title = `Đổi màu chữ: ${event.field.name} / ${$stripHtml(event.field.label, 30)}`;
|
||||
else if (event.name === "template") title = `Định dạng nâng cao: ${$stripHtml(event.field.label, 30)}`;
|
||||
showmodal.value = {
|
||||
component: "datatable/FormatOption",
|
||||
vbind: { event: event, currentField: currentField, pagename: props.pagename },
|
||||
vbind: {
|
||||
event: event,
|
||||
currentField: currentField,
|
||||
pagename: props.pagename,
|
||||
},
|
||||
width: "850px",
|
||||
height: "700px",
|
||||
title: title,
|
||||
|
||||
@@ -1,180 +1,312 @@
|
||||
<template>
|
||||
<div v-if="docid">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label">Đối tượng</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in types" :key="i" v-model="type"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Kích cỡ</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in sizes.filter(v=>type? (type.code==='tag'? v.code!=='is-small' : 1>0) : true)" :key="i" v-model="size"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" v-if="['tag'].find(v=>v===type.code)">Hình khối</label>
|
||||
<p class="control fs-14" v-if="['tag'].find(v=>v===type.code)">
|
||||
<b-radio v-for="(v,i) in shapes" :key="i" v-model="shape"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal" v-if="['tag'].find(v=>v===type.code)">
|
||||
<div class="field-body">
|
||||
<div class="field" v-if="type.code!=='tag'">
|
||||
<label class="label">Outline</label>
|
||||
<div class="field">
|
||||
<label class="label">Đối tượng</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in outlines" :key="i" v-model="outline"
|
||||
:native-value="v" @input="changeType(v)">
|
||||
{{v.name}}
|
||||
<b-radio
|
||||
v-for="(v, i) in types"
|
||||
:key="i"
|
||||
v-model="type"
|
||||
:native-value="v"
|
||||
@input="changeType(v)"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Kích cỡ</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio
|
||||
v-for="(v, i) in sizes.filter((v) =>
|
||||
type ? (type.code === 'tag' ? v.code !== 'is-small' : 1 > 0) : true,
|
||||
)"
|
||||
:key="i"
|
||||
v-model="size"
|
||||
:native-value="v"
|
||||
@input="changeType(v)"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
class="label"
|
||||
v-if="['tag'].find((v) => v === type.code)"
|
||||
>Hình khối</label
|
||||
>
|
||||
<p
|
||||
class="control fs-14"
|
||||
v-if="['tag'].find((v) => v === type.code)"
|
||||
>
|
||||
<b-radio
|
||||
v-for="(v, i) in shapes"
|
||||
:key="i"
|
||||
v-model="shape"
|
||||
:native-value="v"
|
||||
@input="changeType(v)"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags" v-if="type.code==='tag'">
|
||||
<a :class="getClass(v)" v-for="(v,i) in colorscheme" :key="i"
|
||||
@click="doSelect(v)" :ref="'tag' + i"> {{v.name}} </a>
|
||||
</div>
|
||||
<div class="pt-2" v-else-if="type.code==='span'">
|
||||
<a class="mr-3" :class="getSpanClass(v)" v-for="(v,i) in colorscheme" :key="i"
|
||||
@click="doSelectSpan(v)" :ref="'span' + i"> {{v.name}} </a>
|
||||
<div
|
||||
class="field is-horizontal"
|
||||
v-if="['tag'].find((v) => v === type.code)"
|
||||
>
|
||||
<div class="field-body">
|
||||
<div
|
||||
class="field"
|
||||
v-if="type.code !== 'tag'"
|
||||
>
|
||||
<label class="label">Outline</label>
|
||||
<p class="control fs-14">
|
||||
<b-radio
|
||||
v-for="(v, i) in outlines"
|
||||
:key="i"
|
||||
v-model="outline"
|
||||
:native-value="v"
|
||||
@input="changeType(v)"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`tabs is-boxed mt-5 mb-5 ${tab.code==='template'? '' : 'pb-2'}`">
|
||||
<div
|
||||
class="tags"
|
||||
v-if="type.code === 'tag'"
|
||||
>
|
||||
<a
|
||||
:class="getClass(v)"
|
||||
v-for="(v, i) in colorscheme"
|
||||
:key="i"
|
||||
@click="doSelect(v)"
|
||||
:ref="'tag' + i"
|
||||
>
|
||||
{{ v.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="pt-2"
|
||||
v-else-if="type.code === 'span'"
|
||||
>
|
||||
<a
|
||||
class="mr-3"
|
||||
:class="getSpanClass(v)"
|
||||
v-for="(v, i) in colorscheme"
|
||||
:key="i"
|
||||
@click="doSelectSpan(v)"
|
||||
:ref="'span' + i"
|
||||
>
|
||||
{{ v.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div :class="`tabs is-boxed mt-5 mb-5 ${tab.code === 'template' ? '' : 'pb-2'}`">
|
||||
<ul>
|
||||
<li :class="tab.code===v.code? 'is-active' : ''"
|
||||
v-for="(v,i) in tabs" :key="i" @click="changeTab(v)"><a class="fs-15">{{v.name}}</a>
|
||||
<li
|
||||
:class="tab.code === v.code ? 'is-active' : ''"
|
||||
v-for="(v, i) in tabs"
|
||||
:key="i"
|
||||
@click="changeTab(v)"
|
||||
>
|
||||
<a class="fs-15">{{ v.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<template v-if="tab.code==='selected'">
|
||||
<a v-for="(v,i) in tags" :key="i" @click="selected=v">
|
||||
<div class="field is-grouped is-grouped-multiline mt-4">
|
||||
<p class="control">
|
||||
<a :class="v.class">
|
||||
{{v.name}}
|
||||
<template v-if="tab.code === 'selected'">
|
||||
<a
|
||||
v-for="(v, i) in tags"
|
||||
:key="i"
|
||||
@click="selected = v"
|
||||
>
|
||||
<div class="field is-grouped is-grouped-multiline mt-4">
|
||||
<p class="control">
|
||||
<a :class="v.class">
|
||||
{{ v.name }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
v-model="v.name"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a @click="remove(i)">
|
||||
<SvgIcon v-bind="{ name: 'close.svg', type: 'danger', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="control has-text-right ml-5"
|
||||
v-if="selected ? selected.id === v.id : false"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 22 }"></SvgIcon>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<input class="input is-small" type="text" v-model="v.name">
|
||||
</p>
|
||||
<p class="control">
|
||||
<a @click="remove(i)">
|
||||
<SvgIcon v-bind="{name: 'close.svg', type: 'danger', size: 22}"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
<p class="control has-text-right ml-5" v-if="selected? selected.id===v.id : false">
|
||||
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 22}"></SvgIcon>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code==='condition'">
|
||||
<div class="mb-5" v-if="selected">
|
||||
<b-radio v-for="(v,i) in conditions" :key="i" v-model="condition"
|
||||
:native-value="v" @input="changeCondition(v)">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
</div>
|
||||
|
||||
<template v-if="condition? condition.code==='yes' : false">
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14">Chọn trường xây dựng biểu thức <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tagsField"
|
||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="name"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 50)}} </span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>
|
||||
<template v-else-if="tab.code === 'condition'">
|
||||
<div
|
||||
class="mb-5"
|
||||
v-if="selected"
|
||||
>
|
||||
<b-radio
|
||||
v-for="(v, i) in conditions"
|
||||
:key="i"
|
||||
v-model="condition"
|
||||
:native-value="v"
|
||||
@input="changeCondition(v)"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tagsField')"> {{errors.find(v=>v.name==='tagsField').message}} </p>
|
||||
</div>
|
||||
<div class="field mt-1" v-if="tagsField.length>0">
|
||||
<p class="help is-primary"> Click đúp vào để thêm vào biểu thức.</p>
|
||||
|
||||
<template v-if="condition ? condition.code === 'yes' : false">
|
||||
<div class="field mt-3">
|
||||
<label class="label fs-14"
|
||||
>Chọn trường xây dựng biểu thức
|
||||
<span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
v-model="tagsField"
|
||||
:data="pageData ? pageData.fields.filter((v) => v.format === 'number') : []"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="name"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{ props.option.name }}</span>
|
||||
<span :class="tagsField.find((v) => v.id === props.option.id) ? 'has-text-dark' : ''">
|
||||
{{ $stripHtml(props.option.label, 50) }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="empty"> Không có trường thỏa mãn </template>
|
||||
</b-taginput>
|
||||
</div>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'tagsField')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "tagsField").message }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="field mt-1"
|
||||
v-if="tagsField.length > 0"
|
||||
>
|
||||
<p class="help is-primary">Click đúp vào để thêm vào biểu thức.</p>
|
||||
<div class="tagsField">
|
||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
||||
<span class="tooltip">
|
||||
{{v.name}}
|
||||
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
|
||||
</span>
|
||||
<a
|
||||
@dblclick="expression = expression ? expression + ' ' + v.name : v.name"
|
||||
class="tag is-rounded"
|
||||
v-for="(v, i) in tagsField"
|
||||
:key="i"
|
||||
>
|
||||
<span class="tooltip">
|
||||
{{ v.name }}
|
||||
<span class="tooltiptext">{{ $stripHtml(v.label) }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span> </label>
|
||||
<label class="label fs-14"
|
||||
>Biểu thức có dạng Đúng / Sai
|
||||
<span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="text" v-model="expression" placeholder="Tạo biểu thức tại đây">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="expression"
|
||||
placeholder="Tạo biểu thức tại đây"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'expression')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "expression").message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code==='option' && selected">
|
||||
<div class="field is-horizontal border-bottom pb-2 mt-1">
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab.code === 'option' && selected">
|
||||
<div class="field is-horizontal border-bottom pb-2 mt-1">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu nền </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioBGcolor"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
<b-radio
|
||||
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||
:key="i"
|
||||
v-model="radioBGcolor"
|
||||
:native-value="v"
|
||||
@input="changeStyle()"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioBGcolor? radioBGcolor.code==='option' : false">
|
||||
<div
|
||||
class="field"
|
||||
v-if="radioBGcolor ? radioBGcolor.code === 'option' : false"
|
||||
>
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="bgcolor" @change="changeStyle()">
|
||||
<input
|
||||
type="color"
|
||||
v-model="bgcolor"
|
||||
@change="changeStyle()"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field is-horizontal border-bottom pb-2">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Màu chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioColor"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
<b-radio
|
||||
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||
:key="i"
|
||||
v-model="radioColor"
|
||||
:native-value="v"
|
||||
@input="changeStyle()"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioColor? radioColor.code==='option' : false">
|
||||
<div
|
||||
class="field"
|
||||
v-if="radioColor ? radioColor.code === 'option' : false"
|
||||
>
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="color" @change="changeStyle()">
|
||||
<input
|
||||
type="color"
|
||||
v-model="color"
|
||||
@change="changeStyle()"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,119 +316,189 @@
|
||||
<div class="field">
|
||||
<label class="label fs-14">Cỡ chữ </label>
|
||||
<p class="control fs-14">
|
||||
<b-radio v-for="(v,i) in colorchoice.filter(v=>v.code!=='condition')" :key="i" v-model="radioSize"
|
||||
:native-value="v" @input="changeStyle()">
|
||||
{{v.name}}
|
||||
</b-radio>
|
||||
<b-radio
|
||||
v-for="(v, i) in colorchoice.filter((v) => v.code !== 'condition')"
|
||||
:key="i"
|
||||
v-model="radioSize"
|
||||
:native-value="v"
|
||||
@input="changeStyle()"
|
||||
>
|
||||
{{ v.name }}
|
||||
</b-radio>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" v-if="radioSize? radioSize.code==='option' : false">
|
||||
<div
|
||||
class="field"
|
||||
v-if="radioSize ? radioSize.code === 'option' : false"
|
||||
>
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="textsize" @change="changeStyle()">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
placeholder="Nhập số"
|
||||
v-model="textsize"
|
||||
@change="changeStyle()"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="tab.code==='template'">
|
||||
<p class="mb-3">
|
||||
<a @click="copyContent()" class="mr-6">
|
||||
<span class="icon-text">
|
||||
<SvgIcon class="mr-2" v-bind="{name: 'copy.svg', type: 'primary', siz: 18}"></SvgIcon>
|
||||
</template>
|
||||
<template v-else-if="tab.code === 'template'">
|
||||
<p class="mb-3">
|
||||
<a
|
||||
@click="copyContent()"
|
||||
class="mr-6"
|
||||
>
|
||||
<span class="icon-text">
|
||||
<SvgIcon
|
||||
class="mr-2"
|
||||
v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"
|
||||
></SvgIcon>
|
||||
<span class="fs-16">Copy</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click="paste()" class="mr-6">
|
||||
<a
|
||||
@click="paste()"
|
||||
class="mr-6"
|
||||
>
|
||||
<span class="icon-text">
|
||||
<SvgIcon class="mr-2" v-bind="{name: 'pen1.svg', type: 'primary', siz: 18}"></SvgIcon>
|
||||
<SvgIcon
|
||||
class="mr-2"
|
||||
v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"
|
||||
></SvgIcon>
|
||||
<span class="fs-16">Paste</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div>
|
||||
<textarea class="textarea fs-14" rows="8" v-model="text" @dblclick="doCheck"></textarea>
|
||||
</div>
|
||||
<p class="mt-5">
|
||||
<div>
|
||||
<textarea
|
||||
class="textarea fs-14"
|
||||
rows="8"
|
||||
v-model="text"
|
||||
@dblclick="doCheck"
|
||||
></textarea>
|
||||
</div>
|
||||
<p class="mt-5">
|
||||
<span class="icon-text fsb-18">
|
||||
Replace
|
||||
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 22}"></SvgIcon>
|
||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
|
||||
</span>
|
||||
</p>
|
||||
<div class="field is-grouped mt-4">
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Đoạn text</p>
|
||||
<input class="input" type="text" placeholder="" v-model="source">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="source"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Thay bằng</p>
|
||||
<input class="input" type="text" placeholder="" v-model="target">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="target"
|
||||
/>
|
||||
</div>
|
||||
<div class="control pl-5">
|
||||
<button class="button is-primary is-outlined mt-5" @click="replace()">Replace</button>
|
||||
<button
|
||||
class="button is-primary is-outlined mt-5"
|
||||
@click="replace()"
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-5">
|
||||
<button class="button is-primary has-text-white" @click="changeTemplate()">Áp dụng</button>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="changeTemplate()"
|
||||
>
|
||||
Áp dụng
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useStore } from '@/stores/index'
|
||||
const store = useStore()
|
||||
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp()
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object
|
||||
})
|
||||
var colorscheme = store.colorscheme
|
||||
var colorchoice = store.colorchoice
|
||||
var pageData = store[props.pagename]
|
||||
var field = props.field
|
||||
var type = undefined
|
||||
var size = undefined
|
||||
var types = [{code: 'span', name: 'span'}, {code: 'tag', name: 'tag'}]
|
||||
var sizes = [{code: 'is-small', name: 'Nhỏ', value: 'is-size-6'}, {code: 'is-normal', name: 'Trung bình', value: 'is-size-5'},
|
||||
{code: 'is-medium', name: 'Lớn', value: 'is-size-4'}]
|
||||
var shapes = [{code: 'default', name: 'Mặc định'}, {code: 'is-rounded', name: 'Tròn góc'}]
|
||||
var shape = undefined
|
||||
var outlines = [{code: 'default', name: 'Mặc định'}, {code: 'is-outlined', name: 'Outline'}]
|
||||
var outline = undefined
|
||||
var conditions = [{code: 'no', name: 'Không áp dụng'}, {code: 'yes', name: 'Có áp dụng'}]
|
||||
var condition = undefined
|
||||
var tags = []
|
||||
var selected = undefined
|
||||
var tabs = [{code: 'selected', name: 'Bước 1: Tạo nội dung'}, {code: 'condition', name: 'Bước 2: Đặt điều kiện'}, {code: 'option', name: 'Bước 3: Chọn màu, cỡ chữ'},
|
||||
{code: 'template', name: 'Bước 4: Mã lệnh & áp dụng'}]
|
||||
var tab = ref(undefined)
|
||||
var tagsField = []
|
||||
var errors = []
|
||||
var expression = ''
|
||||
var text = ref(null)
|
||||
var radioBGcolor = undefined
|
||||
var radioColor = undefined
|
||||
var radioSize = undefined
|
||||
var bgcolor = undefined
|
||||
var color = undefined
|
||||
var textsize = undefined
|
||||
var source = undefined
|
||||
var target = $copy(field.name)
|
||||
|
||||
const initData = function() {
|
||||
type = types.find(v=>v.code==='tag')
|
||||
size = sizes.find(v=>v.code==='is-normal')
|
||||
shape = shapes.find(v=>v.code==='is-rounded')
|
||||
outline = shapes.find(v=>v.code==='default')
|
||||
if($empty(field.template)) tab.value =tabs.find(v=>v.code==='selected')
|
||||
else {
|
||||
text.value =$copy(field.template)
|
||||
tab.value =tabs.find(v=>v.code==='template')
|
||||
}
|
||||
condition =conditions.find(v=>v.code==='no')
|
||||
import { ref } from "vue";
|
||||
import { useStore } from "@/stores/index";
|
||||
const store = useStore();
|
||||
const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = useNuxtApp();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object,
|
||||
});
|
||||
var colorscheme = store.colorscheme;
|
||||
var colorchoice = store.colorchoice;
|
||||
var pageData = store[props.pagename];
|
||||
var field = props.field;
|
||||
var type = undefined;
|
||||
var size = undefined;
|
||||
var types = [
|
||||
{ code: "span", name: "span" },
|
||||
{ code: "tag", name: "tag" },
|
||||
];
|
||||
var sizes = [
|
||||
{ code: "is-small", name: "Nhỏ", value: "is-size-6" },
|
||||
{ code: "is-normal", name: "Trung bình", value: "is-size-5" },
|
||||
{ code: "is-medium", name: "Lớn", value: "is-size-4" },
|
||||
];
|
||||
var shapes = [
|
||||
{ code: "default", name: "Mặc định" },
|
||||
{ code: "is-rounded", name: "Tròn góc" },
|
||||
];
|
||||
var shape = undefined;
|
||||
var outlines = [
|
||||
{ code: "default", name: "Mặc định" },
|
||||
{ code: "is-outlined", name: "Outline" },
|
||||
];
|
||||
var outline = undefined;
|
||||
var conditions = [
|
||||
{ code: "no", name: "Không áp dụng" },
|
||||
{ code: "yes", name: "Có áp dụng" },
|
||||
];
|
||||
var condition = undefined;
|
||||
var tags = [];
|
||||
var selected = undefined;
|
||||
var tabs = [
|
||||
{ code: "selected", name: "Bước 1: Tạo nội dung" },
|
||||
{ code: "condition", name: "Bước 2: Đặt điều kiện" },
|
||||
{ code: "option", name: "Bước 3: Chọn màu, cỡ chữ" },
|
||||
{ code: "template", name: "Bước 4: Mã lệnh & áp dụng" },
|
||||
];
|
||||
var tab = ref(undefined);
|
||||
var tagsField = [];
|
||||
var errors = [];
|
||||
var expression = "";
|
||||
var text = ref(null);
|
||||
var radioBGcolor = undefined;
|
||||
var radioColor = undefined;
|
||||
var radioSize = undefined;
|
||||
var bgcolor = undefined;
|
||||
var color = undefined;
|
||||
var textsize = undefined;
|
||||
var source = undefined;
|
||||
var target = $copy(field.name);
|
||||
|
||||
const initData = function () {
|
||||
type = types.find((v) => v.code === "tag");
|
||||
size = sizes.find((v) => v.code === "is-normal");
|
||||
shape = shapes.find((v) => v.code === "is-rounded");
|
||||
outline = shapes.find((v) => v.code === "default");
|
||||
if ($empty(field.template)) tab.value = tabs.find((v) => v.code === "selected");
|
||||
else {
|
||||
text.value = $copy(field.template);
|
||||
tab.value = tabs.find((v) => v.code === "template");
|
||||
}
|
||||
/*watch: {
|
||||
condition = conditions.find((v) => v.code === "no");
|
||||
};
|
||||
/*watch: {
|
||||
expression: function(newVal) {
|
||||
if($empty(newVal)) return
|
||||
elsecheckExpression()
|
||||
@@ -332,100 +534,98 @@ const { $id, $copy, $empty, $stripHtml, $calc, $remove, $copyToClipboard } = use
|
||||
}
|
||||
}
|
||||
},*/
|
||||
function changeTab(v) {
|
||||
tab.value = v
|
||||
function changeTab(v) {
|
||||
tab.value = v;
|
||||
}
|
||||
const paste = async function () {
|
||||
text.value = await navigator.clipboard.readText();
|
||||
};
|
||||
const replace = function () {
|
||||
if ($empty(text.value)) return;
|
||||
text.value = text.value.replaceAll(source, target);
|
||||
};
|
||||
const doCheck = function () {
|
||||
let text = window.getSelection().toString();
|
||||
if ($empty(text)) return;
|
||||
source = text;
|
||||
};
|
||||
const changeStyle = function () {
|
||||
selected.bgcolor = selected.color = selected.textsize = selected.style = undefined;
|
||||
let style = "";
|
||||
if (radioBGcolor.code === "option" ? !$empty(bgcolor) : false) {
|
||||
selected.bgcolor = bgcolor;
|
||||
style += "background-color: " + bgcolor + " !important; ";
|
||||
}
|
||||
const paste = async function() {
|
||||
text.value = await navigator.clipboard.readText()
|
||||
if (radioColor.code === "option" ? !$empty(color) : false) {
|
||||
selected.color = color;
|
||||
style += "color: " + color + " !important; ";
|
||||
}
|
||||
if (radioSize.code === "option" ? $isNumber(textsize) : false) {
|
||||
selected.textsize = textsize;
|
||||
style += "font-size: " + textsize + "px !important; ";
|
||||
}
|
||||
$empty(style) ? false : (selected.style = style);
|
||||
};
|
||||
const changeCondition = function (v) {
|
||||
if (v.code === "no") selected.expression = undefined;
|
||||
};
|
||||
const copyContent = function () {
|
||||
$copyToClipboard(text.value);
|
||||
};
|
||||
const changeTemplate = function () {
|
||||
let copy = pageData;
|
||||
let found = copy.fields.find((v) => v.name === field.name);
|
||||
found.template = text.value;
|
||||
store.commit(props.pagename, copy);
|
||||
};
|
||||
const checkExpression = function () {
|
||||
errors = [];
|
||||
let val = $copy(expression);
|
||||
let exp = $copy(expression);
|
||||
tagsField.forEach((v) => {
|
||||
let myRegExp = new RegExp(v.name, "g");
|
||||
val = val.replace(myRegExp, Math.random());
|
||||
exp = exp.replace(myRegExp, "formatNumber(row['" + v.name + "'])");
|
||||
});
|
||||
try {
|
||||
let value = $calc(val);
|
||||
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
|
||||
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
|
||||
} else if (!(eval(value) === true || eval(value) === false)) {
|
||||
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
|
||||
} else if (selected) {
|
||||
selected.expression = exp;
|
||||
selected.formula = expression;
|
||||
selected.tags = $copy(tagsField);
|
||||
}
|
||||
const replace = function() {
|
||||
if($empty(text.value)) return
|
||||
text.value =text.value.replaceAll(source,target)
|
||||
}
|
||||
const doCheck = function() {
|
||||
let text = window.getSelection().toString()
|
||||
if($empty(text)) return
|
||||
source = text
|
||||
}
|
||||
const changeStyle = function() {
|
||||
selected.bgcolor =selected.color =selected.textsize =selected.style = undefined
|
||||
let style = ''
|
||||
if(radioBGcolor.code==='option'? !$empty(bgcolor) : false) {
|
||||
selected.bgcolor =bgcolor
|
||||
style += 'background-color: ' +bgcolor + ' !important; '
|
||||
}
|
||||
if(radioColor.code==='option'? !$empty(color) : false) {
|
||||
selected.color =color
|
||||
style += 'color: ' +color + ' !important; '
|
||||
}
|
||||
if(radioSize.code==='option'?$isNumber(textsize) : false) {
|
||||
selected.textsize =textsize
|
||||
style += 'font-size: ' +textsize + 'px !important; '
|
||||
}
|
||||
$empty(style)? false :selected.style = style
|
||||
}
|
||||
const changeCondition = function(v) {
|
||||
if(v.code==='no')selected.expression = undefined
|
||||
}
|
||||
const copyContent = function() {
|
||||
$copyToClipboard(text.value)
|
||||
}
|
||||
const changeTemplate = function() {
|
||||
let copy = pageData
|
||||
let found = copy.fields.find(v=>v.name===field.name)
|
||||
found.template = text.value
|
||||
store.commit(props.pagename, copy)
|
||||
}
|
||||
const checkExpression = function() {
|
||||
errors = []
|
||||
let val =$copy(expression)
|
||||
let exp =$copy(expression)
|
||||
tagsField.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
exp = exp.replace(myRegExp, "formatNumber(row['" + v.name + "'])")
|
||||
})
|
||||
try {
|
||||
let value =$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(!(eval(value)===true || eval(value)===false)) {
|
||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(selected) {
|
||||
selected.expression = exp
|
||||
selected.formula =expression
|
||||
selected.tags =$copy(tagsField)
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
}
|
||||
returnerrors.length>0? false : true
|
||||
}
|
||||
const changeType = function(v) {
|
||||
}
|
||||
const doSelect = function(v) {
|
||||
tags.push({id:$id(), name: v.name, class:getClass(v)})
|
||||
tab =tabs.find(v=>v.code==='selected')
|
||||
selected =tags[tags.length-1]
|
||||
}
|
||||
const doSelectSpan = function(v) {
|
||||
tags.push({id:$id(), name: v.name, class:getSpanClass(v)})
|
||||
tab =tabs.find(v=>v.code==='selected')
|
||||
selected =tags[tags.length-1]
|
||||
}
|
||||
const remove = function(i) {
|
||||
$remove(tags, i)
|
||||
}
|
||||
const getClass = function(v) {
|
||||
let value =type.code + ' ' + v.code + ' ' +size.code + (shape.code==='default'? '' : ' ' +shape.code)
|
||||
value += (outline.code==='default'? '' : ' ' +outline.code)
|
||||
return value
|
||||
}
|
||||
const getSpanClass = function(v) {
|
||||
let value = 'has-text-' + v.name.toLowerCase() + ' ' +size.value
|
||||
return value
|
||||
}
|
||||
initData()
|
||||
var docid = $id()
|
||||
</script>
|
||||
} catch (err) {
|
||||
errors.push({ name: "expression", message: "Biểu thức không hợp lệ" });
|
||||
}
|
||||
returnerrors.length > 0 ? false : true;
|
||||
};
|
||||
const changeType = function (v) {};
|
||||
const doSelect = function (v) {
|
||||
tags.push({ id: $id(), name: v.name, class: getClass(v) });
|
||||
tab = tabs.find((v) => v.code === "selected");
|
||||
selected = tags[tags.length - 1];
|
||||
};
|
||||
const doSelectSpan = function (v) {
|
||||
tags.push({ id: $id(), name: v.name, class: getSpanClass(v) });
|
||||
tab = tabs.find((v) => v.code === "selected");
|
||||
selected = tags[tags.length - 1];
|
||||
};
|
||||
const remove = function (i) {
|
||||
$remove(tags, i);
|
||||
};
|
||||
const getClass = function (v) {
|
||||
let value = type.code + " " + v.code + " " + size.code + (shape.code === "default" ? "" : " " + shape.code);
|
||||
value += outline.code === "default" ? "" : " " + outline.code;
|
||||
return value;
|
||||
};
|
||||
const getSpanClass = function (v) {
|
||||
let value = "has-text-" + v.name.toLowerCase() + " " + size.value;
|
||||
return value;
|
||||
};
|
||||
initData();
|
||||
var docid = $id();
|
||||
</script>
|
||||
|
||||
@@ -1,193 +1,314 @@
|
||||
<template>
|
||||
<div class="columns mx-0">
|
||||
<div class="column is-2">
|
||||
<Caption class="mb-2" v-bind="{title: 'Tên model (bảng)', type: 'has-text-warning'}"></Caption>
|
||||
<div class="mb-2">
|
||||
<input class="input" v-model="text" placeholder="Tìm model" @change="findModel()">
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: 'Tên model (bảng)', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<div class="mb-2">
|
||||
<input
|
||||
class="input"
|
||||
v-model="text"
|
||||
placeholder="Tìm model"
|
||||
@change="findModel()"
|
||||
/>
|
||||
</div>
|
||||
<div style="max-height: 80vh; overflow: auto">
|
||||
<div
|
||||
:class="`py-1 border-bottom is-clickable ${current.model === v.model ? 'has-background-primary has-text-white' : ''}`"
|
||||
v-for="v in displayData"
|
||||
@click="changeMenu(v)"
|
||||
>
|
||||
{{ v.model }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-height: 80vh; overflow: auto;">
|
||||
<div :class="`py-1 border-bottom is-clickable ${current.model===v.model? 'has-background-primary has-text-white' : ''}`"
|
||||
v-for="v in displayData" @click="changeMenu(v)">
|
||||
{{ v.model}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-10 py-0 px-0">
|
||||
<div class="tabs mb-3">
|
||||
<ul>
|
||||
<li :class="`${v.code===tab? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs">
|
||||
<a @click="changeTab(v)">{{v.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="tab==='datatype'">
|
||||
<Caption class="mb-2" v-bind="{title: 'Kiểu dữ liệu (type)', type: 'has-text-warning'}"></Caption>
|
||||
<div style="max-height:75vh; overflow-y: auto;">
|
||||
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields">
|
||||
{{ x.name}}
|
||||
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
||||
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a>
|
||||
<div class="column is-10 py-0 px-0">
|
||||
<div class="tabs mb-3">
|
||||
<ul>
|
||||
<li
|
||||
:class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`"
|
||||
v-for="v in tabs"
|
||||
>
|
||||
<a @click="changeTab(v)">{{ v.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="tab === 'datatype'">
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<div style="max-height: 75vh; overflow-y: auto">
|
||||
<div
|
||||
class="py-1 border-bottom is-clickable"
|
||||
v-for="x in current.fields"
|
||||
>
|
||||
{{ x.name }}
|
||||
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
||||
<a
|
||||
class="ml-6 has-text-primary"
|
||||
v-if="x.model"
|
||||
@click="openModel(x)"
|
||||
>{{ x.model }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else-if="tab === 'table'">
|
||||
<div class="columns mx-0 mb-0 pb-0">
|
||||
<div class="column is-5">
|
||||
<Caption
|
||||
class="mb-1"
|
||||
v-bind="{ title: 'Values', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<input
|
||||
class="input"
|
||||
rows="1"
|
||||
v-model="values"
|
||||
placeholder="Tên trường không chứa dấu cách, vd: code,name"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<Caption
|
||||
class="mb-1"
|
||||
v-bind="{ title: 'Filter', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<input
|
||||
class="input"
|
||||
rows="1"
|
||||
v-model="filter"
|
||||
placeholder="{'code': 'xyz'}"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<Caption
|
||||
class="mb-1"
|
||||
v-bind="{ title: 'Sort', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<input
|
||||
class="input"
|
||||
rows="1"
|
||||
v-model="sort"
|
||||
placeholder="vd: -code,name"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<Caption
|
||||
class="mb-1"
|
||||
v-bind="{ title: 'Load', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<div>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="loadData()"
|
||||
>
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Caption
|
||||
class="mb-1"
|
||||
v-bind="{ title: 'Query', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<div class="mb-2">
|
||||
{{ query }}
|
||||
<a
|
||||
class="has-text-primary ml-5"
|
||||
@click="copy()"
|
||||
>copy</a
|
||||
>
|
||||
<p>
|
||||
{{ apiUrl }}
|
||||
<a
|
||||
class="has-text-primary ml-5"
|
||||
@click="$copyToClipboard(apiUrl)"
|
||||
>copy</a
|
||||
>
|
||||
<a
|
||||
class="has-text-primary ml-5"
|
||||
target="_blank"
|
||||
:href="apiUrl"
|
||||
>open</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<DataTable
|
||||
v-bind="{ pagename: pagename }"
|
||||
v-if="pagedata"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
<img
|
||||
id="image"
|
||||
:src="filePath"
|
||||
alt=""
|
||||
/>
|
||||
<p class="pl-5">
|
||||
<a
|
||||
class="mr-5"
|
||||
@click="downloadFile()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'download.svg', type: 'black', size: 24 }"></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="filePath"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'open.svg', type: 'black', size: 24 }"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else-if="tab==='table'">
|
||||
<div class="columns mx-0 mb-0 pb-0">
|
||||
<div class="column is-5">
|
||||
<Caption class="mb-1" v-bind="{title: 'Values', type: 'has-text-warning'}"></Caption>
|
||||
<input class="input" rows="1" v-model="values" placeholder="Tên trường không chứa dấu cách, vd: code,name">
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<Caption class="mb-1" v-bind="{title: 'Filter', type: 'has-text-warning'}"></Caption>
|
||||
<input class="input" rows="1" v-model="filter" placeholder="{'code': 'xyz'}">
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<Caption class="mb-1" v-bind="{title: 'Sort', type: 'has-text-warning'}"></Caption>
|
||||
<input class="input" rows="1" v-model="sort" placeholder="vd: -code,name">
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<Caption class="mb-1" v-bind="{title: 'Load', type: 'has-text-warning'}"></Caption>
|
||||
<div>
|
||||
<button class="button is-primary has-text-white" @click="loadData()">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Caption class="mb-1" v-bind="{title: 'Query', type: 'has-text-warning'}"></Caption>
|
||||
<div class="mb-2">
|
||||
{{ query }}
|
||||
<a class="has-text-primary ml-5" @click="copy()">copy</a>
|
||||
<p>{{apiUrl}}
|
||||
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a>
|
||||
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<DataTable v-bind="{pagename: pagename}" v-if="pagedata" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
<img id="image" :src="filePath" alt="">
|
||||
<p class="pl-5">
|
||||
<a class="mr-5" @click="downloadFile()">
|
||||
<SvgIcon v-bind="{name: 'download.svg', type: 'black', size: 24}"></SvgIcon>
|
||||
</a>
|
||||
<a target="_blank" :href="filePath">
|
||||
<SvgIcon v-bind="{name: 'open.svg', type: 'black', size: 24}"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/index'
|
||||
const { $getdata, $getapi, $createField, $clone, $getpage, $empty, $copyToClipboard, $find, $multiSort, $download, $getpath } = useNuxtApp()
|
||||
const store = useStore()
|
||||
var pagename = 'pagedata3'
|
||||
var pagedata = ref()
|
||||
pagedata.value = $getpage()
|
||||
pagedata.value.perPage = 10
|
||||
store.commit(pagename, pagedata)
|
||||
let list = ['LogEntry', 'Permission', 'ContentType', 'Session', 'Group']
|
||||
var data = (await $getdata('getmodel')).filter(v=>list.findIndex(x=>x===v.model)<0)
|
||||
data = $multiSort(data, {model: 'asc'})
|
||||
var current = ref({fields: []})
|
||||
var tabs = [{code: 'datatype', name: 'Kiểu dữ liệu'}, {code: 'table', name: 'Dữ liệu'}, {code: 'datamodel', name: 'Data model'}]
|
||||
var tab = ref('datatype')
|
||||
var datatable = ref()
|
||||
var query = ref()
|
||||
var values, filter
|
||||
var apiUrl = ref()
|
||||
var showmodal = ref()
|
||||
var text = null
|
||||
var displayData = ref(data)
|
||||
var filePath = `${$getpath()}static/files/datamodel.png`
|
||||
var sort = "-id"
|
||||
current.value = data[0]
|
||||
import { useStore } from "@/stores/index";
|
||||
const {
|
||||
$getdata,
|
||||
$getapi,
|
||||
$createField,
|
||||
$clone,
|
||||
$getpage,
|
||||
$empty,
|
||||
$copyToClipboard,
|
||||
$find,
|
||||
$multiSort,
|
||||
$download,
|
||||
$getpath,
|
||||
} = useNuxtApp();
|
||||
const store = useStore();
|
||||
var pagename = "pagedata3";
|
||||
var pagedata = ref();
|
||||
pagedata.value = $getpage();
|
||||
pagedata.value.perPage = 10;
|
||||
store.commit(pagename, pagedata);
|
||||
let list = ["LogEntry", "Permission", "ContentType", "Session", "Group"];
|
||||
var data = (await $getdata("getmodel")).filter((v) => list.findIndex((x) => x === v.model) < 0);
|
||||
data = $multiSort(data, { model: "asc" });
|
||||
var current = ref({ fields: [] });
|
||||
var tabs = [
|
||||
{ code: "datatype", name: "Kiểu dữ liệu" },
|
||||
{ code: "table", name: "Dữ liệu" },
|
||||
{ code: "datamodel", name: "Data model" },
|
||||
];
|
||||
var tab = ref("datatype");
|
||||
var datatable = ref();
|
||||
var query = ref();
|
||||
var values, filter;
|
||||
var apiUrl = ref();
|
||||
var showmodal = ref();
|
||||
var text = null;
|
||||
var displayData = ref(data);
|
||||
var filePath = `${$getpath()}static/files/datamodel.png`;
|
||||
var sort = "-id";
|
||||
current.value = data[0];
|
||||
function changeMenu(v) {
|
||||
values = undefined
|
||||
filter = undefined
|
||||
sort = undefined
|
||||
current.value = v
|
||||
if(tab.value==='table') loadData()
|
||||
values = undefined;
|
||||
filter = undefined;
|
||||
sort = undefined;
|
||||
current.value = v;
|
||||
if (tab.value === "table") loadData();
|
||||
}
|
||||
async function changeTab(v) {
|
||||
tab.value = v.code
|
||||
if(v.code==='table') loadData()
|
||||
tab.value = v.code;
|
||||
if (v.code === "table") loadData();
|
||||
}
|
||||
async function loadData() {
|
||||
let vfilter = filter? filter.trim() : undefined
|
||||
if(vfilter) {
|
||||
let vfilter = filter ? filter.trim() : undefined;
|
||||
if (vfilter) {
|
||||
try {
|
||||
vfilter = JSON.parse(vfilter)
|
||||
vfilter = JSON.parse(vfilter);
|
||||
} catch (error) {
|
||||
alert('Cấu trúc filter có lỗi')
|
||||
vfilter = undefined
|
||||
alert("Cấu trúc filter có lỗi");
|
||||
vfilter = undefined;
|
||||
}
|
||||
}
|
||||
let params = {values: $empty(values)? undefined : values.trim(), filter: filter, sort: $empty(sort)? undefined : sort.trim()}
|
||||
let modelName = current.value.model
|
||||
let found = {name: modelName.toLowerCase().replace('_', ''), url: `data/${modelName}/`, url_detail: `data-detail/${modelName}/`, params: params}
|
||||
query.value = $clone(found)
|
||||
let rs = await $getapi([found])
|
||||
if(rs==='error') return alert('Đã xảy ra lỗi, hãy xem lại câu lệnh.')
|
||||
datatable.value = rs[0].data.rows
|
||||
showData()
|
||||
|
||||
let params = {
|
||||
values: $empty(values) ? undefined : values.trim(),
|
||||
filter: filter,
|
||||
sort: $empty(sort) ? undefined : sort.trim(),
|
||||
};
|
||||
let modelName = current.value.model;
|
||||
let found = {
|
||||
name: modelName.toLowerCase().replace("_", ""),
|
||||
url: `data/${modelName}/`,
|
||||
url_detail: `data-detail/${modelName}/`,
|
||||
params: params,
|
||||
};
|
||||
query.value = $clone(found);
|
||||
let rs = await $getapi([found]);
|
||||
if (rs === "error") return alert("Đã xảy ra lỗi, hãy xem lại câu lệnh.");
|
||||
datatable.value = rs[0].data.rows;
|
||||
showData();
|
||||
|
||||
// api query
|
||||
const baseUrl = $getpath() + `${query.value.url}`
|
||||
apiUrl.value = baseUrl
|
||||
let vparams = !$empty(values)? {values: values} : null
|
||||
if(!$empty(filter)) {
|
||||
vparams = vparams? {values: values, filter:filter} : {filter:filter}
|
||||
const baseUrl = $getpath() + `${query.value.url}`;
|
||||
apiUrl.value = baseUrl;
|
||||
let vparams = !$empty(values) ? { values: values } : null;
|
||||
if (!$empty(filter)) {
|
||||
vparams = vparams ? { values: values, filter: filter } : { filter: filter };
|
||||
}
|
||||
if(!$empty(sort)) {
|
||||
if(vparams) {
|
||||
vparams.sort = sort.trim()
|
||||
if (!$empty(sort)) {
|
||||
if (vparams) {
|
||||
vparams.sort = sort.trim();
|
||||
} else {
|
||||
vparams = {sort: sort.trim()}
|
||||
vparams = { sort: sort.trim() };
|
||||
}
|
||||
}
|
||||
if(vparams) {
|
||||
if (vparams) {
|
||||
let url = new URL(baseUrl);
|
||||
let searchParams = new URLSearchParams(vparams);
|
||||
url.search = searchParams.toString();
|
||||
apiUrl.value = baseUrl + url.search
|
||||
}
|
||||
apiUrl.value = baseUrl + url.search;
|
||||
}
|
||||
}
|
||||
function showData() {
|
||||
let arr = []
|
||||
if(!$empty(values)) {
|
||||
let arr1 = values.trim().split(',')
|
||||
arr1.map(v=>{
|
||||
let val = v.trim()
|
||||
let field = $createField(val, val, 'string', true)
|
||||
arr.push(field)
|
||||
})
|
||||
let arr = [];
|
||||
if (!$empty(values)) {
|
||||
let arr1 = values.trim().split(",");
|
||||
arr1.map((v) => {
|
||||
let val = v.trim();
|
||||
let field = $createField(val, val, "string", true);
|
||||
arr.push(field);
|
||||
});
|
||||
} else {
|
||||
current.value.fields.map(v=>{
|
||||
let field = $createField(v.name, v.name, 'string', true)
|
||||
arr.push(field)
|
||||
})
|
||||
current.value.fields.map((v) => {
|
||||
let field = $createField(v.name, v.name, "string", true);
|
||||
arr.push(field);
|
||||
});
|
||||
}
|
||||
let clone = $clone(pagedata.value)
|
||||
clone.fields = arr
|
||||
clone.data = datatable.value
|
||||
pagedata.value = undefined
|
||||
setTimeout(()=>pagedata.value = clone)
|
||||
let clone = $clone(pagedata.value);
|
||||
clone.fields = arr;
|
||||
clone.data = datatable.value;
|
||||
pagedata.value = undefined;
|
||||
setTimeout(() => (pagedata.value = clone));
|
||||
}
|
||||
function copy() {
|
||||
$copyToClipboard(JSON.stringify(query.value))
|
||||
$copyToClipboard(JSON.stringify(query.value));
|
||||
}
|
||||
function openModel(x) {
|
||||
showmodal.value = {component: 'datatable/ModelInfo', title: x.model, width: '70%', height: '600px',
|
||||
vbind: {data: data, info: $find(data, {model: x.model})}}
|
||||
showmodal.value = {
|
||||
component: "datatable/ModelInfo",
|
||||
title: x.model,
|
||||
width: "70%",
|
||||
height: "600px",
|
||||
vbind: { data: data, info: $find(data, { model: x.model }) },
|
||||
};
|
||||
}
|
||||
function downloadFile() {
|
||||
$download(`${$getpath()}download/?name=datamodel.png&type=file`, 'datamodel.png')
|
||||
$download(`${$getpath()}download/?name=datamodel.png&type=file`, "datamodel.png");
|
||||
}
|
||||
function findModel() {
|
||||
if($empty(text)) return displayData.value = data
|
||||
displayData.value = data.filter(v=>v.model.toLowerCase().indexOf(text.toLowerCase())>=0)
|
||||
if ($empty(text)) return (displayData.value = data);
|
||||
displayData.value = data.filter((v) => v.model.toLowerCase().indexOf(text.toLowerCase()) >= 0);
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,57 +1,104 @@
|
||||
<template>
|
||||
<div class="field is-grouped is-grouped-multiline pl-2" v-if="filters? filters.length>0 : false">
|
||||
<div class="control mr-5">
|
||||
<a class="button is-primary is-small has-text-white has-text-weight-bold" @click="updateData({filters: []})">
|
||||
<span class="fs-14">Xóa lọc</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control pr-2 mr-5">
|
||||
<span class="icon-text">
|
||||
<SvgIcon v-bind="{name: 'sigma.svg', type: 'primary', size: 20}"></SvgIcon>
|
||||
<span class="fsb-18 has-text-primary">{{totalRows}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control" v-for="(v,i) in filters" :key="i">
|
||||
<div class="tags has-addons is-marginless">
|
||||
<a class="tag is-primary has-text-white is-marginless" @click="showCondition(v)">{{v.label.indexOf('>')>=0? $stripHtml(v.label,30) : v.label}}</a>
|
||||
<a class="tag is-delete is-marginless has-text-black-bis" @click="removeFilter(i)"></a>
|
||||
</div>
|
||||
<span class="help has-text-black-bis">
|
||||
{{v.sort? v.sort : (v.select? ('[' + (v.select.length>0? $stripHtml(v.select[0],20) : '') + '...Σ' + v.select.length + ']') :
|
||||
(v.condition))}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="field is-grouped is-grouped-multiline pl-2"
|
||||
v-if="filters ? filters.length > 0 : false"
|
||||
>
|
||||
<div class="control mr-5">
|
||||
<a
|
||||
class="button is-primary is-small has-text-white has-text-weight-bold"
|
||||
@click="updateData({ filters: [] })"
|
||||
>
|
||||
<span class="fs-14">Xóa lọc</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control pr-2 mr-5">
|
||||
<span class="icon-text">
|
||||
<SvgIcon v-bind="{ name: 'sigma.svg', type: 'primary', size: 20 }"></SvgIcon>
|
||||
<span class="fsb-18 has-text-primary">{{ totalRows }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="control"
|
||||
v-for="(v, i) in filters"
|
||||
:key="i"
|
||||
>
|
||||
<div class="tags has-addons is-marginless">
|
||||
<a
|
||||
class="tag is-primary has-text-white is-marginless"
|
||||
@click="showCondition(v)"
|
||||
>{{ v.label.indexOf(">") >= 0 ? $stripHtml(v.label, 30) : v.label }}</a
|
||||
>
|
||||
<a
|
||||
class="tag is-delete is-marginless has-text-black-bis"
|
||||
@click="removeFilter(i)"
|
||||
></a>
|
||||
</div>
|
||||
<div class="table-container mb-0" ref="container" id="docid">
|
||||
<table class="table is-fullwidth is-bordered is-narrow is-hoverable" :style="tableStyle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(field,i) in displayFields" :key="i" :style="field.headerStyle">
|
||||
<div @click="showField(field)" :style="field.dropStyle">
|
||||
<a v-if="field.label.indexOf('<')<0">{{field.label}}</a>
|
||||
<a v-else>
|
||||
<component :is="dynamicComponent(field.label)" :row="v" @clickevent="clickEvent($event, v, field)" />
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(v,i) in displayData" :key="i">
|
||||
<td
|
||||
v-for="(field, j) in displayFields"
|
||||
:key="j"
|
||||
:id="field.name"
|
||||
:style="v[`${field.name}color`]"
|
||||
style="
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
@dblclick="doubleClick(field, v)">
|
||||
<component :is="dynamicComponent(field.template)" :row="v" v-if="field.template" @clickevent="clickEvent($event, v, field)" />
|
||||
<span v-else>{{ v[field.name] }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<span class="help has-text-black-bis">
|
||||
{{
|
||||
v.sort
|
||||
? v.sort
|
||||
: v.select
|
||||
? "[" + (v.select.length > 0 ? $stripHtml(v.select[0], 20) : "") + "...Σ" + v.select.length + "]"
|
||||
: v.condition
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="table-container mb-0"
|
||||
ref="container"
|
||||
id="docid"
|
||||
>
|
||||
<table
|
||||
class="table is-fullwidth is-bordered is-narrow is-hoverable"
|
||||
:style="tableStyle"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(field, i) in displayFields"
|
||||
:key="i"
|
||||
:style="field.headerStyle"
|
||||
>
|
||||
<div
|
||||
@click="showField(field)"
|
||||
:style="field.dropStyle"
|
||||
>
|
||||
<a v-if="field.label.indexOf('<') < 0">{{ field.label }}</a>
|
||||
<a v-else>
|
||||
<component
|
||||
:is="dynamicComponent(field.label)"
|
||||
:row="v"
|
||||
@clickevent="clickEvent($event, v, field)"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(v, i) in displayData"
|
||||
:key="i"
|
||||
>
|
||||
<td
|
||||
v-for="(field, j) in displayFields"
|
||||
:key="j"
|
||||
:id="field.name"
|
||||
:style="v[`${field.name}color`]"
|
||||
style="overflow: hidden; text-overflow: ellipsis"
|
||||
@dblclick="doubleClick(field, v)"
|
||||
>
|
||||
<component
|
||||
:is="dynamicComponent(field.template)"
|
||||
:row="v"
|
||||
v-if="field.template"
|
||||
@clickevent="clickEvent($event, v, field)"
|
||||
/>
|
||||
<span v-else>{{ v[field.name] }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<DatatablePagination
|
||||
v-bind="{ data: data, perPage: perPage }"
|
||||
@@ -124,7 +171,7 @@ watch(
|
||||
() => store[props.pagename],
|
||||
(newVal, oldVal) => {
|
||||
updateChange();
|
||||
}
|
||||
},
|
||||
);
|
||||
function updateChange() {
|
||||
pagedata = store[props.pagename];
|
||||
@@ -141,24 +188,21 @@ function updateChange() {
|
||||
const updateShow = function (full_data) {
|
||||
// allowed JS expressions - should return a boolean
|
||||
const allowedFns = {
|
||||
'$getEditRights()': $getEditRights,
|
||||
"$getEditRights()": $getEditRights,
|
||||
};
|
||||
const arr = pagedata.fields.filter(({ show }) => {
|
||||
if (typeof show === 'boolean') return show;
|
||||
const arr = pagedata.fields.filter(({ show }) => {
|
||||
if (typeof show === "boolean") return show;
|
||||
else {
|
||||
// show is a string
|
||||
if (show === 'true') return true;
|
||||
if (show === 'false') return false;
|
||||
if (show === "true") return true;
|
||||
if (show === "false") return false;
|
||||
return allowedFns[show]?.() || false;
|
||||
}
|
||||
});
|
||||
if (full_data === false) displayData = $copy(data);
|
||||
else
|
||||
displayData = $copy(
|
||||
data.filter(
|
||||
(ele, index) =>
|
||||
index >= (currentPage - 1) * perPage && index < currentPage * perPage
|
||||
)
|
||||
data.filter((ele, index) => index >= (currentPage - 1) * perPage && index < currentPage * perPage),
|
||||
);
|
||||
displayData.map((v) => {
|
||||
arr.map((x) => (v[`${x.name}color`] = getStyle(x, v)));
|
||||
@@ -222,9 +266,7 @@ const getStyle = function (field, record) {
|
||||
field.bgcolor.map((v) => {
|
||||
if (v.type === "search") {
|
||||
if (
|
||||
record[field.name] && !stop
|
||||
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
|
||||
: false
|
||||
record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
|
||||
) {
|
||||
val += ` background-color:${v.color}; `;
|
||||
stop = true;
|
||||
@@ -245,9 +287,7 @@ const getStyle = function (field, record) {
|
||||
field.color.map((v) => {
|
||||
if (v.type === "search") {
|
||||
if (
|
||||
record[field.name] && !stop
|
||||
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
|
||||
: false
|
||||
record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
|
||||
) {
|
||||
val += ` color:${v.color}; `;
|
||||
stop = true;
|
||||
@@ -268,9 +308,7 @@ const getStyle = function (field, record) {
|
||||
field.textsize.map((v) => {
|
||||
if (v.type === "search") {
|
||||
if (
|
||||
record[field.name] && !stop
|
||||
? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0
|
||||
: false
|
||||
record[field.name] && !stop ? record[field.name].toLowerCase().indexOf(v.keyword.toLowerCase()) >= 0 : false
|
||||
) {
|
||||
val += ` font-size:${v.size}px; `;
|
||||
stop = true;
|
||||
@@ -283,10 +321,7 @@ const getStyle = function (field, record) {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else
|
||||
val += ` font-size:${
|
||||
tablesetting.find((v) => v.code === "table-font-size").detail
|
||||
}px;`;
|
||||
} else val += ` font-size:${tablesetting.find((v) => v.code === "table-font-size").detail}px;`;
|
||||
if (field.textalign) val += ` text-align:${field.textalign}; `;
|
||||
if (field.minwidth) val += ` min-width:${field.minwidth}px; `;
|
||||
if (field.maxwidth) val += ` max-width:${field.maxwidth}px; `;
|
||||
@@ -295,56 +330,28 @@ const getStyle = function (field, record) {
|
||||
const getSettingStyle = function (name, field) {
|
||||
let value = "";
|
||||
if (name === "container") {
|
||||
value =
|
||||
"min-height:" +
|
||||
tablesetting.find((v) => v.code === "container-height").detail +
|
||||
"rem; ";
|
||||
value = "min-height:" + tablesetting.find((v) => v.code === "container-height").detail + "rem; ";
|
||||
} else if (name === "table") {
|
||||
value +=
|
||||
"background-color:" +
|
||||
tablesetting.find((v) => v.code === "table-background").detail +
|
||||
"; ";
|
||||
value +=
|
||||
"font-size:" +
|
||||
tablesetting.find((v) => v.code === "table-font-size").detail +
|
||||
"px;";
|
||||
value +=
|
||||
"color:" + tablesetting.find((v) => v.code === "table-font-color").detail + "; ";
|
||||
value += "background-color:" + tablesetting.find((v) => v.code === "table-background").detail + "; ";
|
||||
value += "font-size:" + tablesetting.find((v) => v.code === "table-font-size").detail + "px;";
|
||||
value += "color:" + tablesetting.find((v) => v.code === "table-font-color").detail + "; ";
|
||||
} else if (name === "header") {
|
||||
value +=
|
||||
"background-color:" +
|
||||
tablesetting.find((v) => v.code === "header-background").detail +
|
||||
"; ";
|
||||
value += "background-color:" + tablesetting.find((v) => v.code === "header-background").detail + "; ";
|
||||
if (field.minwidth) value += " min-width: " + field.minwidth + "px; ";
|
||||
if (field.maxwidth) value += " max-width: " + field.maxwidth + "px; ";
|
||||
} else if (name === "menu") {
|
||||
let arg = tablesetting.find((v) => v.code === "menu-width").detail;
|
||||
arg = field ? (field.menuwidth ? field.menuwidth : arg) : arg;
|
||||
value += "width:" + arg + "rem; ";
|
||||
value +=
|
||||
"min-height:" +
|
||||
tablesetting.find((v) => v.code === "menu-min-height").detail +
|
||||
"rem; ";
|
||||
value +=
|
||||
"max-height:" +
|
||||
tablesetting.find((v) => v.code === "menu-max-height").detail +
|
||||
"rem; ";
|
||||
value += "min-height:" + tablesetting.find((v) => v.code === "menu-min-height").detail + "rem; ";
|
||||
value += "max-height:" + tablesetting.find((v) => v.code === "menu-max-height").detail + "rem; ";
|
||||
value += "overflow:auto; ";
|
||||
} else if (name === "dropdown") {
|
||||
value +=
|
||||
"font-size:" +
|
||||
tablesetting.find((v) => v.code === "header-font-size").detail +
|
||||
"px; ";
|
||||
value += "font-size:" + tablesetting.find((v) => v.code === "header-font-size").detail + "px; ";
|
||||
let found = filters.find((v) => v.name === field.name);
|
||||
found
|
||||
? (value +=
|
||||
"color:" +
|
||||
tablesetting.find((v) => v.code === "header-filter-color").detail +
|
||||
"; ")
|
||||
: (value +=
|
||||
"color:" +
|
||||
tablesetting.find((v) => v.code === "header-font-color").detail +
|
||||
"; ");
|
||||
? (value += "color:" + tablesetting.find((v) => v.code === "header-filter-color").detail + "; ")
|
||||
: (value += "color:" + tablesetting.find((v) => v.code === "header-font-color").detail + "; ");
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -369,12 +376,8 @@ const frontendFilter = function (newVal) {
|
||||
else {
|
||||
let text = "";
|
||||
filter.map((y, k) => {
|
||||
text += `${
|
||||
k > 0 ? (filter[k - 1].operator === "and" ? " &&" : " ||") : ""
|
||||
} ${$formatNumber(x[name])}
|
||||
${
|
||||
y.condition === "=" ? "==" : y.condition === "<>" ? "!==" : y.condition
|
||||
} ${$formatNumber(y.value)}`;
|
||||
text += `${k > 0 ? (filter[k - 1].operator === "and" ? " &&" : " ||") : ""} ${$formatNumber(x[name])}
|
||||
${y.condition === "=" ? "==" : y.condition === "<>" ? "!==" : y.condition} ${$formatNumber(y.value)}`;
|
||||
});
|
||||
return $calc(text);
|
||||
}
|
||||
@@ -385,11 +388,7 @@ const frontendFilter = function (newVal) {
|
||||
.filter((m) => m.select || m.filter)
|
||||
.map((v) => {
|
||||
if (v.select) {
|
||||
data = data.filter(
|
||||
(x) =>
|
||||
v.select.findIndex((y) => ($empty(y) ? $empty(x[v.name]) : y === x[v.name])) >
|
||||
-1
|
||||
);
|
||||
data = data.filter((x) => v.select.findIndex((y) => ($empty(y) ? $empty(x[v.name]) : y === x[v.name])) > -1);
|
||||
} else if (v.filter) {
|
||||
data = data.filter((x) => checkValid(v.name, x, v.filter));
|
||||
}
|
||||
@@ -503,9 +502,7 @@ const updateData = async function (newVal) {
|
||||
}
|
||||
tablesetting = $copy(pagedata.tablesetting || gridsetting);
|
||||
if (tablesetting) {
|
||||
perPage = pagedata.perPage
|
||||
? pagedata.perPage
|
||||
: Number(tablesetting.find((v) => v.code === "per-page").detail);
|
||||
perPage = pagedata.perPage ? pagedata.perPage : Number(tablesetting.find((v) => v.code === "per-page").detail);
|
||||
}
|
||||
if (newVal.fields) {
|
||||
fields = $copy(newVal.fields);
|
||||
|
||||
@@ -1,20 +1,46 @@
|
||||
<template>
|
||||
<TimeOption
|
||||
v-bind="{ pagename: vpagename, api: api, timeopt: timeopt, filter: optfilter, importdata: props.importdata, newDataAvailable: newDataAvailable, params: vparams }"
|
||||
ref="timeopt" @option="timeOption" @excel="exportExcel" @add="insert" @manual-refresh="manualRefresh" @refresh-data="refreshData"
|
||||
@import="openImportModal" class="mb-3" v-if="timeopt"></TimeOption>
|
||||
<DataTable v-bind="{ pagename: vpagename }" @edit="edit" @insert="insert" @dataevent="dataEvent" v-if="pagedata" />
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal" />
|
||||
v-bind="{
|
||||
pagename: vpagename,
|
||||
api: api,
|
||||
timeopt: timeopt,
|
||||
filter: optfilter,
|
||||
importdata: props.importdata,
|
||||
newDataAvailable: newDataAvailable,
|
||||
params: vparams,
|
||||
}"
|
||||
ref="timeopt"
|
||||
@option="timeOption"
|
||||
@excel="exportExcel"
|
||||
@add="insert"
|
||||
@manual-refresh="manualRefresh"
|
||||
@refresh-data="refreshData"
|
||||
@import="openImportModal"
|
||||
class="mb-3"
|
||||
v-if="timeopt"
|
||||
></TimeOption>
|
||||
<DataTable
|
||||
v-bind="{ pagename: vpagename }"
|
||||
@edit="edit"
|
||||
@insert="insert"
|
||||
@dataevent="dataEvent"
|
||||
v-if="pagedata"
|
||||
/>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TimeOption from '~/components/datatable/TimeOption'
|
||||
import { useStore } from '~/stores/index'
|
||||
import TimeOption from "~/components/datatable/TimeOption";
|
||||
import { useStore } from "~/stores/index";
|
||||
// [FIX] Thêm onActivated, onDeactivated để xử lý KeepAlive
|
||||
import { ref, watch, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
|
||||
import { ref, watch, onBeforeUnmount, onActivated, onDeactivated } from "vue";
|
||||
|
||||
const emit = defineEmits(['modalevent', 'dataevent', 'dataUpdated'])
|
||||
const store = useStore()
|
||||
const emit = defineEmits(["modalevent", "dataevent", "dataUpdated"]);
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps({
|
||||
pagename: String,
|
||||
@@ -26,43 +52,46 @@ const props = defineProps({
|
||||
modal: Object,
|
||||
timeopt: Object,
|
||||
realtime: Object,
|
||||
importdata: Object
|
||||
})
|
||||
importdata: Object,
|
||||
});
|
||||
|
||||
const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp()
|
||||
const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp();
|
||||
|
||||
const showmodal = ref()
|
||||
const pagedata = ref()
|
||||
const newDataAvailable = ref(false)
|
||||
const pendingNewData = ref(null)
|
||||
const lastDataHash = ref(null)
|
||||
const pollingInterval = ref(null)
|
||||
const showmodal = ref();
|
||||
const pagedata = ref();
|
||||
const newDataAvailable = ref(false);
|
||||
const pendingNewData = ref(null);
|
||||
const lastDataHash = ref(null);
|
||||
const pollingInterval = ref(null);
|
||||
|
||||
let vpagename = props.pagename
|
||||
let vfilter = props.filter ? $copy(props.filter) : undefined
|
||||
let vparams = props.params ? $copy(props.params) : undefined
|
||||
let connection = undefined
|
||||
let optfilter = props.filter || (props.params ? props.params.filter : undefined)
|
||||
let vpagename = props.pagename;
|
||||
let vfilter = props.filter ? $copy(props.filter) : undefined;
|
||||
let vparams = props.params ? $copy(props.params) : undefined;
|
||||
let connection = undefined;
|
||||
let optfilter = props.filter || (props.params ? props.params.filter : undefined);
|
||||
|
||||
const realtimeConfig = ref({ time: 0, update: "true" })
|
||||
const realtimeConfig = ref({ time: 0, update: "true" });
|
||||
|
||||
if (props.realtime) {
|
||||
realtimeConfig.value = { time: props.realtime.time || 0, update: props.realtime.update }
|
||||
realtimeConfig.value = {
|
||||
time: props.realtime.time || 0,
|
||||
update: props.realtime.update,
|
||||
};
|
||||
}
|
||||
|
||||
if (vparams?.filter) {
|
||||
for (const [key, value] of Object.entries(vparams.filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
vparams.filter[key] = store[value.replace('$', '')].id
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
vparams.filter[key] = store[value.replace("$", "")].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateDataHash = (data) => {
|
||||
if (!data) return null
|
||||
if (!data) return null;
|
||||
try {
|
||||
const replacer = (key, value) =>
|
||||
value && typeof value === 'object' && !Array.isArray(value)
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? Object.keys(value)
|
||||
.sort()
|
||||
.reduce((sorted, key) => {
|
||||
@@ -73,341 +102,356 @@ const generateDataHash = (data) => {
|
||||
|
||||
const stringToHash = JSON.stringify(data, replacer);
|
||||
|
||||
return stringToHash.split('').reduce((a, b) => {
|
||||
a = ((a << 5) - a) + b.charCodeAt(0)
|
||||
return a & a
|
||||
}, 0)
|
||||
return stringToHash.split("").reduce((a, b) => {
|
||||
a = (a << 5) - a + b.charCodeAt(0);
|
||||
return a & a;
|
||||
}, 0);
|
||||
} catch (e) {
|
||||
console.error('Error generating data hash:', e);
|
||||
return null
|
||||
console.error("Error generating data hash:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// [FIX] Tách hàm dừng polling ra riêng để tái sử dụng
|
||||
const stopAutoCheck = () => {
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value)
|
||||
pollingInterval.value = null
|
||||
clearInterval(pollingInterval.value);
|
||||
pollingInterval.value = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startAutoCheck = () => {
|
||||
// [FIX] Dừng interval cũ trước khi tạo mới, tránh tạo nhiều interval chồng nhau
|
||||
stopAutoCheck()
|
||||
stopAutoCheck();
|
||||
if (realtimeConfig.value.time && realtimeConfig.value.time > 0) {
|
||||
pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000)
|
||||
pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkDataChanges = async () => {
|
||||
try {
|
||||
const connlist = []
|
||||
const conn1 = $findapi(props.api)
|
||||
const connlist = [];
|
||||
const conn1 = $findapi(props.api);
|
||||
|
||||
if (vfilter) {
|
||||
const filter = $copy(conn1.params.filter) || {}
|
||||
const filter = $copy(conn1.params.filter) || {};
|
||||
for (const [key, value] of Object.entries(vfilter)) {
|
||||
filter[key] = value
|
||||
filter[key] = value;
|
||||
}
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
filter[key] = store[value.replace("$", "")].id;
|
||||
}
|
||||
}
|
||||
conn1.params.filter = filter
|
||||
conn1.params.filter = filter;
|
||||
}
|
||||
|
||||
if (vparams) conn1.params = $copy(vparams)
|
||||
if (vparams) conn1.params = $copy(vparams);
|
||||
|
||||
delete conn1.params.sort
|
||||
delete conn1.params.values
|
||||
delete conn1.params.sort;
|
||||
delete conn1.params.values;
|
||||
|
||||
conn1.params.summary = 'aggregate'
|
||||
conn1.params.summary = "aggregate";
|
||||
conn1.params.distinct_values = JSON.stringify({
|
||||
total_count: { type: 'Count', field: 'id' },
|
||||
last_updated: { type: 'Max', field: 'update_time' },
|
||||
last_created: { type: 'Max', field: 'create_time' }
|
||||
})
|
||||
total_count: { type: "Count", field: "id" },
|
||||
last_updated: { type: "Max", field: "update_time" },
|
||||
last_created: { type: "Max", field: "create_time" },
|
||||
});
|
||||
|
||||
connlist.push(conn1)
|
||||
connlist.push(conn1);
|
||||
|
||||
const rs = await $getapi(connlist)
|
||||
const obj = $find(rs, { name: props.api })
|
||||
const newMetadata = obj ? obj.data.rows : {}
|
||||
const newHash = generateDataHash(newMetadata)
|
||||
const rs = await $getapi(connlist);
|
||||
const obj = $find(rs, { name: props.api });
|
||||
const newMetadata = obj ? obj.data.rows : {};
|
||||
const newHash = generateDataHash(newMetadata);
|
||||
|
||||
if (lastDataHash.value === null) {
|
||||
lastDataHash.value = newHash
|
||||
return
|
||||
lastDataHash.value = newHash;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newHash !== lastDataHash.value) {
|
||||
lastDataHash.value = newHash
|
||||
lastDataHash.value = newHash;
|
||||
|
||||
if (realtimeConfig.value.update === "true") {
|
||||
await loadFullDataAsync()
|
||||
emit('dataUpdated', { newData: store[vpagename].data, autoUpdate: true, hasChanges: true })
|
||||
await loadFullDataAsync();
|
||||
emit("dataUpdated", {
|
||||
newData: store[vpagename].data,
|
||||
autoUpdate: true,
|
||||
hasChanges: true,
|
||||
});
|
||||
} else {
|
||||
newDataAvailable.value = true
|
||||
emit('dataUpdated', { newData: null, autoUpdate: false, hasChanges: true })
|
||||
newDataAvailable.value = true;
|
||||
emit("dataUpdated", {
|
||||
newData: null,
|
||||
autoUpdate: false,
|
||||
hasChanges: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking data:', error)
|
||||
console.error("Error checking data:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadFullDataAsync = async () => {
|
||||
try {
|
||||
const connlist = []
|
||||
const conn1 = $findapi(props.api)
|
||||
const connlist = [];
|
||||
const conn1 = $findapi(props.api);
|
||||
|
||||
if (vfilter) {
|
||||
const filter = $copy(conn1.params.filter) || {}
|
||||
const filter = $copy(conn1.params.filter) || {};
|
||||
for (const [key, value] of Object.entries(vfilter)) {
|
||||
filter[key] = value
|
||||
filter[key] = value;
|
||||
}
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
filter[key] = store[value.replace("$", "")].id;
|
||||
}
|
||||
}
|
||||
conn1.params.filter = filter
|
||||
conn1.params.filter = filter;
|
||||
}
|
||||
|
||||
if (vparams) conn1.params = $copy(vparams)
|
||||
if (vparams) conn1.params = $copy(vparams);
|
||||
|
||||
delete conn1.params.summary
|
||||
delete conn1.params.distinct_values
|
||||
delete conn1.params.summary;
|
||||
delete conn1.params.distinct_values;
|
||||
|
||||
connlist.push(conn1)
|
||||
connlist.push(conn1);
|
||||
|
||||
const rs = await $getapi(connlist)
|
||||
const obj = $find(rs, { name: props.api })
|
||||
const newData = obj ? $copy(obj.data.rows) : []
|
||||
const rs = await $getapi(connlist);
|
||||
const obj = $find(rs, { name: props.api });
|
||||
const newData = obj ? $copy(obj.data.rows) : [];
|
||||
|
||||
updateDataDisplay(newData)
|
||||
updateDataDisplay(newData);
|
||||
} catch (error) {
|
||||
console.error('Error loading full data:', error)
|
||||
console.error("Error loading full data:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openImportModal = () => {
|
||||
const copy = $copy(props.importdata)
|
||||
showmodal.value = copy
|
||||
}
|
||||
const copy = $copy(props.importdata);
|
||||
showmodal.value = copy;
|
||||
};
|
||||
|
||||
const updateDataDisplay = (newData) => {
|
||||
const copy = $clone(store[vpagename])
|
||||
copy.data = newData
|
||||
copy.update = { data: newData }
|
||||
store.commit(vpagename, copy)
|
||||
newDataAvailable.value = false
|
||||
pendingNewData.value = null
|
||||
}
|
||||
const copy = $clone(store[vpagename]);
|
||||
copy.data = newData;
|
||||
copy.update = { data: newData };
|
||||
store.commit(vpagename, copy);
|
||||
newDataAvailable.value = false;
|
||||
pendingNewData.value = null;
|
||||
};
|
||||
|
||||
const manualRefresh = () => {
|
||||
if (pendingNewData.value) {
|
||||
updateDataDisplay(pendingNewData.value)
|
||||
updateDataDisplay(pendingNewData.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
stopAutoCheck()
|
||||
stopAutoCheck();
|
||||
newDataAvailable.value = false;
|
||||
pendingNewData.value = null;
|
||||
|
||||
|
||||
await getApi();
|
||||
|
||||
|
||||
lastDataHash.value = null;
|
||||
await checkDataChanges();
|
||||
|
||||
|
||||
newDataAvailable.value = false;
|
||||
startAutoCheck();
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.realtime, (newVal) => {
|
||||
if (newVal) {
|
||||
realtimeConfig.value.time = newVal.time || 0
|
||||
realtimeConfig.value.update = newVal.update === true
|
||||
startAutoCheck()
|
||||
}
|
||||
}, { deep: true })
|
||||
watch(
|
||||
() => props.realtime,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
realtimeConfig.value.time = newVal.time || 0;
|
||||
realtimeConfig.value.update = newVal.update === true;
|
||||
startAutoCheck();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
onDeactivated(() => {
|
||||
stopAutoCheck()
|
||||
})
|
||||
stopAutoCheck();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
startAutoCheck()
|
||||
})
|
||||
startAutoCheck();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAutoCheck()
|
||||
})
|
||||
stopAutoCheck();
|
||||
});
|
||||
|
||||
const timeOption = (v) => {
|
||||
if (!v) return getApi()
|
||||
if (!v) return getApi();
|
||||
|
||||
if (v.filter_or) {
|
||||
if (vfilter) vfilter = undefined
|
||||
if (vfilter) vfilter = undefined;
|
||||
if (vparams) {
|
||||
vparams.filter_or = v.filter_or
|
||||
vparams.filter_or = v.filter_or;
|
||||
} else {
|
||||
const found = $copy($findapi(props.api))
|
||||
found.params.filter_or = v.filter_or
|
||||
const found = $copy($findapi(props.api));
|
||||
found.params.filter_or = v.filter_or;
|
||||
if (props.filter) {
|
||||
const filter = $copy(props.filter)
|
||||
const filter = $copy(props.filter);
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
filter[key] = store[value.replace("$", "")].id;
|
||||
}
|
||||
}
|
||||
found.params.filter = filter
|
||||
found.params.filter = filter;
|
||||
}
|
||||
vparams = found.params
|
||||
vparams = found.params;
|
||||
}
|
||||
return getApi()
|
||||
return getApi();
|
||||
}
|
||||
|
||||
let filter = vfilter ? vfilter : (props.params ? props.params.filter || {} : {})
|
||||
let filter = vfilter ? vfilter : props.params ? props.params.filter || {} : {};
|
||||
for (const [key, value] of Object.entries(v.filter)) {
|
||||
filter[key] = value
|
||||
filter[key] = value;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
filter[key] = store[value.replace("$", "")].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (vfilter) {
|
||||
vfilter = filter
|
||||
vparams = undefined
|
||||
vfilter = filter;
|
||||
vparams = undefined;
|
||||
} else if (vparams) {
|
||||
vparams.filter = filter
|
||||
vparams.filter_or = undefined
|
||||
vparams.filter = filter;
|
||||
vparams.filter_or = undefined;
|
||||
}
|
||||
|
||||
if (!vfilter && !vparams) vfilter = filter
|
||||
getApi()
|
||||
}
|
||||
if (!vfilter && !vparams) vfilter = filter;
|
||||
getApi();
|
||||
};
|
||||
|
||||
const edit = (v) => {
|
||||
const copy = props.modal ? $copy(props.modal) : {}
|
||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v }
|
||||
f.pagename = vpagename
|
||||
f.row = v
|
||||
copy.vbind = f
|
||||
showmodal.value = copy
|
||||
}
|
||||
const copy = props.modal ? $copy(props.modal) : {};
|
||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v };
|
||||
f.pagename = vpagename;
|
||||
f.row = v;
|
||||
copy.vbind = f;
|
||||
showmodal.value = copy;
|
||||
};
|
||||
|
||||
const insert = () => {
|
||||
const copy = props.modal ? $copy(props.modal) : {}
|
||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api }
|
||||
f.pagename = vpagename
|
||||
copy.vbind = f
|
||||
showmodal.value = copy
|
||||
}
|
||||
const copy = props.modal ? $copy(props.modal) : {};
|
||||
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api };
|
||||
f.pagename = vpagename;
|
||||
copy.vbind = f;
|
||||
showmodal.value = copy;
|
||||
};
|
||||
|
||||
const getApi = async () => {
|
||||
const connlist = []
|
||||
let row = props.setting?.id ? $copy(props.setting) : undefined
|
||||
const connlist = [];
|
||||
let row = props.setting?.id ? $copy(props.setting) : undefined;
|
||||
|
||||
if (!row) {
|
||||
const found = $find(store.settings.filter(v => v), props.setting > 0 ? { id: props.setting } : { name: props.setting })
|
||||
if (found) row = $copy(found)
|
||||
const found = $find(
|
||||
store.settings.filter((v) => v),
|
||||
props.setting > 0 ? { id: props.setting } : { name: props.setting },
|
||||
);
|
||||
if (found) row = $copy(found);
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
const conn = $findapi('usersetting')
|
||||
conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting }
|
||||
connlist.push(conn)
|
||||
const conn = $findapi("usersetting");
|
||||
conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting };
|
||||
connlist.push(conn);
|
||||
}
|
||||
|
||||
let data = props.data ? $copy(props.data) : undefined
|
||||
let data = props.data ? $copy(props.data) : undefined;
|
||||
|
||||
if (!data) {
|
||||
const conn1 = $findapi(props.api)
|
||||
const conn1 = $findapi(props.api);
|
||||
if (vfilter) {
|
||||
const filter = conn1.params.filter || {}
|
||||
const filter = conn1.params.filter || {};
|
||||
for (const [key, value] of Object.entries(vfilter)) {
|
||||
filter[key] = value
|
||||
filter[key] = value;
|
||||
}
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf('$') >= 0) {
|
||||
filter[key] = store[value.replace('$', '')].id
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
filter[key] = store[value.replace("$", "")].id;
|
||||
}
|
||||
}
|
||||
conn1.params.filter = filter
|
||||
conn1.params.filter = filter;
|
||||
}
|
||||
if (vparams) conn1.params = vparams
|
||||
connection = conn1
|
||||
connlist.push(conn1)
|
||||
if (vparams) conn1.params = vparams;
|
||||
connection = conn1;
|
||||
connlist.push(conn1);
|
||||
}
|
||||
|
||||
let obj = undefined
|
||||
let obj = undefined;
|
||||
if (connlist.length > 0) {
|
||||
const rs = await $getapi(connlist)
|
||||
const ele = $find(rs, { name: 'usersetting' })
|
||||
const rs = await $getapi(connlist);
|
||||
const ele = $find(rs, { name: "usersetting" });
|
||||
if (ele) {
|
||||
row = $find(ele.data.rows, { name: props.setting.name || props.setting })
|
||||
const copy = $copy(store.settings)
|
||||
copy.push(row)
|
||||
store.commit('settings', copy)
|
||||
row = $find(ele.data.rows, { name: props.setting.name || props.setting });
|
||||
const copy = $copy(store.settings);
|
||||
copy.push(row);
|
||||
store.commit("settings", copy);
|
||||
}
|
||||
obj = $find(rs, { name: props.api })
|
||||
if (obj) data = $copy(obj.data.rows)
|
||||
obj = $find(rs, { name: props.api });
|
||||
if (obj) data = $copy(obj.data.rows);
|
||||
}
|
||||
|
||||
pagedata.value = $setpage(vpagename, row, obj)
|
||||
const copy = $clone(pagedata.value)
|
||||
copy.data = data
|
||||
copy.update = { data: data }
|
||||
store.commit(vpagename, copy)
|
||||
}
|
||||
pagedata.value = $setpage(vpagename, row, obj);
|
||||
const copy = $clone(pagedata.value);
|
||||
copy.data = data;
|
||||
copy.update = { data: data };
|
||||
store.commit(vpagename, copy);
|
||||
};
|
||||
|
||||
const dataEvent = (v, field, data) => {
|
||||
if (data?.modal) {
|
||||
const copy = $copy(data.modal)
|
||||
const f = copy.vbind ? copy.vbind : {}
|
||||
if (!f.api) f.api = props.api
|
||||
f.pagename = vpagename
|
||||
if (!f.row) f.row = v
|
||||
copy.vbind = f
|
||||
copy.field = field
|
||||
showmodal.value = copy
|
||||
const copy = $copy(data.modal);
|
||||
const f = copy.vbind ? copy.vbind : {};
|
||||
if (!f.api) f.api = props.api;
|
||||
f.pagename = vpagename;
|
||||
if (!f.row) f.row = v;
|
||||
copy.vbind = f;
|
||||
copy.field = field;
|
||||
showmodal.value = copy;
|
||||
}
|
||||
emit('modalevent', { name: 'dataevent', data: { row: v, field: field } })
|
||||
emit('dataevent', v, field)
|
||||
}
|
||||
emit("modalevent", { name: "dataevent", data: { row: v, field: field } });
|
||||
emit("dataevent", v, field);
|
||||
};
|
||||
|
||||
const exportExcel = async () => {
|
||||
if (!props.api) return
|
||||
if (!props.api) return;
|
||||
|
||||
const found = $findapi('exportcsv')
|
||||
found.params = connection.params
|
||||
const found = $findapi("exportcsv");
|
||||
found.params = connection.params;
|
||||
const fields = pagedata.value.fields
|
||||
.filter(v => (v.show && v.export !== 'no') || v.export === 'yes')
|
||||
.map(x => ({ name: x.name, label: $stripHtml(x.label) }))
|
||||
found.params.fields = JSON.stringify(fields)
|
||||
found.url = connection.url.replace('data/', 'exportcsv/')
|
||||
const rs = await $getapi([found])
|
||||
.filter((v) => (v.show && v.export !== "no") || v.export === "yes")
|
||||
.map((x) => ({ name: x.name, label: $stripHtml(x.label) }));
|
||||
found.params.fields = JSON.stringify(fields);
|
||||
found.url = connection.url.replace("data/", "exportcsv/");
|
||||
const rs = await $getapi([found]);
|
||||
|
||||
if (rs === 'error') {
|
||||
$snackbar('Đã xảy ra lỗi. Vui lòng thử lại.')
|
||||
return
|
||||
if (rs === "error") {
|
||||
$snackbar("Đã xảy ra lỗi. Vui lòng thử lại.");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([rs[0].data]))
|
||||
const link = document.createElement('a')
|
||||
const fileName = `${$dayjs(new Date()).format('YYYYMMDDhhmmss')}-data.csv`
|
||||
link.href = url
|
||||
link.setAttribute('download', fileName)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
const url = window.URL.createObjectURL(new Blob([rs[0].data]));
|
||||
const link = document.createElement("a");
|
||||
const fileName = `${$dayjs(new Date()).format("YYYYMMDDhhmmss")}-data.csv`;
|
||||
link.href = url;
|
||||
link.setAttribute("download", fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
|
||||
if (!props.timeopt) await getApi()
|
||||
startAutoCheck()
|
||||
</script>
|
||||
if (!props.timeopt) await getApi();
|
||||
startAutoCheck();
|
||||
</script>
|
||||
|
||||
@@ -1,82 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="fsb-20 mb-5">Điều chỉnh tiêu đề</p>
|
||||
<div v-for="(v, i) in arr" :key="i" :class="(i>0? 'mt-4' : null)">
|
||||
<p class="fsb-14">Dòng thứ {{(i+1)}}<span class="has-text-danger"> *</span></p>
|
||||
<div class="field has-addons mt-1">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="v.label">
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button px-2 is-primary" @click="add()">
|
||||
<span>
|
||||
<SvgIcon v-bind="{name: 'add1.png', type: 'white', size: 17}"></SvgIcon></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control" @click="remove(i)" v-if="(i>0)">
|
||||
<a class="button px-2 is-dark">
|
||||
<span>
|
||||
<SvgIcon v-bind="{name: 'bin.svg', type: 'white', size: 17}"></SvgIcon>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="v.error"> {{v.error}} </p>
|
||||
</div>
|
||||
<div class="buttons mt-5">
|
||||
<button class="button is-primary has-text-white" @click="update()">Cập nhật</button>
|
||||
<button class="button is-dark" @click="$emit('close')">Hủy bỏ</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(v, i) in arr"
|
||||
:key="i"
|
||||
:class="i > 0 ? 'mt-4' : null"
|
||||
>
|
||||
<p class="fsb-14">Dòng thứ {{ i + 1 }}<span class="has-text-danger"> *</span></p>
|
||||
<div class="field has-addons mt-1">
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="v.label"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a
|
||||
class="button px-2 is-primary"
|
||||
@click="add()"
|
||||
>
|
||||
<span> <SvgIcon v-bind="{ name: 'add1.png', type: 'white', size: 17 }"></SvgIcon></span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="control"
|
||||
@click="remove(i)"
|
||||
v-if="i > 0"
|
||||
>
|
||||
<a class="button px-2 is-dark">
|
||||
<span>
|
||||
<SvgIcon v-bind="{ name: 'bin.svg', type: 'white', size: 17 }"></SvgIcon>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="v.error"
|
||||
>
|
||||
{{ v.error }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="buttons mt-5">
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="update()"
|
||||
>
|
||||
Cập nhật
|
||||
</button>
|
||||
<button
|
||||
class="button is-dark"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
Hủy bỏ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['label'],
|
||||
props: ["label"],
|
||||
data() {
|
||||
return {
|
||||
arr: []
|
||||
}
|
||||
arr: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
let arr1 = this.label.replace('<div>', '').replace('</div>', '').split("</p>")
|
||||
arr1.map(v=>{
|
||||
if(!this.$empty(v)) {
|
||||
let label = v + '</p>'
|
||||
label = this.$stripHtml(label)
|
||||
this.arr.push({label: label})
|
||||
let arr1 = this.label.replace("<div>", "").replace("</div>", "").split("</p>");
|
||||
arr1.map((v) => {
|
||||
if (!this.$empty(v)) {
|
||||
let label = v + "</p>";
|
||||
label = this.$stripHtml(label);
|
||||
this.arr.push({ label: label });
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.arr.push({label: undefined})
|
||||
this.arr.push({ label: undefined });
|
||||
},
|
||||
remove(i) {
|
||||
this.$remove(this.arr, i)
|
||||
this.$remove(this.arr, i);
|
||||
},
|
||||
checkError() {
|
||||
let error = false
|
||||
this.arr.map(v=>{
|
||||
if(this.$empty(v.label)) {
|
||||
v.error = 'Nội dung không được bỏ trống'
|
||||
error = true
|
||||
let error = false;
|
||||
this.arr.map((v) => {
|
||||
if (this.$empty(v.label)) {
|
||||
v.error = "Nội dung không được bỏ trống";
|
||||
error = true;
|
||||
}
|
||||
})
|
||||
if(error) this.arr = this.$copy(this.arr)
|
||||
return error
|
||||
});
|
||||
if (error) this.arr = this.$copy(this.arr);
|
||||
return error;
|
||||
},
|
||||
update() {
|
||||
if(this.checkError()) return
|
||||
let label = ''
|
||||
if(this.arr.length>1) {
|
||||
this.arr.map((v,i)=>{
|
||||
label += `<p${i<this.arr.length-1? ' style="border-bottom: 1px solid white;"' : ''}>${v.label.trim()}</p>`
|
||||
})
|
||||
label = `<div>${label}</div>`
|
||||
} else label = this.arr[0].label.trim()
|
||||
this.$emit('modalevent', {name: 'label', data: label})
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
if (this.checkError()) return;
|
||||
let label = "";
|
||||
if (this.arr.length > 1) {
|
||||
this.arr.map((v, i) => {
|
||||
label += `<p${i < this.arr.length - 1 ? ' style="border-bottom: 1px solid white;"' : ""}>${v.label.trim()}</p>`;
|
||||
});
|
||||
label = `<div>${label}</div>`;
|
||||
} else label = this.arr[0].label.trim();
|
||||
this.$emit("modalevent", { name: "label", data: label });
|
||||
this.$emit("close");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,88 +1,127 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="keys.length>0">
|
||||
<div class="field is-horizontal" v-for="(v,i) in keys" :key="i">
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<input class="input fs-14" type="text" placeholder="" v-model="keys[i]">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input fs-14" type="text" placeholder="" v-model="values[i]">
|
||||
<template v-if="keys.length > 0">
|
||||
<div
|
||||
class="field is-horizontal"
|
||||
v-for="(v, i) in keys"
|
||||
:key="i"
|
||||
>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<input
|
||||
class="input fs-14"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="keys[i]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input
|
||||
class="input fs-14"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="values[i]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<p class="control">
|
||||
<a @click="addAttr()">
|
||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
class="ml-2"
|
||||
@click="remove(i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
class="ml-2"
|
||||
@click="jsonData(v, i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'apps.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<p class="control">
|
||||
<a @click="addAttr()">
|
||||
<SvgIcon v-bind="{name: 'add1.png', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a class="ml-2" @click="remove(i)">
|
||||
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a class="ml-2" @click="jsonData(v, i)">
|
||||
<SvgIcon v-bind="{name: 'apps.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mb-6" v-else>
|
||||
<button class="button is-primary has-text-white" @click="addAttr()">Thêm thuộc tính</button>
|
||||
<div
|
||||
class="mb-6"
|
||||
v-else
|
||||
>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="addAttr()"
|
||||
>
|
||||
Thêm thuộc tính
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons mt-5">
|
||||
<a class="button is-primary has-text-white" @click="update()">Cập nhật</a>
|
||||
<a
|
||||
class="button is-primary has-text-white"
|
||||
@click="update()"
|
||||
>Cập nhật</a
|
||||
>
|
||||
</div>
|
||||
<Modal @close="comp=undefined" @update="doUpdate"
|
||||
v-bind="{component: comp, width: '40%', height: '300px', vbind: vbind}" v-if="comp"></Modal>
|
||||
<Modal
|
||||
@close="comp = undefined"
|
||||
@update="doUpdate"
|
||||
v-bind="{ component: comp, width: '40%', height: '300px', vbind: vbind }"
|
||||
v-if="comp"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['field', 'close'],
|
||||
data() {
|
||||
return {
|
||||
keys: [],
|
||||
values: [],
|
||||
comp: undefined,
|
||||
vbind: undefined,
|
||||
current: undefined
|
||||
}
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["field", "close"],
|
||||
data() {
|
||||
return {
|
||||
keys: [],
|
||||
values: [],
|
||||
comp: undefined,
|
||||
vbind: undefined,
|
||||
current: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
Object.keys(this.field).map((v) => {
|
||||
this.keys.push(v);
|
||||
this.values.push(this.field[v]);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
doUpdate(v) {
|
||||
this.values[this.current.i] = v;
|
||||
},
|
||||
created() {
|
||||
Object.keys(this.field).map(v=>{
|
||||
this.keys.push(v)
|
||||
this.values.push(this.field[v])
|
||||
})
|
||||
jsonData(v, i) {
|
||||
this.current = { v: v, i: i };
|
||||
this.vbind = {
|
||||
field: this.$empty(this.values[i]) || typeof this.values[i] === "string" ? {} : this.values[i],
|
||||
close: true,
|
||||
};
|
||||
this.comp = "datatable/FieldAttribute";
|
||||
},
|
||||
methods: {
|
||||
doUpdate(v) {
|
||||
this.values[this.current.i] = v
|
||||
},
|
||||
jsonData(v, i) {
|
||||
this.current = {v: v, i: i}
|
||||
this.vbind = {field: this.$empty(this.values[i]) || typeof this.values[i] === 'string'? {} : this.values[i], close: true}
|
||||
this.comp = 'datatable/FieldAttribute'
|
||||
},
|
||||
addAttr() {
|
||||
this.keys.push(undefined)
|
||||
this.values.push(undefined)
|
||||
},
|
||||
remove(i) {
|
||||
this.$remove(this.keys, i)
|
||||
this.$remove(this.values, i)
|
||||
},
|
||||
update() {
|
||||
let obj = {}
|
||||
this.keys.map((v,i)=>{
|
||||
if(!this.$empty(v)) obj[v] = v.indexOf('__in')>0? this.values[i].split(',') : this.values[i]
|
||||
})
|
||||
this.$emit('update', obj)
|
||||
this.$emit('modalevent', {name: 'update', data: obj})
|
||||
if(this.close) this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
addAttr() {
|
||||
this.keys.push(undefined);
|
||||
this.values.push(undefined);
|
||||
},
|
||||
remove(i) {
|
||||
this.$remove(this.keys, i);
|
||||
this.$remove(this.values, i);
|
||||
},
|
||||
update() {
|
||||
let obj = {};
|
||||
this.keys.map((v, i) => {
|
||||
if (!this.$empty(v)) obj[v] = v.indexOf("__in") > 0 ? this.values[i].split(",") : this.values[i];
|
||||
});
|
||||
this.$emit("update", obj);
|
||||
this.$emit("modalevent", { name: "update", data: obj });
|
||||
if (this.close) this.$emit("close");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field mt-3 mb-1" v-if="field.format==='number'">
|
||||
<div
|
||||
class="field mt-3 mb-1"
|
||||
v-if="field.format === 'number'"
|
||||
>
|
||||
<label class="label fs-14">Chọn trường<span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-taginput
|
||||
size="is-small"
|
||||
size="is-small"
|
||||
v-model="tagsField"
|
||||
:data="pageData? pageData.fields.filter(v=>v.format==='number') : []"
|
||||
:data="pageData ? pageData.fields.filter((v) => v.format === 'number') : []"
|
||||
type="is-dark is-light"
|
||||
autocomplete
|
||||
:open-on-focus="true"
|
||||
field="name"
|
||||
icon="plus"
|
||||
placeholder="Chọn trường"
|
||||
>
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{props.option.name}}</span>
|
||||
<span :class="tagsField.find(v=>v.id===props.option.id)? 'has-text-dark' : ''"> {{$stripHtml(props.option.label, 60)}} </span>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
Không có trường thỏa mãn
|
||||
<span class="mr-3 has-text-danger has-text-weight-bold"> {{ props.option.name }}</span>
|
||||
<span :class="tagsField.find((v) => v.id === props.option.id) ? 'has-text-dark' : ''">
|
||||
{{ $stripHtml(props.option.label, 60) }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="empty"> Không 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>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'tagsField')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "tagsField").message }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2" v-if="tagsField.length>0">
|
||||
<a @dblclick="expression = expression? (expression + ' ' + v.name) : v.name"
|
||||
class="tag is-rounded" v-for="(v,i) in tagsField" :key="i">
|
||||
<span class="tooltip">
|
||||
{{ v.name }}
|
||||
<span class="tooltiptext" style="top: 60%; bottom: unset; min-width: max-content; left: 25px;">{{ $stripHtml(v.label) }}</span>
|
||||
</span>
|
||||
<div
|
||||
class="mt-2"
|
||||
v-if="tagsField.length > 0"
|
||||
>
|
||||
<a
|
||||
@dblclick="expression = expression ? expression + ' ' + v.name : v.name"
|
||||
class="tag is-rounded"
|
||||
v-for="(v, i) in tagsField"
|
||||
:key="i"
|
||||
>
|
||||
<span class="tooltip">
|
||||
{{ v.name }}
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="top: 60%; bottom: unset; min-width: max-content; left: 25px"
|
||||
>{{ $stripHtml(v.label) }}</span
|
||||
>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-3">
|
||||
<div class="field-body">
|
||||
<div class="field" v-if="field.format==='number'">
|
||||
<label class="label fs-14">Biểu thức có dạng Đúng / Sai <span class="has-text-danger"> * </span> </label>
|
||||
<div class="field is-horizontal mt-3">
|
||||
<div class="field-body">
|
||||
<div
|
||||
class="field"
|
||||
v-if="field.format === 'number'"
|
||||
>
|
||||
<label class="label fs-14"
|
||||
>Biểu thức có dạng Đúng / Sai
|
||||
<span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<p class="control is-expanded">
|
||||
<input class="input is-small" type="text" v-model="expression">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='expression')"> {{errors.find(v=>v.name==='expression').message}} </p>
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
v-model="expression"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'expression')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "expression").message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field" v-else>
|
||||
<label class="label"> Chuỗi kí tự <span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<div
|
||||
class="field"
|
||||
v-else
|
||||
>
|
||||
<label class="label"> Chuỗi kí tự <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<input
|
||||
class="input is-small"
|
||||
@@ -63,27 +99,53 @@
|
||||
{{ errors.find((v) => v.name === "searchText").msg }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-narrow" v-if="filterType==='color'">
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" v-model="color" @change="changeStyle()">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='color')"> {{errors.find(v=>v.name==='color').message}} </p>
|
||||
</div>
|
||||
<div class="field is-narrow" v-else-if="filterType==='size'">
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" placeholder="Nhập số" v-model="size" @change="changeStyle()">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='size')"> {{errors.find(v=>v.name==='size').message}} </p>
|
||||
</div>
|
||||
<div
|
||||
class="field is-narrow"
|
||||
v-if="filterType === 'color'"
|
||||
>
|
||||
<label class="label fs-14"> Mã màu <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
type="color"
|
||||
v-model="color"
|
||||
@change="changeStyle()"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'color')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "color").message }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="field is-narrow"
|
||||
v-else-if="filterType === 'size'"
|
||||
>
|
||||
<label class="label fs-14"> Cỡ chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
placeholder="Nhập số"
|
||||
v-model="size"
|
||||
@change="changeStyle()"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'size')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "size").message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['filterObj', 'filterType', 'pagename', 'field'],
|
||||
props: ["filterObj", "filterType", "pagename", "field"],
|
||||
data() {
|
||||
return {
|
||||
tagsField: [],
|
||||
@@ -92,79 +154,113 @@ export default {
|
||||
color: undefined,
|
||||
size: undefined,
|
||||
errors: [],
|
||||
searchText: undefined
|
||||
}
|
||||
searchText: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.color = this.filterObj.color
|
||||
this.size = this.filterObj.size
|
||||
this.expression = this.filterObj.expression? this.filterObj.expression : this.field.name
|
||||
if(this.filterObj.tags) {
|
||||
this.filterObj.tags.map(v=>{
|
||||
this.tagsField.push(this.pageData.fields.find(x=>x.name===v))
|
||||
})
|
||||
} else if(this.field.format==='number') this.tagsField.push(this.pageData.fields.find(v=>v.name===this.field.name))
|
||||
this.color = this.filterObj.color;
|
||||
this.size = this.filterObj.size;
|
||||
this.expression = this.filterObj.expression ? this.filterObj.expression : this.field.name;
|
||||
if (this.filterObj.tags) {
|
||||
this.filterObj.tags.map((v) => {
|
||||
this.tagsField.push(this.pageData.fields.find((x) => x.name === v));
|
||||
});
|
||||
} else if (this.field.format === "number")
|
||||
this.tagsField.push(this.pageData.fields.find((v) => v.name === this.field.name));
|
||||
},
|
||||
watch: {
|
||||
expression: function(newVal) {
|
||||
if(this.$empty(newVal)) return
|
||||
else this.changeStyle()
|
||||
}
|
||||
expression: function (newVal) {
|
||||
if (this.$empty(newVal)) return;
|
||||
else this.changeStyle();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
colorscheme: {
|
||||
get: function() {return this.$store.state.colorscheme},
|
||||
set: function(val) {this.$store.commit("updateColorScheme", {colorscheme: val})}
|
||||
get: function () {
|
||||
return this.$store.state.colorscheme;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit("updateColorScheme", { colorscheme: val });
|
||||
},
|
||||
},
|
||||
pageData: {
|
||||
get: function() {return this.$store.state[this.pagename]},
|
||||
set: function(val) {this.$store.commit('updateStore', {name: this.pagename, data: val})}
|
||||
get: function () {
|
||||
return this.$store.state[this.pagename];
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit("updateStore", { name: this.pagename, data: val });
|
||||
},
|
||||
},
|
||||
colorchoice: {
|
||||
get: function() {return this.$store.state.colorchoice},
|
||||
set: function(val) {this.$store.commit("updateColorChoice", {colorchoice: val})}
|
||||
}
|
||||
get: function () {
|
||||
return this.$store.state.colorchoice;
|
||||
},
|
||||
set: function (val) {
|
||||
this.$store.commit("updateColorChoice", { colorchoice: val });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeStyle() {
|
||||
let check = this.field.format==='number'? this.checkExpression() : this.checkCondition()
|
||||
if(!check) return
|
||||
var row = this.field.format==='number'? {expression: this.expression, tags: this.tagsField.map(v=>v.name)}
|
||||
: {keyword: this.searchText, type: 'search'}
|
||||
this.filterType==='color'? row.color = this.color : row.size = this.size
|
||||
this.$emit('databack', row)
|
||||
let check = this.field.format === "number" ? this.checkExpression() : this.checkCondition();
|
||||
if (!check) return;
|
||||
var row =
|
||||
this.field.format === "number"
|
||||
? {
|
||||
expression: this.expression,
|
||||
tags: this.tagsField.map((v) => v.name),
|
||||
}
|
||||
: { keyword: this.searchText, type: "search" };
|
||||
this.filterType === "color" ? (row.color = this.color) : (row.size = this.size);
|
||||
this.$emit("databack", row);
|
||||
},
|
||||
checkCondition() {
|
||||
this.errors = []
|
||||
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
|
||||
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'})
|
||||
if(this.$empty(this.searchText)) this.errors.push({name: 'searchText', message: 'Chưa nhập chuỗi kí tự'})
|
||||
return this.errors.length>0? false : true
|
||||
this.errors = [];
|
||||
if (this.filterType === "color" && this.$empty(this.color))
|
||||
this.errors.push({ name: "color", message: "Chọn màu" });
|
||||
if (this.filterType === "size" && this.$empty(this.size))
|
||||
this.errors.push({ name: "size", message: "Nhập cỡ chữ" });
|
||||
if (this.$empty(this.searchText))
|
||||
this.errors.push({
|
||||
name: "searchText",
|
||||
message: "Chưa nhập chuỗi kí tự",
|
||||
});
|
||||
return this.errors.length > 0 ? false : true;
|
||||
},
|
||||
checkExpression() {
|
||||
this.errors = []
|
||||
if(this.filterType==='color' && this.$empty(this.color)) this.errors.push({name: 'color', message: 'Chọn màu'})
|
||||
if(this.filterType==='size' && this.$empty(this.size)) this.errors.push({name: 'size', message: 'Nhập cỡ chữ'})
|
||||
let val = this.$copy(this.expression)
|
||||
let exp = this.$copy(this.expression)
|
||||
this.tagsField.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])")
|
||||
})
|
||||
try {
|
||||
let value = this.$calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} else if(!(eval(value)===true || eval(value)===false)) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
this.errors = [];
|
||||
if (this.filterType === "color" && this.$empty(this.color))
|
||||
this.errors.push({ name: "color", message: "Chọn màu" });
|
||||
if (this.filterType === "size" && this.$empty(this.size))
|
||||
this.errors.push({ name: "size", message: "Nhập cỡ chữ" });
|
||||
let val = this.$copy(this.expression);
|
||||
let exp = this.$copy(this.expression);
|
||||
this.tagsField.forEach((v) => {
|
||||
let myRegExp = new RegExp(v.name, "g");
|
||||
val = val.replace(myRegExp, Math.random());
|
||||
exp = exp.replace(myRegExp, "field.formatNumber(row['" + v.name + "'])");
|
||||
});
|
||||
try {
|
||||
let value = this.$calc(val);
|
||||
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
|
||||
this.errors.push({
|
||||
name: "expression",
|
||||
message: "Biểu thức không hợp lệ",
|
||||
});
|
||||
} else if (!(eval(value) === true || eval(value) === false)) {
|
||||
this.errors.push({
|
||||
name: "expression",
|
||||
message: "Biểu thức không hợp lệ",
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
this.errors.push({name: 'expression', message: 'Biểu thức không hợp lệ'})
|
||||
} catch (err) {
|
||||
this.errors.push({
|
||||
name: "expression",
|
||||
message: "Biểu thức không hợp lệ",
|
||||
});
|
||||
}
|
||||
return this.errors.length>0? false : true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
return this.errors.length > 0 ? false : true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
</p>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li v-for="(v, i) in tabs" :key="i" :class="tab.code === v.code ? 'is-active' : ''" @click="tab = v">
|
||||
<li
|
||||
v-for="(v, i) in tabs"
|
||||
:key="i"
|
||||
:class="tab.code === v.code ? 'is-active' : ''"
|
||||
@click="tab = v"
|
||||
>
|
||||
<a>{{ v.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -14,17 +19,37 @@
|
||||
|
||||
<template v-if="tab.code === 'expression' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
||||
<template v-if="radio ? radio.code === 'condition' && sideBar === 'bgcolor' : false">
|
||||
<div v-for="(v, i) in bgcolorFilter" :key="v.id" class="px-4">
|
||||
<div
|
||||
v-for="(v, i) in bgcolorFilter"
|
||||
:key="v.id"
|
||||
class="px-4"
|
||||
>
|
||||
<FilterOption
|
||||
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }"
|
||||
v-bind="{
|
||||
filterObj: v,
|
||||
filterType: 'color',
|
||||
pagename: pagename,
|
||||
field: openField,
|
||||
}"
|
||||
:ref="v.id"
|
||||
@databack="doConditionFilter($event, 'bgcolor', v.id)"
|
||||
/>
|
||||
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'">
|
||||
<a class="has-text-primary mr-5" @click="addCondition(bgcolorFilter)" v-if="bgcolorFilter.length <= 30">
|
||||
<p
|
||||
class="fs-14 mt-1"
|
||||
:class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
|
||||
>
|
||||
<a
|
||||
class="has-text-primary mr-5"
|
||||
@click="addCondition(bgcolorFilter)"
|
||||
v-if="bgcolorFilter.length <= 30"
|
||||
>
|
||||
Thêm
|
||||
</a>
|
||||
<a class="has-text-danger" @click="removeCondition(bgcolorFilter, i)" v-if="bgcolorFilter.length > 1">
|
||||
<a
|
||||
class="has-text-danger"
|
||||
@click="removeCondition(bgcolorFilter, i)"
|
||||
v-if="bgcolorFilter.length > 1"
|
||||
>
|
||||
Bớt
|
||||
</a>
|
||||
</p>
|
||||
@@ -32,29 +57,77 @@
|
||||
</template>
|
||||
|
||||
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'color' : false">
|
||||
<div v-for="(v, i) in colorFilter" :key="v.id" class="px-4">
|
||||
<div
|
||||
v-for="(v, i) in colorFilter"
|
||||
:key="v.id"
|
||||
class="px-4"
|
||||
>
|
||||
<FilterOption
|
||||
v-bind="{ filterObj: v, filterType: 'color', pagename: pagename, field: openField }"
|
||||
v-bind="{
|
||||
filterObj: v,
|
||||
filterType: 'color',
|
||||
pagename: pagename,
|
||||
field: openField,
|
||||
}"
|
||||
:ref="v.id"
|
||||
@databack="doConditionFilter($event, 'color', v.id)"
|
||||
/>
|
||||
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'">
|
||||
<a class="has-text-primary mr-5" @click="addCondition(colorFilter)" v-if="colorFilter.length <= 30"> Thêm </a>
|
||||
<a class="has-text-danger" @click="removeCondition(colorFilter, i)" v-if="colorFilter.length > 1"> Bớt </a>
|
||||
<p
|
||||
class="fs-14 mt-1"
|
||||
:class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
|
||||
>
|
||||
<a
|
||||
class="has-text-primary mr-5"
|
||||
@click="addCondition(colorFilter)"
|
||||
v-if="colorFilter.length <= 30"
|
||||
>
|
||||
Thêm
|
||||
</a>
|
||||
<a
|
||||
class="has-text-danger"
|
||||
@click="removeCondition(colorFilter, i)"
|
||||
v-if="colorFilter.length > 1"
|
||||
>
|
||||
Bớt
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="radio ? radio.code === 'condition' && sideBar === 'textsize' : false">
|
||||
<div v-for="(v, i) in sizeFilter" :key="v.id" class="px-4">
|
||||
<div
|
||||
v-for="(v, i) in sizeFilter"
|
||||
:key="v.id"
|
||||
class="px-4"
|
||||
>
|
||||
<FilterOption
|
||||
v-bind="{ filterObj: v, filterType: 'size', pagename: pagename, field: openField }"
|
||||
v-bind="{
|
||||
filterObj: v,
|
||||
filterType: 'size',
|
||||
pagename: pagename,
|
||||
field: openField,
|
||||
}"
|
||||
:ref="v.id"
|
||||
@databack="doConditionFilter($event, 'textsize', v.id)"
|
||||
/>
|
||||
<p class="fs-14 mt-1" :class="currentField.format === 'string' ? 'mb-1' : 'mb-2'">
|
||||
<a class="has-text-primary mr-5" @click="addCondition(sizeFilter)" v-if="sizeFilter.length <= 30"> Thêm </a>
|
||||
<a class="has-text-danger" @click="removeCondition(sizeFilter, i)" v-if="sizeFilter.length > 1"> Bớt </a>
|
||||
<p
|
||||
class="fs-14 mt-1"
|
||||
:class="currentField.format === 'string' ? 'mb-1' : 'mb-2'"
|
||||
>
|
||||
<a
|
||||
class="has-text-primary mr-5"
|
||||
@click="addCondition(sizeFilter)"
|
||||
v-if="sizeFilter.length <= 30"
|
||||
>
|
||||
Thêm
|
||||
</a>
|
||||
<a
|
||||
class="has-text-danger"
|
||||
@click="removeCondition(sizeFilter, i)"
|
||||
v-if="sizeFilter.length > 1"
|
||||
>
|
||||
Bớt
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -62,21 +135,39 @@
|
||||
|
||||
<template v-else-if="tab.code === 'script' && ['bgcolor', 'color', 'textsize'].find((x) => x === sideBar)">
|
||||
<p class="my-4 mx-4">
|
||||
<a @click="copyContent(script ? script : '')" class="mr-6">
|
||||
<a
|
||||
@click="copyContent(script ? script : '')"
|
||||
class="mr-6"
|
||||
>
|
||||
<span class="icon-text">
|
||||
<SvgIcon class="mr-2" v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"></SvgIcon>
|
||||
<SvgIcon
|
||||
class="mr-2"
|
||||
v-bind="{ name: 'copy.svg', type: 'primary', siz: 18 }"
|
||||
></SvgIcon>
|
||||
<span class="fs-16">Copy</span>
|
||||
</span>
|
||||
</a>
|
||||
<a @click="paste()" class="mr-6">
|
||||
<a
|
||||
@click="paste()"
|
||||
class="mr-6"
|
||||
>
|
||||
<span class="icon-text">
|
||||
<SvgIcon class="mr-2" v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"></SvgIcon>
|
||||
<SvgIcon
|
||||
class="mr-2"
|
||||
v-bind="{ name: 'pen1.svg', type: 'primary', siz: 18 }"
|
||||
></SvgIcon>
|
||||
<span class="fs-16">Paste</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div class="mx-4">
|
||||
<textarea class="textarea fs-14" rows="8" v-model="script" @change="checkScript()" @dblclick="doCheck"></textarea>
|
||||
<textarea
|
||||
class="textarea fs-14"
|
||||
rows="8"
|
||||
v-model="script"
|
||||
@change="checkScript()"
|
||||
@dblclick="doCheck"
|
||||
></textarea>
|
||||
</div>
|
||||
<p class="mt-5 mx-4">
|
||||
<span class="icon-text fsb-18">
|
||||
@@ -87,14 +178,29 @@
|
||||
<div class="field is-grouped mx-4 mt-4">
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Đoạn text</p>
|
||||
<input class="input" type="text" placeholder="" v-model="source" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="source"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<p class="fsb-14 mb-1">Thay bằng</p>
|
||||
<input class="input" type="text" placeholder="" v-model="target" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="target"
|
||||
/>
|
||||
</div>
|
||||
<div class="control pl-5">
|
||||
<button class="button is-primary is-rounded is-outlined mt-5" @click="replace()">Replace</button>
|
||||
<button
|
||||
class="button is-primary is-rounded is-outlined mt-5"
|
||||
@click="replace()"
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-5 pt-2 mx-4">
|
||||
@@ -103,10 +209,24 @@
|
||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 22 }"></SvgIcon>
|
||||
</span>
|
||||
</p>
|
||||
<p class="mx-4 mt-4"><button class="button is-primary is-rounded" @click="changeScript()">Cập nhật</button></p>
|
||||
<p class="mx-4 mt-4">
|
||||
<button
|
||||
class="button is-primary is-rounded"
|
||||
@click="changeScript()"
|
||||
>
|
||||
Cập nhật
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
<TableOption v-bind="{ pagename: pagename }" v-else-if="sideBar === 'option'"> </TableOption>
|
||||
<CreateTemplate v-else-if="sideBar === 'template'" v-bind="{ pagename: pagename, field: openField }">
|
||||
<TableOption
|
||||
v-bind="{ pagename: pagename }"
|
||||
v-else-if="sideBar === 'option'"
|
||||
>
|
||||
</TableOption>
|
||||
<CreateTemplate
|
||||
v-else-if="sideBar === 'template'"
|
||||
v-bind="{ pagename: pagename, field: openField }"
|
||||
>
|
||||
</CreateTemplate>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="mb-4" v-if="currentsetting ? currentsetting.user === login.id : false">
|
||||
<div
|
||||
class="mb-4"
|
||||
v-if="currentsetting ? currentsetting.user === login.id : false"
|
||||
>
|
||||
<p class="fs-16 has-text-findata">
|
||||
Đang mở: <b>{{ $stripHtml(currentsetting.name, 40) }}</b>
|
||||
</p>
|
||||
@@ -7,7 +10,11 @@
|
||||
<div class="field">
|
||||
<label class="label fs-14">Chọn chế độ lưu <span class="has-text-danger"> * </span></label>
|
||||
<div class="control is-expanded fs-14">
|
||||
<a class="mr-5" v-if="isOverwrite()" @click="changeType('overwrite')">
|
||||
<a
|
||||
class="mr-5"
|
||||
v-if="isOverwrite()"
|
||||
@click="changeType('overwrite')"
|
||||
>
|
||||
<span class="icon-text">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
@@ -22,7 +29,11 @@
|
||||
<a @click="changeType('new')">
|
||||
<span class="icon-text">
|
||||
<SvgIcon
|
||||
v-bind="{ name: radioSave === 'new' ? 'radio-checked.svg' : 'radio-unchecked.svg', type: 'gray', size: 22 }"
|
||||
v-bind="{
|
||||
name: radioSave === 'new' ? 'radio-checked.svg' : 'radio-unchecked.svg',
|
||||
type: 'gray',
|
||||
size: 22,
|
||||
}"
|
||||
></SvgIcon>
|
||||
Tạo mới
|
||||
</span>
|
||||
@@ -33,16 +44,30 @@
|
||||
<div class="field mt-4 px-0 mx-0">
|
||||
<label class="label fs-14">Tên thiết lập <span class="has-text-danger"> * </span></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" v-model="name" ref="name" v-on:keyup.enter="saveSetting" />
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="name"
|
||||
ref="name"
|
||||
v-on:keyup.enter="saveSetting"
|
||||
/>
|
||||
</div>
|
||||
<div class="help has-text-danger" v-if="errors.find((v) => v.name === 'name')">
|
||||
<div
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'name')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "name").msg }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-4 px-0 mx-0">
|
||||
<label class="label fs-14"> Mô tả </label>
|
||||
<p class="control is-expanded">
|
||||
<textarea class="textarea" rows="4" v-model="note"></textarea>
|
||||
<textarea
|
||||
class="textarea"
|
||||
rows="4"
|
||||
v-model="note"
|
||||
></textarea>
|
||||
</p>
|
||||
</div>
|
||||
<!--
|
||||
@@ -60,11 +85,19 @@
|
||||
</div>-->
|
||||
</template>
|
||||
<div class="field mt-5 px-0 mx-0">
|
||||
<label class="label fs-14" v-if="status !== undefined" :class="status ? 'has-text-primary' : 'has-text-danger'">
|
||||
<label
|
||||
class="label fs-14"
|
||||
v-if="status !== undefined"
|
||||
:class="status ? 'has-text-primary' : 'has-text-danger'"
|
||||
>
|
||||
{{ status ? "Lưu thiết lập thành công." : "Lỗi. Lưu thiết lập thất bại." }}
|
||||
</label>
|
||||
<p class="control is-expanded">
|
||||
<a class="button is-primary has-text-white" @click="saveSetting()">Lưu lại</a>
|
||||
<a
|
||||
class="button is-primary has-text-white"
|
||||
@click="saveSetting()"
|
||||
>Lưu lại</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -118,7 +151,10 @@ async function saveSetting() {
|
||||
let result;
|
||||
if (radioSave.value === "new") {
|
||||
if ($empty(name)) {
|
||||
return errors.push({ name: "name", msg: "Tên thiết lập không được bỏ trống" });
|
||||
return errors.push({
|
||||
name: "name",
|
||||
msg: "Tên thiết lập không được bỏ trống",
|
||||
});
|
||||
}
|
||||
result = await $insertapi("usersetting", data);
|
||||
} else {
|
||||
|
||||
@@ -1,52 +1,114 @@
|
||||
<template>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li :class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`" v-for="v in tabs">
|
||||
<li
|
||||
:class="`${v.code === tab ? 'is-active has-text-weight-bold fs-18' : 'fs-18'}`"
|
||||
v-for="v in tabs"
|
||||
>
|
||||
<a @click="changeTab(v)">{{ v.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="tab === 'datatype'">
|
||||
<Caption class="mb-3" v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"></Caption>
|
||||
<div class="py-1 border-bottom is-clickable" v-for="x in current.fields">
|
||||
<Caption
|
||||
class="mb-3"
|
||||
v-bind="{ title: 'Kiểu dữ liệu (type)', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<div
|
||||
class="py-1 border-bottom is-clickable"
|
||||
v-for="x in current.fields"
|
||||
>
|
||||
{{ x.name }}
|
||||
<span class="ml-6 has-text-grey">{{ x.type }}</span>
|
||||
<a class="ml-6 has-text-primary" v-if="x.model" @click="openModel(x)">{{ x.model }}</a>
|
||||
<a
|
||||
class="ml-6 has-text-primary"
|
||||
v-if="x.model"
|
||||
@click="openModel(x)"
|
||||
>{{ x.model }}</a
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="columns mx-0 mb-0 pb-0">
|
||||
<div class="column is-7">
|
||||
<Caption class="mb-2" v-bind="{ title: 'Values', type: 'has-text-warning' }"></Caption>
|
||||
<input class="input" rows="1" v-model="values" />
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: 'Values', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<input
|
||||
class="input"
|
||||
rows="1"
|
||||
v-model="values"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-4s">
|
||||
<Caption class="mb-2" v-bind="{ title: 'Filter', type: 'has-text-warning' }"></Caption>
|
||||
<input class="input" rows="1" v-model="filter" />
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: 'Filter', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<input
|
||||
class="input"
|
||||
rows="1"
|
||||
v-model="filter"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<Caption class="mb-2" v-bind="{ title: 'Load', type: 'has-text-warning' }"></Caption>
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: 'Load', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<div>
|
||||
<button class="button is-primary has-text-white" @click="loadData()">Load</button>
|
||||
<button
|
||||
class="button is-primary has-text-white"
|
||||
@click="loadData()"
|
||||
>
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Caption class="mb-2" v-bind="{ title: 'Query', type: 'has-text-warning' }"></Caption>
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: 'Query', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<div class="mb-4">
|
||||
{{ query }}
|
||||
<a class="has-text-primary ml-5" @click="copy()">copy</a>
|
||||
<a
|
||||
class="has-text-primary ml-5"
|
||||
@click="copy()"
|
||||
>copy</a
|
||||
>
|
||||
<p>
|
||||
{{ apiUrl }}
|
||||
<a class="has-text-primary ml-5" @click="$copyToClipboard(apiUrl)">copy</a>
|
||||
<a class="has-text-primary ml-5" target="_blank" :href="apiUrl">open</a>
|
||||
<a
|
||||
class="has-text-primary ml-5"
|
||||
@click="$copyToClipboard(apiUrl)"
|
||||
>copy</a
|
||||
>
|
||||
<a
|
||||
class="has-text-primary ml-5"
|
||||
target="_blank"
|
||||
:href="apiUrl"
|
||||
>open</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Caption class="mb-2" v-bind="{ title: 'Data', type: 'has-text-warning' }"></Caption>
|
||||
<DataTable v-bind="{ pagename: pagename }" v-if="pagedata" />
|
||||
<Caption
|
||||
class="mb-2"
|
||||
v-bind="{ title: 'Data', type: 'has-text-warning' }"
|
||||
></Caption>
|
||||
<DataTable
|
||||
v-bind="{ pagename: pagename }"
|
||||
v-if="pagedata"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
<template>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li :class="selectType.code===v.code? 'is-active fs-16' : 'fs-16'" v-for="v in fieldType">
|
||||
<a @click="selectType = v"><span>{{ v.name }}</span></a>
|
||||
<li
|
||||
:class="selectType.code === v.code ? 'is-active fs-16' : 'fs-16'"
|
||||
v-for="v in fieldType"
|
||||
>
|
||||
<a @click="selectType = v"
|
||||
><span>{{ v.name }}</span></a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="selectType.code==='formula'">
|
||||
<b-radio :class="i===1? 'ml-5' : null" v-model="choice" v-for="(v,i) in choices" :key="i"
|
||||
:native-value="v.code">
|
||||
<span :class="v.code===choice? 'fsb-16' : 'fs-16'">{{v.name}}</span>
|
||||
</b-radio>
|
||||
<div class="has-background-light mt-3 px-3 py-3">
|
||||
<div class="tags are-medium mb-0" v-if="choice==='function'">
|
||||
<span :class="`tag ${func===v.code? 'is-primary' : 'is-dark'} is-rounded is-clickable`"
|
||||
v-for="(v,i) in funcs" :key="i" @click="changeFunc(v)" @dblclick="addFunc(v)">{{v.name}}</span>
|
||||
<template v-if="selectType.code === 'formula'">
|
||||
<b-radio
|
||||
:class="i === 1 ? 'ml-5' : null"
|
||||
v-model="choice"
|
||||
v-for="(v, i) in choices"
|
||||
:key="i"
|
||||
:native-value="v.code"
|
||||
>
|
||||
<span :class="v.code === choice ? 'fsb-16' : 'fs-16'">{{ v.name }}</span>
|
||||
</b-radio>
|
||||
<div class="has-background-light mt-3 px-3 py-3">
|
||||
<div
|
||||
class="tags are-medium mb-0"
|
||||
v-if="choice === 'function'"
|
||||
>
|
||||
<span
|
||||
:class="`tag ${func === v.code ? 'is-primary' : 'is-dark'} is-rounded is-clickable`"
|
||||
v-for="(v, i) in funcs"
|
||||
:key="i"
|
||||
@click="changeFunc(v)"
|
||||
@dblclick="addFunc(v)"
|
||||
>{{ v.name }}</span
|
||||
>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="field px-0 mx-0">
|
||||
<label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label>
|
||||
<div class="control">
|
||||
<!--<b-taginput
|
||||
<template v-else>
|
||||
<div class="field px-0 mx-0">
|
||||
<label class="label fs-14">Chọn trường<span class="has-text-danger"> *</span> </label>
|
||||
<div class="control">
|
||||
<!--<b-taginput
|
||||
size="is-small"
|
||||
v-model="tags"
|
||||
:data="fields.filter(v=>v.format==='number')"
|
||||
@@ -39,296 +58,418 @@
|
||||
Không có trường thỏa mãn
|
||||
</template>
|
||||
</b-taginput>-->
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='tags')"> {{errors.find(v=>v.name==='tags').message}} </p>
|
||||
</div>
|
||||
<div class="field mt-3" v-if="tags.length>0">
|
||||
</div>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'tags')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "tags").message }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="field mt-3"
|
||||
v-if="tags.length > 0"
|
||||
>
|
||||
<p class="help is-primary mb-1">Click đúp vào để thêm vào công thức tính.</p>
|
||||
<div class="tags mb-2">
|
||||
<span @dblclick="formula = formula? (formula + ' ' + v.name) : v.name" class="tag is-dark is-rounded is-clickable"
|
||||
v-for="v in tags">
|
||||
{{v.name}}
|
||||
</span>
|
||||
<span
|
||||
@dblclick="formula = formula ? formula + ' ' + v.name : v.name"
|
||||
class="tag is-dark is-rounded is-clickable"
|
||||
v-for="v in tags"
|
||||
>
|
||||
{{ v.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<span v-for="(v,i) in operator" :key="i">
|
||||
<span @dblclick="addOperator(v)" class="tag is-primary is-rounded is-clickable mr-4">
|
||||
<span class="fs-16">{{v.code}}</span>
|
||||
<span
|
||||
v-for="(v, i) in operator"
|
||||
:key="i"
|
||||
>
|
||||
<span
|
||||
@dblclick="addOperator(v)"
|
||||
class="tag is-primary is-rounded is-clickable mr-4"
|
||||
>
|
||||
<span class="fs-16">{{ v.code }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="field mt-3 px-0 mx-0">
|
||||
<label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<textarea class="textarea" rows="3" type="text" :placeholder="placeholder" v-model="formula"> </textarea>
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='formula')"> {{errors.find(v=>v.name==='formula').message}} </p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-3 px-0 mx-0">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Hiển thị theo <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectUnit? selectUnit.name : ''"
|
||||
placeholder=""
|
||||
:keep-first=true
|
||||
:open-on-focus=true
|
||||
:data="moneyunit"
|
||||
field="name"
|
||||
@select="option => selectUnit = option">
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14">Phần thập phân</label>
|
||||
<div class="control">
|
||||
<input class="input is-small" type="text" placeholder="" v-model="decimal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="field px-0 mx-0">
|
||||
<label class="label">Tên trường <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<input class="input" type="text" placeholder="Tên trường phải là duy nhất" v-model="name"
|
||||
:readonly="selectType? selectType.code==='formula': false">
|
||||
</p>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='name')"> {{errors.find(v=>v.name==='name').message}} </p>
|
||||
<p class="help has-text-primary" v-else> Tên trường do hệ thống tự sinh.</p>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<label class="label">Mô tả<span class="has-text-danger"> *</span></label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded" >
|
||||
<input
|
||||
class="input"
|
||||
<div class="field mt-3 px-0 mx-0">
|
||||
<label class="label fs-14">Công thức tính <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<textarea
|
||||
class="textarea"
|
||||
rows="3"
|
||||
type="text"
|
||||
v-model="label"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button" @click="editLabel()">
|
||||
<span><SvgIcon v-bind="{name: 'pen.svg', type: 'dark', size: 17}"></SvgIcon></span>
|
||||
</button>
|
||||
</div>
|
||||
:placeholder="placeholder"
|
||||
v-model="formula"
|
||||
>
|
||||
</textarea>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'formula')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "formula").message }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="help has-text-danger" v-if="errors.find(v=>v.name==='label')"> {{errors.find(v=>v.name==='label').message}} </p>
|
||||
</div>
|
||||
<div class="field mt-5" v-if="selectType.code==='empty'">
|
||||
<label class="label"
|
||||
>Kiểu dữ liệu
|
||||
<span class="has-text-danger"> * </span>
|
||||
</label>
|
||||
<div class="control fs-14">
|
||||
<span class="mr-4" v-for="(v,i) in datatype">
|
||||
<a class="icon-text" @click="changeType(v)">
|
||||
<SvgIcon v-bind="{name: `radio-${radioType.code===v.code? '' : 'un'}checked.svg`, type: 'gray', size: 22}"></SvgIcon>
|
||||
</a>
|
||||
{{v.name}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-3 px-0 mx-0">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14">Hiển thị theo <span class="has-text-danger"> * </span> </label>
|
||||
<div class="control">
|
||||
<b-autocomplete
|
||||
size="is-small"
|
||||
icon-right="magnify"
|
||||
:value="selectUnit ? selectUnit.name : ''"
|
||||
placeholder=""
|
||||
:keep-first="true"
|
||||
:open-on-focus="true"
|
||||
:data="moneyunit"
|
||||
field="name"
|
||||
@select="(option) => (selectUnit = option)"
|
||||
>
|
||||
</b-autocomplete>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field mt-5">
|
||||
<p class="control">
|
||||
<a class="button is-primary has-text-white"
|
||||
@click="selectType.code==='formula'? createField() : createEmptyField()">Tạo cột</a>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label fs-14">Phần thập phân</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="decimal"
|
||||
/>
|
||||
</div>
|
||||
<Modal v-bind="showmodal" v-if="showmodal" @label="changeLabel" @close="close"></Modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="field px-0 mx-0">
|
||||
<label class="label">Tên trường <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Tên trường phải là duy nhất"
|
||||
v-model="name"
|
||||
:readonly="selectType ? selectType.code === 'formula' : false"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'name')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "name").message }}
|
||||
</p>
|
||||
<p
|
||||
class="help has-text-primary"
|
||||
v-else
|
||||
>
|
||||
Tên trường do hệ thống tự sinh.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<label class="label">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>
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/index'
|
||||
import ScrollBox from '~/components/datatable/ScrollBox'
|
||||
const emit = defineEmits(['modalevent'])
|
||||
const store = useStore()
|
||||
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp()
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object,
|
||||
filters: Object,
|
||||
filterData: Object,
|
||||
width: String
|
||||
})
|
||||
const moneyunit = store.moneyunit
|
||||
const datatype = store.datatype
|
||||
var showmodal = ref()
|
||||
var pagedata = store[props.pagename]
|
||||
var selectUnit = moneyunit.find(v=>v.code==='one')
|
||||
var data = []
|
||||
var current = 1
|
||||
var filterData = []
|
||||
var loading = false
|
||||
var fieldType = [{code: 'formula', name: 'Tạo công thức'}, {code: 'empty', name: 'Tạo cột rỗng'}]
|
||||
var errors = []
|
||||
var tags = []
|
||||
var formula = undefined
|
||||
var name = `f${$id().toLocaleLowerCase()}`
|
||||
var label = undefined
|
||||
var errors = []
|
||||
var selectType = fieldType.find(v=>v.code==='empty')
|
||||
var radioType = ref(datatype.find(v=>v.code==='string'))
|
||||
var fields = []
|
||||
var options = undefined
|
||||
var columns = $copy(pagedata.fields.filter(v=>v.format==='number'))
|
||||
var decimal = undefined
|
||||
var choices = [{code: 'column', name: 'Dùng cột dữ liệu'}, {code: 'function', name: 'Dùng hàm số'}]
|
||||
var choice = 'column'
|
||||
var funcs = [{code: 'sum', name: 'Sum'}, {code: 'max', name: 'Max'}, {code: 'min', name: 'Min'}, {code: 'avg', name: 'Avg'}]
|
||||
var func = 'sum'
|
||||
var placeholder = 'Minh hoạ công thức: f10001 + f10002'
|
||||
var args = undefined
|
||||
var operator = [{code: '+', name: 'Cộng'}, {code: '-', name: 'Trừ'}, {code: '*', name: 'Nhân'}, {code: '/', name: 'Chia'}, {code: '>', name: 'Lớn hơn'},
|
||||
{code: '>=', name: 'Lớn hơn hoặc bằng'}, {code: '<', name: 'Nhỏ hơn'}, {code: '<=', name: 'Nhỏ hơn hoặc bằng'}, {code: '==', name: 'Bằng'},
|
||||
{code: '&&', name: 'Và'}, {code: '||', name: 'Hoặc'}, {code: 'iif', name: 'Điều kiện rẽ nhánh'}]
|
||||
function editLabel() {
|
||||
if($empty(label)) return
|
||||
showmodal.value = {component: 'datatable/EditLabel', width: '500px', height: '300px', vbind: {label: label}}
|
||||
}
|
||||
function close() {
|
||||
showmodal.value = null
|
||||
}
|
||||
function changeLabel(evt) {
|
||||
label = evt
|
||||
showmodal.value = null
|
||||
}
|
||||
function changeType(v) {
|
||||
radioType.value = v
|
||||
}
|
||||
function addFunc(v) {
|
||||
formula = (formula? formula + ' ' : '') + v.name + '(C0: C2)'
|
||||
}
|
||||
function addOperator(v) {
|
||||
let text = v.code==='iif'? 'a>b? c : d' : v.code
|
||||
formula = `${formula || ''} ${text}`
|
||||
}
|
||||
function changeFunc(v) {
|
||||
placeholder = `${v.name}(C0:C2) hoặc ${v.name}(C0,C1,C2). C là viết tắt của cột dữ liệu, số thứ tự của cột bắt đầu từ 0`
|
||||
func = v.code
|
||||
}
|
||||
function getFields() {
|
||||
fields = pagedata? $copy(pagedata.fields) : []
|
||||
fields.map(v=>v.caption = (v.label? v.label.indexOf('<')>=0 : false)? v.name : v.label)
|
||||
}
|
||||
function checkFunc() {
|
||||
let error = false
|
||||
let val = formula.trim().replaceAll(' ', '')
|
||||
if(val.toLowerCase().indexOf(func)<0) error = true
|
||||
let start = val.toLowerCase().indexOf('(')
|
||||
let end = val.toLowerCase().indexOf(')')
|
||||
if( start<0 || end<0) error = true
|
||||
let content = val.substring(start+1, end)
|
||||
if($empty(content)) error = true
|
||||
let content1 = content.replaceAll(':', ',')
|
||||
let arr = content1.split(',')
|
||||
arr.map(v=>{
|
||||
let arr1 = v.toLowerCase().split('c')
|
||||
if(arr1.length!==2) error = true
|
||||
else if(!$isNumber(arr1[1])) error = true
|
||||
})
|
||||
return error? 'error' : content
|
||||
}
|
||||
function checkValid() {
|
||||
errors = []
|
||||
if(tags.length===0 && choice==='column') {
|
||||
errors.push({name: 'tags', message: 'Chưa chọn trường xây dựng công thức.'})
|
||||
import { useStore } from "@/stores/index";
|
||||
import ScrollBox from "~/components/datatable/ScrollBox";
|
||||
const emit = defineEmits(["modalevent"]);
|
||||
const store = useStore();
|
||||
const { $id, $copy, $clone, $empty, $stripHtml, $createField, $calc, $isNumber } = useNuxtApp();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
field: Object,
|
||||
filters: Object,
|
||||
filterData: Object,
|
||||
width: String,
|
||||
});
|
||||
const moneyunit = store.moneyunit;
|
||||
const datatype = store.datatype;
|
||||
var showmodal = ref();
|
||||
var pagedata = store[props.pagename];
|
||||
var selectUnit = moneyunit.find((v) => v.code === "one");
|
||||
var data = [];
|
||||
var current = 1;
|
||||
var filterData = [];
|
||||
var loading = false;
|
||||
var fieldType = [
|
||||
{ code: "formula", name: "Tạo công thức" },
|
||||
{ code: "empty", name: "Tạo cột rỗng" },
|
||||
];
|
||||
var errors = [];
|
||||
var tags = [];
|
||||
var formula = undefined;
|
||||
var name = `f${$id().toLocaleLowerCase()}`;
|
||||
var label = undefined;
|
||||
var errors = [];
|
||||
var selectType = fieldType.find((v) => v.code === "empty");
|
||||
var radioType = ref(datatype.find((v) => v.code === "string"));
|
||||
var fields = [];
|
||||
var options = undefined;
|
||||
var columns = $copy(pagedata.fields.filter((v) => v.format === "number"));
|
||||
var decimal = undefined;
|
||||
var choices = [
|
||||
{ code: "column", name: "Dùng cột dữ liệu" },
|
||||
{ code: "function", name: "Dùng hàm số" },
|
||||
];
|
||||
var choice = "column";
|
||||
var funcs = [
|
||||
{ code: "sum", name: "Sum" },
|
||||
{ code: "max", name: "Max" },
|
||||
{ code: "min", name: "Min" },
|
||||
{ code: "avg", name: "Avg" },
|
||||
];
|
||||
var func = "sum";
|
||||
var placeholder = "Minh hoạ công thức: f10001 + f10002";
|
||||
var args = undefined;
|
||||
var operator = [
|
||||
{ code: "+", name: "Cộng" },
|
||||
{ code: "-", name: "Trừ" },
|
||||
{ code: "*", name: "Nhân" },
|
||||
{ code: "/", name: "Chia" },
|
||||
{ code: ">", name: "Lớn hơn" },
|
||||
{ code: ">=", name: "Lớn hơn hoặc bằng" },
|
||||
{ code: "<", name: "Nhỏ hơn" },
|
||||
{ code: "<=", name: "Nhỏ hơn hoặc bằng" },
|
||||
{ code: "==", name: "Bằng" },
|
||||
{ code: "&&", name: "Và" },
|
||||
{ code: "||", name: "Hoặc" },
|
||||
{ code: "iif", name: "Điều kiện rẽ nhánh" },
|
||||
];
|
||||
function editLabel() {
|
||||
if ($empty(label)) return;
|
||||
showmodal.value = {
|
||||
component: "datatable/EditLabel",
|
||||
width: "500px",
|
||||
height: "300px",
|
||||
vbind: { label: label },
|
||||
};
|
||||
}
|
||||
function close() {
|
||||
showmodal.value = null;
|
||||
}
|
||||
function changeLabel(evt) {
|
||||
label = evt;
|
||||
showmodal.value = null;
|
||||
}
|
||||
function changeType(v) {
|
||||
radioType.value = v;
|
||||
}
|
||||
function addFunc(v) {
|
||||
formula = (formula ? formula + " " : "") + v.name + "(C0: C2)";
|
||||
}
|
||||
function addOperator(v) {
|
||||
let text = v.code === "iif" ? "a>b? c : d" : v.code;
|
||||
formula = `${formula || ""} ${text}`;
|
||||
}
|
||||
function changeFunc(v) {
|
||||
placeholder = `${v.name}(C0:C2) hoặc ${v.name}(C0,C1,C2). C là viết tắt của cột dữ liệu, số thứ tự của cột bắt đầu từ 0`;
|
||||
func = v.code;
|
||||
}
|
||||
function getFields() {
|
||||
fields = pagedata ? $copy(pagedata.fields) : [];
|
||||
fields.map((v) => (v.caption = (v.label ? v.label.indexOf("<") >= 0 : false) ? v.name : v.label));
|
||||
}
|
||||
function checkFunc() {
|
||||
let error = false;
|
||||
let val = formula.trim().replaceAll(" ", "");
|
||||
if (val.toLowerCase().indexOf(func) < 0) error = true;
|
||||
let start = val.toLowerCase().indexOf("(");
|
||||
let end = val.toLowerCase().indexOf(")");
|
||||
if (start < 0 || end < 0) error = true;
|
||||
let content = val.substring(start + 1, end);
|
||||
if ($empty(content)) error = true;
|
||||
let content1 = content.replaceAll(":", ",");
|
||||
let arr = content1.split(",");
|
||||
arr.map((v) => {
|
||||
let arr1 = v.toLowerCase().split("c");
|
||||
if (arr1.length !== 2) error = true;
|
||||
else if (!$isNumber(arr1[1])) error = true;
|
||||
});
|
||||
return error ? "error" : content;
|
||||
}
|
||||
function checkValid() {
|
||||
errors = [];
|
||||
if (tags.length === 0 && choice === "column") {
|
||||
errors.push({
|
||||
name: "tags",
|
||||
message: "Chưa chọn trường xây dựng công thức.",
|
||||
});
|
||||
}
|
||||
if (!$empty(formula) ? $empty(formula.trim()) : true) {
|
||||
errors.push({ name: "formula", message: "Công thức không được bỏ trống." });
|
||||
}
|
||||
if (!$empty(label) ? $empty(label.trim()) : true)
|
||||
errors.push({ name: "label", message: "Mô tả không được bỏ trống." });
|
||||
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
|
||||
errors.push({
|
||||
name: "label",
|
||||
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
|
||||
});
|
||||
}
|
||||
if (errors.length > 0) return false;
|
||||
//check formula in case use column
|
||||
if (choice === "column") {
|
||||
let val = $copy(formula);
|
||||
tags.forEach((v) => {
|
||||
let myRegExp = new RegExp(v.name, "g");
|
||||
val = val.replace(myRegExp, Math.random());
|
||||
});
|
||||
try {
|
||||
let value = $calc(val);
|
||||
if (isNaN(value) || value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY) {
|
||||
errors.push({ name: "formula", message: "Công thức không hợp lệ" });
|
||||
}
|
||||
if(!$empty(formula)? $empty(formula.trim()) : true) {
|
||||
errors.push({name: 'formula', message: 'Công thức không được bỏ trống.'})
|
||||
}
|
||||
if(!$empty(label)? $empty(label.trim()) : true )
|
||||
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
||||
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) {
|
||||
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
||||
}
|
||||
if(errors.length>0) return false
|
||||
//check formula in case use column
|
||||
if(choice==='column') {
|
||||
let val = $copy(formula)
|
||||
tags.forEach(v => {
|
||||
let myRegExp = new RegExp(v.name, 'g')
|
||||
val = val.replace(myRegExp, Math.random())
|
||||
})
|
||||
try {
|
||||
let value = $calc(val)
|
||||
if(isNaN(value) || value===Number.POSITIVE_INFINITY || value===Number.NEGATIVE_INFINITY) {
|
||||
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err)
|
||||
errors.push({name: 'formula', message: 'Công thức không hợp lệ'})
|
||||
}
|
||||
} else {
|
||||
if(checkFunc()==='error') errors.push({name: 'formula', message: `Hàm ${func.toUpperCase()} không hợp lệ`})
|
||||
}
|
||||
return errors.length>0? false : true
|
||||
}
|
||||
function createField() {
|
||||
if(!checkValid()) return
|
||||
let field = $createField(name.trim(), label.trim(), 'number', true)
|
||||
field.formula = formula.trim().replaceAll(' ', '')
|
||||
if(choice==='function') {
|
||||
field.func = func
|
||||
field.vals = checkFunc()
|
||||
} else field.tags = tags.map(v=>v.name)
|
||||
field.level = Math.max(...pagedata.fields.map(v=>v.level? v.level : 0)) + 1
|
||||
field.unit = selectUnit.detail
|
||||
field.decimal = decimal
|
||||
field.disable = 'search,value'
|
||||
let copy = $copy(pagedata)
|
||||
copy.fields.push(field)
|
||||
store.commit(props.pagename, copy)
|
||||
emit('newfield', field)
|
||||
tags = []
|
||||
formula = undefined
|
||||
label = undefined
|
||||
name = `f${$id()}`
|
||||
emit('close')
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
errors.push({ name: "formula", message: "Công thức không hợp lệ" });
|
||||
}
|
||||
function createEmptyField() {
|
||||
errors = []
|
||||
if(!$empty(name)? $empty(name.trim()) : true )
|
||||
errors.push({name: 'name', message: 'Tên không được bỏ trống.'})
|
||||
else if(pagedata.fields.find(v=>v.name.toLowerCase()===name.toLowerCase())) {
|
||||
errors.push({name: 'name', message: 'Tên trường bị trùng. Hãy đặt tên khác.'})
|
||||
}
|
||||
if(!$empty(label)? $empty(label.trim()) : true )
|
||||
errors.push({name: 'label', message: 'Mô tả không được bỏ trống.'})
|
||||
else if(pagedata.fields.find(v=>v.label.toLowerCase()===label.toLowerCase())) {
|
||||
errors.push({name: 'label', message: 'Mô tả bị trùng. Hãy đặt mô tả khác.'})
|
||||
}
|
||||
if(errors.length>0) return
|
||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true)
|
||||
if(selectType.code==='chart') field = createChartField()
|
||||
let copy = $clone(pagedata)
|
||||
copy.fields.push(field)
|
||||
copy.update = {fields: copy.fields}
|
||||
store.commit(props.pagename, copy)
|
||||
//pagedata = copy
|
||||
emit('newfield', field)
|
||||
label = undefined
|
||||
name = `f${$id()}`
|
||||
emit('close')
|
||||
}
|
||||
function createChartField() {
|
||||
let array = pagedata.fields.filter(v=>v.format==='number' && v.show)
|
||||
if(args) array = $copy(args)
|
||||
let text = ''
|
||||
array.map((v,i)=>text += `'${v.name}${i<array.length-1? "'," : "'"}`)
|
||||
let label = ''
|
||||
array.map((v,i)=>label += `'${$stripHtml(v.label)}${i<array.length-1? "'," : "'"}`)
|
||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true)
|
||||
field.chart = 'yes'
|
||||
field.template = `<TrendingChart class="is-clickable" v-bind="{row: row, fields: [${text}], labels: [${label}], width: '80', height: '26', 'header': ['stock_code', 'name']}"/>`
|
||||
return field
|
||||
}
|
||||
//============
|
||||
getFields()
|
||||
</script>
|
||||
} else {
|
||||
if (checkFunc() === "error")
|
||||
errors.push({
|
||||
name: "formula",
|
||||
message: `Hàm ${func.toUpperCase()} không hợp lệ`,
|
||||
});
|
||||
}
|
||||
return errors.length > 0 ? false : true;
|
||||
}
|
||||
function createField() {
|
||||
if (!checkValid()) return;
|
||||
let field = $createField(name.trim(), label.trim(), "number", true);
|
||||
field.formula = formula.trim().replaceAll(" ", "");
|
||||
if (choice === "function") {
|
||||
field.func = func;
|
||||
field.vals = checkFunc();
|
||||
} else field.tags = tags.map((v) => v.name);
|
||||
field.level = Math.max(...pagedata.fields.map((v) => (v.level ? v.level : 0))) + 1;
|
||||
field.unit = selectUnit.detail;
|
||||
field.decimal = decimal;
|
||||
field.disable = "search,value";
|
||||
let copy = $copy(pagedata);
|
||||
copy.fields.push(field);
|
||||
store.commit(props.pagename, copy);
|
||||
emit("newfield", field);
|
||||
tags = [];
|
||||
formula = undefined;
|
||||
label = undefined;
|
||||
name = `f${$id()}`;
|
||||
emit("close");
|
||||
}
|
||||
function createEmptyField() {
|
||||
errors = [];
|
||||
if (!$empty(name) ? $empty(name.trim()) : true) errors.push({ name: "name", message: "Tên không được bỏ trống." });
|
||||
else if (pagedata.fields.find((v) => v.name.toLowerCase() === name.toLowerCase())) {
|
||||
errors.push({
|
||||
name: "name",
|
||||
message: "Tên trường bị trùng. Hãy đặt tên khác.",
|
||||
});
|
||||
}
|
||||
if (!$empty(label) ? $empty(label.trim()) : true)
|
||||
errors.push({ name: "label", message: "Mô tả không được bỏ trống." });
|
||||
else if (pagedata.fields.find((v) => v.label.toLowerCase() === label.toLowerCase())) {
|
||||
errors.push({
|
||||
name: "label",
|
||||
message: "Mô tả bị trùng. Hãy đặt mô tả khác.",
|
||||
});
|
||||
}
|
||||
if (errors.length > 0) return;
|
||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true);
|
||||
if (selectType.code === "chart") field = createChartField();
|
||||
let copy = $clone(pagedata);
|
||||
copy.fields.push(field);
|
||||
copy.update = { fields: copy.fields };
|
||||
store.commit(props.pagename, copy);
|
||||
//pagedata = copy
|
||||
emit("newfield", field);
|
||||
label = undefined;
|
||||
name = `f${$id()}`;
|
||||
emit("close");
|
||||
}
|
||||
function createChartField() {
|
||||
let array = pagedata.fields.filter((v) => v.format === "number" && v.show);
|
||||
if (args) array = $copy(args);
|
||||
let text = "";
|
||||
array.map((v, i) => (text += `'${v.name}${i < array.length - 1 ? "'," : "'"}`));
|
||||
let label = "";
|
||||
array.map((v, i) => (label += `'${$stripHtml(v.label)}${i < array.length - 1 ? "'," : "'"}`));
|
||||
let field = $createField(name.trim(), label.trim(), radioType.value.code, true);
|
||||
field.chart = "yes";
|
||||
field.template = `<TrendingChart class="is-clickable" v-bind="{row: row, fields: [${text}], labels: [${label}], width: '80', height: '26', 'header': ['stock_code', 'name']}"/>`;
|
||||
return field;
|
||||
}
|
||||
//============
|
||||
getFields();
|
||||
</script>
|
||||
|
||||
@@ -1,64 +1,106 @@
|
||||
<template>
|
||||
<nav class="pagination mx-0" role="navigation" aria-label="pagination">
|
||||
<ul class="pagination-list" v-if="pageInfo">
|
||||
<li v-for="v in pageInfo">
|
||||
<a v-if="currentPage===v" class="pagination-link is-current has-background-primary has-text-white" :aria-label="`Page ${v}`" aria-current="page">{{ v }}</a>
|
||||
<a v-else href="#" class="pagination-link" :aria-label="`Goto page ${v}`" @click="changePage(v)">{{ v }}</a>
|
||||
</li>
|
||||
<a @click="previous()" class="pagination-previous ml-5">
|
||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon>
|
||||
</a>
|
||||
<a @click="next()" class="pagination-next">
|
||||
<SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 20, alt: 'Tìm kiếm'}"></SvgIcon>
|
||||
</a>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav
|
||||
class="pagination mx-0"
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
>
|
||||
<ul
|
||||
class="pagination-list"
|
||||
v-if="pageInfo"
|
||||
>
|
||||
<li v-for="v in pageInfo">
|
||||
<a
|
||||
v-if="currentPage === v"
|
||||
class="pagination-link is-current has-background-primary has-text-white"
|
||||
:aria-label="`Page ${v}`"
|
||||
aria-current="page"
|
||||
>{{ v }}</a
|
||||
>
|
||||
<a
|
||||
v-else
|
||||
href="#"
|
||||
class="pagination-link"
|
||||
:aria-label="`Goto page ${v}`"
|
||||
@click="changePage(v)"
|
||||
>{{ v }}</a
|
||||
>
|
||||
</li>
|
||||
<a
|
||||
@click="previous()"
|
||||
class="pagination-previous ml-5"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'left1.svg',
|
||||
type: 'dark',
|
||||
size: 20,
|
||||
alt: 'Tìm kiếm',
|
||||
}"
|
||||
></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
@click="next()"
|
||||
class="pagination-next"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'right.svg',
|
||||
type: 'dark',
|
||||
size: 20,
|
||||
alt: 'Tìm kiếm',
|
||||
}"
|
||||
></SvgIcon>
|
||||
</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
<script setup>
|
||||
const emit = defineEmits(['changepage'])
|
||||
var props = defineProps({
|
||||
data: Array,
|
||||
perPage: Number
|
||||
})
|
||||
var currentPage = 1
|
||||
var totalRows = props.data.length
|
||||
var lastPage = parseInt(totalRows / props.perPage)
|
||||
if(lastPage*props.perPage<totalRows) lastPage += 1
|
||||
var pageInfo = ref()
|
||||
function pages(current_page, last_page, onSides = 2) {
|
||||
// pages
|
||||
let pages = [];
|
||||
// Loop through
|
||||
for (let i = 1; i <= last_page; i++) {
|
||||
// Define offset
|
||||
let offset = (i == 1 || last_page) ? onSides + 1 : onSides;
|
||||
// If added
|
||||
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) ||
|
||||
i == current_page || i == last_page) {
|
||||
pages.push(i);
|
||||
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
|
||||
pages.push('...');
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
const emit = defineEmits(["changepage"]);
|
||||
var props = defineProps({
|
||||
data: Array,
|
||||
perPage: Number,
|
||||
});
|
||||
var currentPage = 1;
|
||||
var totalRows = props.data.length;
|
||||
var lastPage = parseInt(totalRows / props.perPage);
|
||||
if (lastPage * props.perPage < totalRows) lastPage += 1;
|
||||
var pageInfo = ref();
|
||||
function pages(current_page, last_page, onSides = 2) {
|
||||
// pages
|
||||
let pages = [];
|
||||
// Loop through
|
||||
for (let i = 1; i <= last_page; i++) {
|
||||
// Define offset
|
||||
let offset = i == 1 || last_page ? onSides + 1 : onSides;
|
||||
// If added
|
||||
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) || i == current_page || i == last_page) {
|
||||
pages.push(i);
|
||||
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
|
||||
pages.push("...");
|
||||
}
|
||||
function changePage(page) {
|
||||
if(page==='...') return
|
||||
currentPage = page
|
||||
pageInfo.value = pages(page, lastPage, 2)
|
||||
emit('changepage', page)
|
||||
}
|
||||
pageInfo.value = pages(1, lastPage, 2)
|
||||
watch(() => props.data, (newVal, oldVal) => {
|
||||
totalRows = props.data.length
|
||||
lastPage = parseInt(totalRows / props.perPage)
|
||||
if(lastPage*props.perPage<totalRows) lastPage += 1
|
||||
pageInfo.value = pages(1, lastPage, 2)
|
||||
})
|
||||
function previous() {
|
||||
if(currentPage>1) changePage(currentPage-1)
|
||||
}
|
||||
function next() {
|
||||
if(currentPage<lastPage) changePage(currentPage+1)
|
||||
}
|
||||
</script>
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
function changePage(page) {
|
||||
if (page === "...") return;
|
||||
currentPage = page;
|
||||
pageInfo.value = pages(page, lastPage, 2);
|
||||
emit("changepage", page);
|
||||
}
|
||||
pageInfo.value = pages(1, lastPage, 2);
|
||||
watch(
|
||||
() => props.data,
|
||||
(newVal, oldVal) => {
|
||||
totalRows = props.data.length;
|
||||
lastPage = parseInt(totalRows / props.perPage);
|
||||
if (lastPage * props.perPage < totalRows) lastPage += 1;
|
||||
pageInfo.value = pages(1, lastPage, 2);
|
||||
},
|
||||
);
|
||||
function previous() {
|
||||
if (currentPage > 1) changePage(currentPage - 1);
|
||||
}
|
||||
function next() {
|
||||
if (currentPage < lastPage) changePage(currentPage + 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,79 @@
|
||||
<template>
|
||||
<div class="px-2" :style="`max-height: ${maxheight}; overflow-y: auto;`">
|
||||
<div
|
||||
v-for="(v, i) in rows" :key="i"
|
||||
:class="[
|
||||
'field is-grouped py-1 my-0',
|
||||
i !== rows.length - 1 && 'border-bottom'
|
||||
]"
|
||||
<div
|
||||
class="px-2"
|
||||
:style="`max-height: ${maxheight}; overflow-y: auto;`"
|
||||
>
|
||||
<div
|
||||
v-for="(v, i) in rows"
|
||||
:key="i"
|
||||
:class="['field is-grouped py-1 my-0', i !== rows.length - 1 && 'border-bottom']"
|
||||
>
|
||||
<p class="control is-expanded py-0 fs-14 hyperlink" @click="doClick(v,i)">
|
||||
{{ $stripHtml(v[name] || v.fullname || v.code || 'n/a', 75) }}
|
||||
<span class="icon has-text-primary" v-if="checked[i] && notick!==true">
|
||||
<SvgIcon v-bind="{name: 'tick.svg', type: 'primary', size: 15}"></SvgIcon>
|
||||
<p
|
||||
class="control is-expanded py-0 fs-14 hyperlink"
|
||||
@click="doClick(v, i)"
|
||||
>
|
||||
{{ $stripHtml(v[name] || v.fullname || v.code || "n/a", 75) }}
|
||||
<span
|
||||
class="icon has-text-primary"
|
||||
v-if="checked[i] && notick !== true"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'tick.svg', type: 'primary', size: 15 }"></SvgIcon>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control py-0" v-if="show">
|
||||
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.author">
|
||||
<SvgIcon v-bind="{name: 'user.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
<p
|
||||
class="control py-0"
|
||||
v-if="show"
|
||||
>
|
||||
<span
|
||||
class="icon-text has-text-grey mr-2 fs-13"
|
||||
v-if="show.author"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'user.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||
<span>{{ v[show.author] }}</span>
|
||||
</span>
|
||||
<span class="icon-text has-text-grey mr-2 fs-13" v-if="show.view">
|
||||
<SvgIcon v-bind="{name: 'view.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
<span
|
||||
class="icon-text has-text-grey mr-2 fs-13"
|
||||
v-if="show.view"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'view.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||
<span>{{ v[show.view] }}</span>
|
||||
</span>
|
||||
<span class="fs-13 has-text-grey" v-if="show.time">{{$dayjs(v['create_time']).fromNow(true)}}</span>
|
||||
<span class="tooltip">
|
||||
<a class="icon ml-1" v-if="show.link" @click="doClick(v,i, 'newtab')">
|
||||
<SvgIcon v-bind="{name: 'opennew.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
<span
|
||||
class="fs-13 has-text-grey"
|
||||
v-if="show.time"
|
||||
>{{ $dayjs(v["create_time"]).fromNow(true) }}</span
|
||||
>
|
||||
<span class="tooltip">
|
||||
<a
|
||||
class="icon ml-1"
|
||||
v-if="show.link"
|
||||
@click="doClick(v, i, 'newtab')"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'opennew.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext">Mở trong tab mớ</span>
|
||||
</span>
|
||||
<span class="tooltip" v-if="show.rename">
|
||||
<a class="icon ml-1" @click="$emit('rename', v, i)">
|
||||
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
<span
|
||||
class="tooltip"
|
||||
v-if="show.rename"
|
||||
>
|
||||
<a
|
||||
class="icon ml-1"
|
||||
@click="$emit('rename', v, i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext">Đổi tên</span>
|
||||
</span>
|
||||
<span class="tooltip" v-if="show.rename">
|
||||
<a class="icon has-text-danger ml-1" @click="$emit('remove', v, i)">
|
||||
<SvgIcon v-bind="{name: 'bin1.svg', type: 'gray', size: 15}"></SvgIcon>
|
||||
<span
|
||||
class="tooltip"
|
||||
v-if="show.rename"
|
||||
>
|
||||
<a
|
||||
class="icon has-text-danger ml-1"
|
||||
@click="$emit('remove', v, i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'gray', size: 15 }"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext">Xóa</span>
|
||||
</span>
|
||||
@@ -47,7 +83,7 @@
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['data', 'name', 'maxheight', 'perpage', 'sort', 'selects', 'keyval', 'show', 'notick'],
|
||||
props: ["data", "name", "maxheight", "perpage", "sort", "selects", "keyval", "show", "notick"],
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
@@ -56,62 +92,64 @@ export default {
|
||||
selected: [],
|
||||
checked: {},
|
||||
time: undefined,
|
||||
array: []
|
||||
}
|
||||
array: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getdata()
|
||||
this.getdata();
|
||||
},
|
||||
watch: {
|
||||
data: function(newVal) {
|
||||
this.getdata()
|
||||
data: function (newVal) {
|
||||
this.getdata();
|
||||
},
|
||||
selects: function (newVal) {
|
||||
this.getSelect();
|
||||
},
|
||||
selects: function(newVal) {
|
||||
this.getSelect()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getdata() {
|
||||
this.currentPage = 1
|
||||
this.array = this.$copy(this.data)
|
||||
if(this.sort!==false) {
|
||||
let f = {}
|
||||
let showtime = this.show? this.show.time : false
|
||||
showtime? f['create_time'] = 'desc' : f[this.name] = 'asc'
|
||||
this.$multiSort(this.array, f)
|
||||
this.currentPage = 1;
|
||||
this.array = this.$copy(this.data);
|
||||
if (this.sort !== false) {
|
||||
let f = {};
|
||||
let showtime = this.show ? this.show.time : false;
|
||||
showtime ? (f["create_time"] = "desc") : (f[this.name] = "asc");
|
||||
this.$multiSort(this.array, f);
|
||||
}
|
||||
this.rows = this.array.slice(0, this.perpage)
|
||||
this.getSelect()
|
||||
this.rows = this.array.slice(0, this.perpage);
|
||||
this.getSelect();
|
||||
},
|
||||
getSelect() {
|
||||
if(!this.selects) return
|
||||
this.selected = []
|
||||
this.checked = {}
|
||||
this.selects.map(v=>{
|
||||
let idx = this.rows.findIndex(x=>x[this.keyval? this.keyval : this.name]===v)
|
||||
if(idx>=0) {
|
||||
this.selected.push(this.rows[idx])
|
||||
this.checked[idx] = true
|
||||
if (!this.selects) return;
|
||||
this.selected = [];
|
||||
this.checked = {};
|
||||
this.selects.map((v) => {
|
||||
let idx = this.rows.findIndex((x) => x[this.keyval ? this.keyval : this.name] === v);
|
||||
if (idx >= 0) {
|
||||
this.selected.push(this.rows[idx]);
|
||||
this.checked[idx] = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
doClick(v, i, type) {
|
||||
this.checked[i] = this.checked[i]? false : true
|
||||
this.checked = this.$copy(this.checked)
|
||||
let idx = this.selected.findIndex(x=>x.id===v.id)
|
||||
idx>=0? this.$remove(this.selected) : this.selected.push(v)
|
||||
this.$emit('selected', v, type)
|
||||
this.checked[i] = this.checked[i] ? false : true;
|
||||
this.checked = this.$copy(this.checked);
|
||||
let idx = this.selected.findIndex((x) => x.id === v.id);
|
||||
idx >= 0 ? this.$remove(this.selected) : this.selected.push(v);
|
||||
this.$emit("selected", v, type);
|
||||
},
|
||||
handleScroll(e) {
|
||||
const bottom = e.target.scrollHeight - e.target.scrollTop -5 < e.target.clientHeight
|
||||
if (bottom) {
|
||||
if(this.total? this.total>this.rows.length : true) {
|
||||
this.currentPage +=1
|
||||
let arr = this.array.filter((ele,index) => (index>=(this.currentPage-1)*this.perpage && index<this.currentPage*this.perpage))
|
||||
this.rows = this.rows.concat(arr)
|
||||
const bottom = e.target.scrollHeight - e.target.scrollTop - 5 < e.target.clientHeight;
|
||||
if (bottom) {
|
||||
if (this.total ? this.total > this.rows.length : true) {
|
||||
this.currentPage += 1;
|
||||
let arr = this.array.filter(
|
||||
(ele, index) => index >= (this.currentPage - 1) * this.perpage && index < this.currentPage * this.perpage,
|
||||
);
|
||||
this.rows = this.rows.concat(arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,17 +9,30 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="fs-14" v-for="(v, i) in fields">
|
||||
<tr
|
||||
class="fs-14"
|
||||
v-for="(v, i) in fields"
|
||||
>
|
||||
<td>{{ i }}</td>
|
||||
<td>
|
||||
<a class="has-text-primary" @click="openField(v, i)">{{ v.name }}</a>
|
||||
<a
|
||||
class="has-text-primary"
|
||||
@click="openField(v, i)"
|
||||
>{{ v.name }}</a
|
||||
>
|
||||
</td>
|
||||
<td>{{ $stripHtml(v.label, 50) }}</td>
|
||||
<td>
|
||||
<a class="mr-4" @click="moveDown(v, i)">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="moveDown(v, i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'down1.png', type: 'dark', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a class="mr-4" @click="moveUp(v, i)">
|
||||
<a
|
||||
class="mr-4"
|
||||
@click="moveUp(v, i)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'up.png', type: 'dark', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a @click="askConfirm(v, i)">
|
||||
@@ -29,7 +42,13 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Modal @close="showmodal = undefined" @update="update" @confirm="remove" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
@update="update"
|
||||
@confirm="remove"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
@@ -1,112 +1,144 @@
|
||||
<template>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Cỡ chữ của bảng <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='table-font-size').detail"
|
||||
@change="changeSetting($event.target.value, 'table-font-size')">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field" >
|
||||
<label class="label fs-14"> Cỡ chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='header-font-size').detail"
|
||||
@change="changeSetting($event.target.value, 'header-font-size')">
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Số dòng trên 1 trang <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text" :value="tablesetting.find(v=>v.code==='per-page').detail"
|
||||
@change="changeSetting($event.target.value, 'per-page')">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-5">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu nền bảng <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='table-background').detail"
|
||||
@change="changeSetting($event.target.value, 'table-background')">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='table-font-color').detail"
|
||||
@change="changeSetting($event.target.value, 'table-font-color')">
|
||||
</p>
|
||||
<label class="label fs-14"> Cỡ chữ của bảng <span class="has-text-danger"> * </span></label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
:value="tablesetting.find((v) => v.code === 'table-font-size').detail"
|
||||
@change="changeSetting($event.target.value, 'table-font-size')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Cỡ chữ tiêu đề <span class="has-text-danger"> * </span></label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
:value="tablesetting.find((v) => v.code === 'header-font-size').detail"
|
||||
@change="changeSetting($event.target.value, 'header-font-size')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Số dòng trên 1 trang <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
:value="tablesetting.find((v) => v.code === 'per-page').detail"
|
||||
@change="changeSetting($event.target.value, 'per-page')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-5">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-font-color').detail"
|
||||
@change="changeSetting($event.target.value, 'header-font-color')">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu nền tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-background').detail"
|
||||
@change="changeSetting($event.target.value, 'header-background')">
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ khi filter<span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input type="color" :value="tablesetting.find(v=>v.code==='header-filter-color').detail"
|
||||
@change="changeSetting($event.target.value, 'header-filter-color')">
|
||||
</p>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu nền bảng <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
type="color"
|
||||
:value="tablesetting.find((v) => v.code === 'table-background').detail"
|
||||
@change="changeSetting($event.target.value, 'table-background')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
type="color"
|
||||
:value="tablesetting.find((v) => v.code === 'table-font-color').detail"
|
||||
@change="changeSetting($event.target.value, 'table-font-color')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-5">
|
||||
<div class="field-body">
|
||||
<div class="field" >
|
||||
<label class="label fs-14"> Đường viền <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input class="input is-small" type="text"
|
||||
:value="tablesetting.find(v=>v.code==='td-border')? tablesetting.find(v=>v.code==='td-border').detail : undefined"
|
||||
@change="changeSetting($event.target.value, 'td-border')">
|
||||
</p>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
type="color"
|
||||
:value="tablesetting.find((v) => v.code === 'header-font-color').detail"
|
||||
@change="changeSetting($event.target.value, 'header-font-color')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu nền tiêu đề <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
type="color"
|
||||
:value="tablesetting.find((v) => v.code === 'header-background').detail"
|
||||
@change="changeSetting($event.target.value, 'header-background')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Màu chữ khi filter<span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
type="color"
|
||||
:value="tablesetting.find((v) => v.code === 'header-filter-color').detail"
|
||||
@change="changeSetting($event.target.value, 'header-filter-color')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal mt-5">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label fs-14"> Đường viền <span class="has-text-danger"> * </span> </label>
|
||||
<p class="control fs-14">
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
:value="
|
||||
tablesetting.find((v) => v.code === 'td-border')
|
||||
? tablesetting.find((v) => v.code === 'td-border').detail
|
||||
: undefined
|
||||
"
|
||||
@change="changeSetting($event.target.value, 'td-border')"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useStore } from '@/stores/index'
|
||||
const store = useStore()
|
||||
var props = defineProps({
|
||||
pagename: String
|
||||
})
|
||||
const { $copy, $clone, $empty } = useNuxtApp()
|
||||
var pagedata = $clone(store[props.pagename])
|
||||
var errors = []
|
||||
var radioNote = 'no'
|
||||
var tablesetting = pagedata.tablesetting
|
||||
let found = tablesetting.find(v=>v.code==='note')
|
||||
if(found? found.detail!=='@' : false) radioNote = 'yes'
|
||||
function changeSetting(value, code) {
|
||||
if(code==='note' && $empty(value)) return
|
||||
let copy = $copy(tablesetting)
|
||||
let found = copy.find(v=>v.code===code)
|
||||
if(found) found.detail = value
|
||||
else {
|
||||
found = $copy(tablesetting.find(v=>v.code===code))
|
||||
found.detail = value
|
||||
copy.push(found)
|
||||
}
|
||||
tablesetting = copy
|
||||
pagedata.tablesetting = tablesetting
|
||||
store.commit(props.pagename, pagedata)
|
||||
import { useStore } from "@/stores/index";
|
||||
const store = useStore();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
});
|
||||
const { $copy, $clone, $empty } = useNuxtApp();
|
||||
var pagedata = $clone(store[props.pagename]);
|
||||
var errors = [];
|
||||
var radioNote = "no";
|
||||
var tablesetting = pagedata.tablesetting;
|
||||
let found = tablesetting.find((v) => v.code === "note");
|
||||
if (found ? found.detail !== "@" : false) radioNote = "yes";
|
||||
function changeSetting(value, code) {
|
||||
if (code === "note" && $empty(value)) return;
|
||||
let copy = $copy(tablesetting);
|
||||
let found = copy.find((v) => v.code === code);
|
||||
if (found) found.detail = value;
|
||||
else {
|
||||
found = $copy(tablesetting.find((v) => v.code === code));
|
||||
found.detail = value;
|
||||
copy.push(found);
|
||||
}
|
||||
</script>
|
||||
tablesetting = copy;
|
||||
pagedata.tablesetting = tablesetting;
|
||||
store.commit(props.pagename, pagedata);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,92 +1,157 @@
|
||||
<template>
|
||||
<div class="pb-1" style="border-bottom: 2px solid #3c5b63" v-if="array || !enableTime">
|
||||
<div
|
||||
class="pb-1"
|
||||
style="border-bottom: 2px solid #3c5b63"
|
||||
v-if="array || !enableTime"
|
||||
>
|
||||
<div class="columns mx-0 mb-0">
|
||||
<div class="column is-8 px-0 pb-0" v-if="enableTime">
|
||||
<div
|
||||
class="column is-8 px-0 pb-0"
|
||||
v-if="enableTime"
|
||||
>
|
||||
<div class="field is-grouped is-grouped-multiline mb-0">
|
||||
<div class="control mb-0">
|
||||
<Caption v-bind="{ title: lang === 'vi' ? 'Thời gian' : 'Time', type: 'has-text-warning' }" />
|
||||
<Caption
|
||||
v-bind="{
|
||||
title: lang === 'vi' ? 'Thời gian' : 'Time',
|
||||
type: 'has-text-warning',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div class="control mb-0" v-for="v in array" :key="v.code">
|
||||
<span class="icon-text fsb-16 has-text-warning px-1" v-if="v.code === current">
|
||||
<div
|
||||
class="control mb-0"
|
||||
v-for="v in array"
|
||||
:key="v.code"
|
||||
>
|
||||
<span
|
||||
class="icon-text fsb-16 has-text-warning px-1"
|
||||
v-if="v.code === current"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'tick.png', size: 20 }"></SvgIcon>
|
||||
<span>{{ v.name }}</span>
|
||||
</span>
|
||||
<span class="icon-text has-text-grey hyperlink px-1 fsb-16" @click="changeOption(v)" v-else>{{
|
||||
v.name
|
||||
}}</span>
|
||||
<span
|
||||
class="icon-text has-text-grey hyperlink px-1 fsb-16"
|
||||
@click="changeOption(v)"
|
||||
v-else
|
||||
>{{ v.name }}</span
|
||||
>
|
||||
</div>
|
||||
<span v-if="newDataAvailable" class="has-text-danger is-italic is-size-6 ml-2">Có dữ liệu mới, vui lòng làm
|
||||
mới.</span>
|
||||
<span
|
||||
v-if="newDataAvailable"
|
||||
class="has-text-danger is-italic is-size-6 ml-2"
|
||||
>Có dữ liệu mới, vui lòng làm mới.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4 px-0">
|
||||
<div class="field is-grouped is-grouped-multiline mb-0">
|
||||
<div class="control mb-0">
|
||||
<Caption v-bind="{
|
||||
type: 'has-text-warning',
|
||||
title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search',
|
||||
}" />
|
||||
<Caption
|
||||
v-bind="{
|
||||
type: 'has-text-warning',
|
||||
title: lang === 'vi' ? `Tìm ${viewport === 1 ? '' : 'kiếm'}` : 'Search',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div class="control mb-0">
|
||||
<input class="input is-small" type="text" v-model="text"
|
||||
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`" @keyup="startSearch"
|
||||
id="input" :placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'" />
|
||||
<input
|
||||
class="input is-small"
|
||||
type="text"
|
||||
v-model="text"
|
||||
:style="`${viewport === 1 ? 'width:150px;' : ''} border: 1px solid #BEBEBE;`"
|
||||
@keyup="startSearch"
|
||||
id="input"
|
||||
:placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'"
|
||||
/>
|
||||
</div>
|
||||
<div class="control mb-0">
|
||||
<span class="tooltip" v-if="importdata && $getEditRights()">
|
||||
<a class="mr-2" @click="openImport()">
|
||||
<SvgIcon v-bind="{
|
||||
name: 'upload.svg',
|
||||
type: 'findata',
|
||||
size: 22
|
||||
}"></SvgIcon>
|
||||
<span
|
||||
class="tooltip"
|
||||
v-if="importdata && $getEditRights()"
|
||||
>
|
||||
<a
|
||||
class="mr-2"
|
||||
@click="openImport()"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'upload.svg',
|
||||
type: 'findata',
|
||||
size: 22,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext" style="min-width: max-content">
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="min-width: max-content"
|
||||
>
|
||||
{{ lang === "vi" ? "Nhập dữ liệu" : "Import data" }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="tooltip" v-if="enableAdd && $getEditRights()">
|
||||
<a class="mr-2" @click="$emit('add')">
|
||||
<span
|
||||
class="tooltip"
|
||||
v-if="enableAdd && $getEditRights()"
|
||||
>
|
||||
<a
|
||||
class="mr-2"
|
||||
@click="$emit('add')"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'findata', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext" style="min-width: max-content">{{
|
||||
lang === "vi" ? "Thêm mới" : "Add new"
|
||||
}}</span>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="min-width: max-content"
|
||||
>{{ lang === "vi" ? "Thêm mới" : "Add new" }}</span
|
||||
>
|
||||
</span>
|
||||
<span class="tooltip">
|
||||
<a class="mr-2" @click="$emit('excel')">
|
||||
<a
|
||||
class="mr-2"
|
||||
@click="$emit('excel')"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'excel.png', type: 'findata', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext" style="min-width: max-content">{{
|
||||
lang === "vi" ? "Xuất excel" : "Export excel"
|
||||
}}</span>
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="min-width: max-content"
|
||||
>{{ lang === "vi" ? "Xuất excel" : "Export excel" }}</span
|
||||
>
|
||||
</span>
|
||||
|
||||
<span class="tooltip">
|
||||
<a @click="$emit('refresh-data')">
|
||||
<SvgIcon v-bind="{ name: 'refresh.svg', type: 'findata', size: 22 }"></SvgIcon>
|
||||
</a>
|
||||
<span class="tooltiptext" style="min-width: max-content">{{
|
||||
lang === "vi" ? "Làm mới" : "Refresh"
|
||||
}}</span>
|
||||
|
||||
<span
|
||||
class="tooltiptext"
|
||||
style="min-width: max-content"
|
||||
>{{ lang === "vi" ? "Làm mới" : "Refresh" }}</span
|
||||
>
|
||||
</span>
|
||||
<a class="button is-primary is-loading is-small ml-3" v-if="loading"></a>
|
||||
<a
|
||||
class="button is-primary is-loading is-small ml-3"
|
||||
v-if="loading"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useStore } from "@/stores/index"
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const store = useStore()
|
||||
return { store }
|
||||
const store = useStore();
|
||||
return { store };
|
||||
},
|
||||
|
||||
props: ["pagename", "api", "timeopt", "filter", "realtime", "newDataAvailable", "params", "importdata"],
|
||||
@@ -117,19 +182,19 @@ export default {
|
||||
pagedata: undefined,
|
||||
loading: false,
|
||||
pollingInterval: null,
|
||||
searchableFields: [] // Lưu thông tin các field có thể tìm kiếm
|
||||
}
|
||||
searchableFields: [], // Lưu thông tin các field có thể tìm kiếm
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
pagename(newVal) {
|
||||
this.updateSearchableFields()
|
||||
}
|
||||
this.updateSearchableFields();
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
// Cập nhật searchable fields ngay từ đầu
|
||||
this.updateSearchableFields()
|
||||
this.updateSearchableFields();
|
||||
|
||||
if (this.viewport < 5) {
|
||||
this.options = [
|
||||
@@ -139,85 +204,83 @@ export default {
|
||||
{ code: 30, name: "1M" },
|
||||
{ code: 90, name: "3M" },
|
||||
{ code: 36000, name: "Tất cả" },
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
this.checkTimeopt()
|
||||
if (!this.enableTime) return this.$emit("option")
|
||||
this.checkTimeopt();
|
||||
if (!this.enableTime) return this.$emit("option");
|
||||
|
||||
let found = this.$findapi(this.api)
|
||||
found.commit = undefined
|
||||
let found = this.$findapi(this.api);
|
||||
found.commit = undefined;
|
||||
|
||||
let filter = this.$copy(this.filter)
|
||||
let filter = this.$copy(this.filter);
|
||||
if (filter) {
|
||||
//dynamic parameter
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (value.toString().indexOf("$") >= 0) {
|
||||
filter[key] = this.store[value.replace("$", "")].id
|
||||
filter[key] = this.store[value.replace("$", "")].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found.params.filter) {
|
||||
if (!filter) filter = {}
|
||||
if (!filter) filter = {};
|
||||
for (const [key, value] of Object.entries(found.params.filter)) {
|
||||
filter[key] = value
|
||||
filter[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.options.map((v) => {
|
||||
let f = filter ? this.$copy(filter) : {}
|
||||
f["create_time__date__gte"] = this.$dayjs()
|
||||
.subtract(v.code, "day")
|
||||
.format("YYYY-MM-DD")
|
||||
v.filter = f
|
||||
})
|
||||
let f = filter ? this.$copy(filter) : {};
|
||||
f["create_time__date__gte"] = this.$dayjs().subtract(v.code, "day").format("YYYY-MM-DD");
|
||||
v.filter = f;
|
||||
});
|
||||
|
||||
this.$emit("option", this.$find(this.options, { code: this.current }))
|
||||
this.$emit("option", this.$find(this.options, { code: this.current }));
|
||||
|
||||
let f = {}
|
||||
let f = {};
|
||||
this.options.map((v) => {
|
||||
f[`${v.code}`] = {
|
||||
type: "Count",
|
||||
field: "create_time__date",
|
||||
filter: v.filter
|
||||
}
|
||||
})
|
||||
filter: v.filter,
|
||||
};
|
||||
});
|
||||
|
||||
let params = { summary: "aggregate", distinct_values: f }
|
||||
found.params = params
|
||||
let params = { summary: "aggregate", distinct_values: f };
|
||||
found.params = params;
|
||||
|
||||
try {
|
||||
let rs = await this.$getapi([found])
|
||||
let rs = await this.$getapi([found]);
|
||||
for (const [key, value] of Object.entries(rs[0].data.rows)) {
|
||||
let found = this.$find(this.options, { code: Number(key) })
|
||||
let found = this.$find(this.options, { code: Number(key) });
|
||||
if (found) {
|
||||
found.name = `${found.name} (${value})`
|
||||
found.name = `${found.name} (${value})`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error)
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
|
||||
this.array = this.$copy(this.options)
|
||||
this.array = this.$copy(this.options);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.realtime) {
|
||||
const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000
|
||||
this.pollingInterval = setInterval(this.refresh, interval)
|
||||
const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000;
|
||||
this.pollingInterval = setInterval(this.refresh, interval);
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
if (this.pollingInterval) {
|
||||
clearInterval(this.pollingInterval)
|
||||
clearInterval(this.pollingInterval);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
lang: function () {
|
||||
return this.store.lang
|
||||
return this.store.lang;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -226,140 +289,141 @@ export default {
|
||||
updateSearchableFields() {
|
||||
try {
|
||||
// Lấy API config
|
||||
const found = this.$findapi(this.api)
|
||||
const found = this.$findapi(this.api);
|
||||
|
||||
if (!found) {
|
||||
console.warn('Không tìm thấy API config')
|
||||
this.choices = []
|
||||
this.searchableFields = []
|
||||
return
|
||||
console.warn("Không tìm thấy API config");
|
||||
this.choices = [];
|
||||
this.searchableFields = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Ưu tiên lấy values từ props.params, nếu không có thì lấy từ API config
|
||||
let valuesString = ''
|
||||
let valuesString = "";
|
||||
|
||||
if (this.params && this.params.values) {
|
||||
// Lấy từ props.params (ưu tiên cao nhất)
|
||||
valuesString = this.params.values
|
||||
valuesString = this.params.values;
|
||||
} else if (found.params && found.params.values) {
|
||||
// Lấy từ API config mặc định
|
||||
valuesString = found.params.values
|
||||
valuesString = found.params.values;
|
||||
} else {
|
||||
console.warn('Không tìm thấy API values trong props hoặc config')
|
||||
this.choices = []
|
||||
this.searchableFields = []
|
||||
return
|
||||
console.warn("Không tìm thấy API values trong props hoặc config");
|
||||
this.choices = [];
|
||||
this.searchableFields = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse values string từ API
|
||||
let fieldNames = valuesString.split(',').map(v => v.trim())
|
||||
let fieldNames = valuesString.split(",").map((v) => v.trim());
|
||||
// Lấy pagedata để lấy label
|
||||
this.pagedata = this.store[this.pagename]
|
||||
this.pagedata = this.store[this.pagename];
|
||||
|
||||
// Lấy tất cả các field để search (không lọc format)
|
||||
const searchable = fieldNames.filter(fieldName => {
|
||||
const searchable = fieldNames.filter((fieldName) => {
|
||||
// Loại bỏ các field kỹ thuật
|
||||
if (fieldName === 'id' ||
|
||||
fieldName === 'create_time' ||
|
||||
fieldName === 'update_time' ||
|
||||
fieldName === 'created_at' ||
|
||||
fieldName === 'updated_at') {
|
||||
return false
|
||||
if (
|
||||
fieldName === "id" ||
|
||||
fieldName === "create_time" ||
|
||||
fieldName === "update_time" ||
|
||||
fieldName === "created_at" ||
|
||||
fieldName === "updated_at"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true;
|
||||
});
|
||||
|
||||
// Lấy tên và label các field
|
||||
this.choices = searchable
|
||||
this.searchableFields = searchable.map(fieldName => {
|
||||
this.choices = searchable;
|
||||
this.searchableFields = searchable.map((fieldName) => {
|
||||
// Lấy field base name (trước dấu __)
|
||||
const baseFieldName = fieldName.split('__')[0]
|
||||
const fieldInfo = this.pagedata && this.pagedata.fields
|
||||
? this.pagedata.fields.find(f => f.name === baseFieldName)
|
||||
: null
|
||||
const baseFieldName = fieldName.split("__")[0];
|
||||
const fieldInfo =
|
||||
this.pagedata && this.pagedata.fields ? this.pagedata.fields.find((f) => f.name === baseFieldName) : null;
|
||||
|
||||
return {
|
||||
name: fieldName,
|
||||
label: fieldInfo ? fieldInfo.label : fieldName
|
||||
}
|
||||
})
|
||||
label: fieldInfo ? fieldInfo.label : fieldName,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating searchable fields:', error)
|
||||
this.choices = []
|
||||
this.searchableFields = []
|
||||
console.error("Error updating searchable fields:", error);
|
||||
this.choices = [];
|
||||
this.searchableFields = [];
|
||||
}
|
||||
},
|
||||
|
||||
refresh() {
|
||||
let found = this.$find(this.options, { code: this.current })
|
||||
this.changeOption(found)
|
||||
let found = this.$find(this.options, { code: this.current });
|
||||
this.changeOption(found);
|
||||
},
|
||||
|
||||
changeOption(v) {
|
||||
this.current = v.code
|
||||
this.current = v.code;
|
||||
if (this.search) {
|
||||
this.text = undefined
|
||||
this.search = undefined
|
||||
this.text = undefined;
|
||||
this.search = undefined;
|
||||
}
|
||||
this.$emit("option", this.$find(this.array, { code: this.current }))
|
||||
this.$emit("option", this.$find(this.array, { code: this.current }));
|
||||
},
|
||||
|
||||
doSearch() {
|
||||
// Cập nhật choices trước khi search
|
||||
this.updateSearchableFields()
|
||||
this.updateSearchableFields();
|
||||
|
||||
this.pagedata = this.store[this.pagename]
|
||||
this.pagedata = this.store[this.pagename];
|
||||
|
||||
if (!this.pagedata || !this.pagedata.fields) {
|
||||
console.warn('Không có pagedata hoặc fields')
|
||||
return
|
||||
console.warn("Không có pagedata hoặc fields");
|
||||
return;
|
||||
}
|
||||
|
||||
let fields = this.pagedata.fields.filter(
|
||||
(v) => this.choices.findIndex((x) => x === v.name) >= 0
|
||||
)
|
||||
let fields = this.pagedata.fields.filter((v) => this.choices.findIndex((x) => x === v.name) >= 0);
|
||||
|
||||
if (fields.length === 0) {
|
||||
console.warn('Không tìm thấy field để tìm kiếm')
|
||||
return
|
||||
console.warn("Không tìm thấy field để tìm kiếm");
|
||||
return;
|
||||
}
|
||||
|
||||
let f = {}
|
||||
let f = {};
|
||||
fields.map((v) => {
|
||||
f[`${v.name}__icontains`] = this.search
|
||||
})
|
||||
f[`${v.name}__icontains`] = this.search;
|
||||
});
|
||||
|
||||
this.$emit("option", { filter_or: f })
|
||||
this.$emit("option", { filter_or: f });
|
||||
},
|
||||
|
||||
openImport() {
|
||||
if (!this.importdata) return
|
||||
if (!this.importdata) return;
|
||||
// Emit event lên parent (DataView)
|
||||
this.$emit('import', this.importdata)
|
||||
this.$emit("import", this.importdata);
|
||||
},
|
||||
|
||||
startSearch(val) {
|
||||
this.search = this.$empty(val.target.value)
|
||||
? ""
|
||||
: val.target.value.trim()
|
||||
this.search = this.$empty(val.target.value) ? "" : val.target.value.trim();
|
||||
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.doSearch(), 300)
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => this.doSearch(), 300);
|
||||
},
|
||||
|
||||
checkTimeopt() {
|
||||
if (this.timeopt > 0) {
|
||||
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt) })
|
||||
if (obj) this.current = obj.code
|
||||
let obj = this.$find(this.options, {
|
||||
code: this.$formatNumber(this.timeopt),
|
||||
});
|
||||
if (obj) this.current = obj.code;
|
||||
}
|
||||
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return
|
||||
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false
|
||||
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false
|
||||
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return;
|
||||
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false;
|
||||
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false;
|
||||
if (this.timeopt.time) {
|
||||
let obj = this.$find(this.options, { code: this.$formatNumber(this.timeopt.time) })
|
||||
if (obj) this.current = obj.code
|
||||
let obj = this.$find(this.options, {
|
||||
code: this.$formatNumber(this.timeopt.time),
|
||||
});
|
||||
if (obj) this.current = obj.code;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
text: { type: String, required: true },
|
||||
color: { type: String, default: "#000" }
|
||||
})
|
||||
color: { type: String, default: "#000" },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<span :style="color? `color:${color}` : ''">{{ $dayjs(date).format('DD/MM/YYYY') }}</span>
|
||||
<span :style="color ? `color:${color}` : ''">{{ $dayjs(date).format("DD/MM/YYYY") }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
const { $dayjs } = useNuxtApp()
|
||||
const { $dayjs } = useNuxtApp();
|
||||
const props = defineProps({
|
||||
date: String,
|
||||
color: String
|
||||
})
|
||||
</script>
|
||||
color: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<span :style="color? `color:${color}` : ''">{{ value === 0 || value === null ? '-' : $numtoString(value) }}</span>
|
||||
<span :style="color ? `color:${color}` : ''">{{ value === 0 || value === null ? "-" : $numtoString(value) }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
const { $numtoString } = useNuxtApp()
|
||||
const { $numtoString } = useNuxtApp();
|
||||
const props = defineProps({
|
||||
value: Number,
|
||||
color: String
|
||||
})
|
||||
</script>
|
||||
color: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<span :style="color? `color:${color}` : ''">{{ $numtoString(value) }}</span>
|
||||
<span :style="color ? `color:${color}` : ''">{{ $numtoString(value) }}</span>
|
||||
</template>
|
||||
<script setup>
|
||||
const { $numtoString } = useNuxtApp()
|
||||
const { $numtoString } = useNuxtApp();
|
||||
const props = defineProps({
|
||||
value: Number,
|
||||
color: String
|
||||
})
|
||||
</script>
|
||||
color: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,54 @@
|
||||
<template>
|
||||
<div class="control has-icons-left" :id="docid">
|
||||
<div :class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`" style="width: 100%">
|
||||
<div class="dropdown-trigger" style="width: 100%;">
|
||||
<input :disabled="disabled" :class="`input ${error? 'is-danger' : ''} ${disabled? 'has-text-dark' : ''}`" type="text" placeholder="DD/MM/YYYY"
|
||||
maxlength="10" @focus="setFocus" @blur="lostFocus" @keyup.enter="pressEnter" @keyup="checkDate" v-model="show" />
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu" @click="doClick()">
|
||||
<div class="dropdown-content">
|
||||
<PickDay v-bind="{ date, maxdate }" @date="selectDate"></PickDay>
|
||||
<div
|
||||
class="control has-icons-left"
|
||||
:id="docid"
|
||||
>
|
||||
<div
|
||||
:class="`dropdown ${pos || ''} ${focused ? 'is-active' : ''}`"
|
||||
style="width: 100%"
|
||||
>
|
||||
<div
|
||||
class="dropdown-trigger"
|
||||
style="width: 100%"
|
||||
>
|
||||
<input
|
||||
:disabled="disabled"
|
||||
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-dark' : ''}`"
|
||||
type="text"
|
||||
placeholder="DD/MM/YYYY"
|
||||
maxlength="10"
|
||||
@focus="setFocus"
|
||||
@blur="lostFocus"
|
||||
@keyup.enter="pressEnter"
|
||||
@keyup="checkDate"
|
||||
v-model="show"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
role="menu"
|
||||
@click="doClick()"
|
||||
>
|
||||
<div class="dropdown-content">
|
||||
<PickDay
|
||||
v-bind="{ date, maxdate }"
|
||||
@date="selectDate"
|
||||
></PickDay>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="icon is-left">
|
||||
<Icon
|
||||
name="material-symbols:calendar-today-outline-rounded"
|
||||
:size="21"
|
||||
class="has-text-grey"
|
||||
/>
|
||||
</span>
|
||||
<span class="icon is-left">
|
||||
<Icon
|
||||
name="material-symbols:calendar-today-outline-rounded"
|
||||
:size="21"
|
||||
class="has-text-grey"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['record', 'attr', 'position', 'mindate', 'maxdate', 'disabled'],
|
||||
props: ["record", "attr", "position", "mindate", "maxdate", "disabled"],
|
||||
data() {
|
||||
return {
|
||||
date: undefined,
|
||||
@@ -32,103 +58,102 @@ export default {
|
||||
docid: this.$id(),
|
||||
count1: 0,
|
||||
count2: 0,
|
||||
pos: undefined
|
||||
}
|
||||
pos: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getPos()
|
||||
if(this.record) {
|
||||
this.date = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined
|
||||
if(this.date) this.show = this.$dayjs(this.date).format('DD/MM/YYYY')
|
||||
this.getPos();
|
||||
if (this.record) {
|
||||
this.date = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
|
||||
if (this.date) this.show = this.$dayjs(this.date).format("DD/MM/YYYY");
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
record: function(newVal) {
|
||||
if(this.record) {
|
||||
this.date = this.record[this.attr]? this.$copy(this.record[this.attr]) : undefined
|
||||
if(this.date) this.show = this.$dayjs(this.date).format('DD/MM/YYYY')
|
||||
record: function (newVal) {
|
||||
if (this.record) {
|
||||
this.date = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
|
||||
if (this.date) this.show = this.$dayjs(this.date).format("DD/MM/YYYY");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
pressEnter() {
|
||||
this.checkDate()
|
||||
if(!this.error) this.focused = false
|
||||
this.checkDate();
|
||||
if (!this.error) this.focused = false;
|
||||
},
|
||||
setFocus() {
|
||||
this.focused = true
|
||||
this.count1 = 0
|
||||
this.count2 = 0
|
||||
this.focused = true;
|
||||
this.count1 = 0;
|
||||
this.count2 = 0;
|
||||
},
|
||||
lostFocus() {
|
||||
let self = this
|
||||
setTimeout(()=>{
|
||||
if(self.focused && self.count1===0) self.focused = false
|
||||
}, 200)
|
||||
let self = this;
|
||||
setTimeout(() => {
|
||||
if (self.focused && self.count1 === 0) self.focused = false;
|
||||
}, 200);
|
||||
},
|
||||
processEvent(event) {
|
||||
var doc = document.getElementById(this.docid)
|
||||
if(!doc) return
|
||||
this.count2 += 1
|
||||
var isClickInside = false
|
||||
var doc = document.getElementById(this.docid);
|
||||
if (!doc) return;
|
||||
this.count2 += 1;
|
||||
var isClickInside = false;
|
||||
isClickInside = doc.contains(event.target);
|
||||
if(!isClickInside && this.focused) {
|
||||
if(this.count2-1!==this.count1) {
|
||||
this.focused = false
|
||||
this.count1 = 0
|
||||
this.count2 = 0
|
||||
if (!isClickInside && this.focused) {
|
||||
if (this.count2 - 1 !== this.count1) {
|
||||
this.focused = false;
|
||||
this.count1 = 0;
|
||||
this.count2 = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
doClick() {
|
||||
this.count1 += 1
|
||||
this.count1 += 1;
|
||||
},
|
||||
selectDate(v) {
|
||||
this.date = v
|
||||
this.show = this.$dayjs(v).format('DD/MM/YYYY')
|
||||
this.$emit('date', this.date)
|
||||
if(this.focused) this.focused = false
|
||||
this.count1 = 0
|
||||
this.count2 = 0
|
||||
this.date = v;
|
||||
this.show = this.$dayjs(v).format("DD/MM/YYYY");
|
||||
this.$emit("date", this.date);
|
||||
if (this.focused) this.focused = false;
|
||||
this.count1 = 0;
|
||||
this.count2 = 0;
|
||||
},
|
||||
getDate(value) {
|
||||
let v = value.replace(/\D/g,'').slice(0, 10);
|
||||
let v = value.replace(/\D/g, "").slice(0, 10);
|
||||
if (v.length >= 5) {
|
||||
return `${v.slice(0,2)}/${v.slice(2,4)}/${v.slice(4)}`;
|
||||
return `${v.slice(0, 2)}/${v.slice(2, 4)}/${v.slice(4)}`;
|
||||
} else if (v.length >= 3) {
|
||||
return `${v.slice(0, 2)}/${v.slice(2)}`;
|
||||
}
|
||||
else if (v.length >= 3) {
|
||||
return `${v.slice(0,2)}/${v.slice(2)}`;
|
||||
}
|
||||
return v
|
||||
return v;
|
||||
},
|
||||
checkDate() {
|
||||
if(!this.focused) this.setFocus()
|
||||
this.error = false
|
||||
this.date = undefined
|
||||
if(this.$empty(this.show)) return this.$emit('date', null)
|
||||
this.show = this.getDate(this.show)
|
||||
let val = `${this.show.substring(6,10)}-${this.show.substring(3,5)}-${this.show.substring(0,2)}`
|
||||
if(this.$dayjs(val, "YYYY-MM-DD", true).isValid()) {
|
||||
this.date = val
|
||||
this.$emit('date', this.date)
|
||||
} else this.error = true
|
||||
if (!this.focused) this.setFocus();
|
||||
this.error = false;
|
||||
this.date = undefined;
|
||||
if (this.$empty(this.show)) return this.$emit("date", null);
|
||||
this.show = this.getDate(this.show);
|
||||
let val = `${this.show.substring(6, 10)}-${this.show.substring(3, 5)}-${this.show.substring(0, 2)}`;
|
||||
if (this.$dayjs(val, "YYYY-MM-DD", true).isValid()) {
|
||||
this.date = val;
|
||||
this.$emit("date", this.date);
|
||||
} else this.error = true;
|
||||
},
|
||||
getPos() {
|
||||
switch(this.position) {
|
||||
case 'is-top-left':
|
||||
this.pos = 'is-up is-left'
|
||||
switch (this.position) {
|
||||
case "is-top-left":
|
||||
this.pos = "is-up is-left";
|
||||
break;
|
||||
case 'is-top-right':
|
||||
this.pos = 'is-up is-right'
|
||||
case "is-top-right":
|
||||
this.pos = "is-up is-right";
|
||||
break;
|
||||
case 'is-bottom-left':
|
||||
this.pos = 'is-left'
|
||||
case "is-bottom-left":
|
||||
this.pos = "is-left";
|
||||
break;
|
||||
case 'is-bottom-right':
|
||||
this.pos = 'is-right'
|
||||
case "is-bottom-right":
|
||||
this.pos = "is-right";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,185 +1,270 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field is-grouped mb-4 border-bottom" v-if="1<0">
|
||||
<div class="control pl-2" v-if="mode!=='simple'">
|
||||
<a class="mr-1" @click="previousYear()">
|
||||
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a @click="previousMonth()" v-if="type==='days'">
|
||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<div
|
||||
class="field is-grouped mb-4 border-bottom"
|
||||
v-if="1 < 0"
|
||||
>
|
||||
<div
|
||||
class="control pl-2"
|
||||
v-if="mode !== 'simple'"
|
||||
>
|
||||
<a
|
||||
class="mr-1"
|
||||
@click="previousYear()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
@click="previousMonth()"
|
||||
v-if="type === 'days'"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control is-expanded has-text-centered">
|
||||
<span class="fsb-16 hyperlink mr-3" @click="type='months'" v-if="type==='days'">{{`Tháng ${month}`}}</span>
|
||||
<span class="fsb-16 hyperlink" @click="type='years'">{{ caption || year }}</span>
|
||||
<span
|
||||
class="fsb-16 hyperlink mr-3"
|
||||
@click="type = 'months'"
|
||||
v-if="type === 'days'"
|
||||
>{{ `Tháng ${month}` }}</span
|
||||
>
|
||||
<span
|
||||
class="fsb-16 hyperlink"
|
||||
@click="type = 'years'"
|
||||
>{{ caption || year }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="control pr-2" v-if="mode!=='simple'">
|
||||
<a class="mr-1" @click="nextMonth()" v-if="type==='days'">
|
||||
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a @click="nextYear()">
|
||||
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<div
|
||||
class="control pr-2"
|
||||
v-if="mode !== 'simple'"
|
||||
>
|
||||
<a
|
||||
class="mr-1"
|
||||
@click="nextMonth()"
|
||||
v-if="type === 'days'"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a @click="nextYear()">
|
||||
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="type==='days'">
|
||||
<div :class="`columns mx-0 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
|
||||
<div class="column px-3 py-1 has-text-centered" v-for="(m,h) in v.dates" :key="h" style="min-height: 100px;"
|
||||
:style="`border-right: 1px solid #DCDCDC; border-bottom: 1px solid #DCDCDC; ${(viewport>1 && h===0 || viewport===1)? 'border-left: 1px solid #DCDCDC;' : ''}
|
||||
${i===0? 'border-top: 1px solid #DCDCDC;' : ''}`">
|
||||
<p class="mb-1" v-if="i===0"><b>{{ $find(dateOfWeek, {id: h}).text}}</b></p>
|
||||
<span class="has-background-primary has-text-white px-1 py-1" v-if="m.date===today">{{ m.dayPrint }}</span>
|
||||
<span v-else>{{m.dayPrint}}</span>
|
||||
<div class="has-text-left fs-15 mt-1" v-if="m.event && m.currentMonth===m.mothCondition">
|
||||
<p :class="`pt-1 ${j===m.event.length-1? '' : 'border-bottom'}`" v-for="(h,j) in m.event">
|
||||
<SvgIcon v-bind="{name: h.icon, type: h.color, size: 16}"></SvgIcon>
|
||||
<a class="ml-3" @click="openEvent(h)">{{ h.text }}</a>
|
||||
<div v-if="type === 'days'">
|
||||
<div
|
||||
:class="`columns mx-0 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
|
||||
v-for="(v, i) in weeks"
|
||||
:key="i"
|
||||
>
|
||||
<div
|
||||
class="column px-3 py-1 has-text-centered"
|
||||
v-for="(m, h) in v.dates"
|
||||
:key="h"
|
||||
style="min-height: 100px"
|
||||
:style="`border-right: 1px solid #DCDCDC; border-bottom: 1px solid #DCDCDC; ${(viewport > 1 && h === 0) || viewport === 1 ? 'border-left: 1px solid #DCDCDC;' : ''}
|
||||
${i === 0 ? 'border-top: 1px solid #DCDCDC;' : ''}`"
|
||||
>
|
||||
<p
|
||||
class="mb-1"
|
||||
v-if="i === 0"
|
||||
>
|
||||
<b>{{ $find(dateOfWeek, { id: h }).text }}</b>
|
||||
</p>
|
||||
<span
|
||||
class="has-background-primary has-text-white px-1 py-1"
|
||||
v-if="m.date === today"
|
||||
>{{ m.dayPrint }}</span
|
||||
>
|
||||
<span v-else>{{ m.dayPrint }}</span>
|
||||
<div
|
||||
class="has-text-left fs-15 mt-1"
|
||||
v-if="m.event && m.currentMonth === m.mothCondition"
|
||||
>
|
||||
<p
|
||||
:class="`pt-1 ${j === m.event.length - 1 ? '' : 'border-bottom'}`"
|
||||
v-for="(h, j) in m.event"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: h.icon, type: h.color, size: 16 }"></SvgIcon>
|
||||
<a
|
||||
class="ml-3"
|
||||
@click="openEvent(h)"
|
||||
>{{ h.text }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['date', 'events', 'mode', 'vyear', 'vmonth'],
|
||||
props: ["date", "events", "mode", "vyear", "vmonth"],
|
||||
data() {
|
||||
return {
|
||||
dates: [],
|
||||
dateOfWeek: [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"},
|
||||
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}],
|
||||
dateOfWeek: [
|
||||
{ id: 0, text: "CN" },
|
||||
{ id: 1, text: "T2" },
|
||||
{ id: 2, text: "T3" },
|
||||
{ id: 3, text: "T4" },
|
||||
{ id: 4, text: "T5" },
|
||||
{ id: 5, text: "T6" },
|
||||
{ id: 6, text: "T7" },
|
||||
],
|
||||
weeks: [],
|
||||
today: this.$dayjs().format('YYYY/MM/DD'),
|
||||
today: this.$dayjs().format("YYYY/MM/DD"),
|
||||
year: undefined,
|
||||
month: undefined,
|
||||
type: 'days',
|
||||
type: "days",
|
||||
caption: undefined,
|
||||
action: undefined,
|
||||
curdate: undefined,
|
||||
showmodal: undefined,
|
||||
viewport: 5
|
||||
}
|
||||
viewport: 5,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.showDate()
|
||||
this.showDate();
|
||||
},
|
||||
watch: {
|
||||
date: function(newVal) {
|
||||
if(newVal) this.showDate()
|
||||
date: function (newVal) {
|
||||
if (newVal) this.showDate();
|
||||
},
|
||||
vmonth: function(newVal) {
|
||||
this.showDate()
|
||||
vmonth: function (newVal) {
|
||||
this.showDate();
|
||||
},
|
||||
events: function (newVal) {
|
||||
this.showDate();
|
||||
},
|
||||
events: function(newVal) {
|
||||
this.showDate()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async openEvent(event) {
|
||||
let row = await this.$getdata('sale', {id: event.sale}, undefined, true)
|
||||
this.showmodal = {title: 'Bán hàng', height: '500px', width: '90%', component: 'sale/Sale', vbind: {row: row, highlight: event.id}}
|
||||
let row = await this.$getdata("sale", { id: event.sale }, undefined, true);
|
||||
this.showmodal = {
|
||||
title: "Bán hàng",
|
||||
height: "500px",
|
||||
width: "90%",
|
||||
component: "sale/Sale",
|
||||
vbind: { row: row, highlight: event.id },
|
||||
};
|
||||
},
|
||||
compiledComponent(value) {
|
||||
return {
|
||||
template: `${value}`
|
||||
}
|
||||
template: `${value}`,
|
||||
};
|
||||
},
|
||||
showDate() {
|
||||
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined
|
||||
this.year = this.$dayjs(this.curdate || this.today).year()
|
||||
this.month = this.$dayjs(this.curdate || this.today).month() + 1
|
||||
if(this.vyear) this.year = this.$copy(this.vyear)
|
||||
if(this.vmonth) this.month = this.$copy(this.vmonth)
|
||||
this.getDates()
|
||||
this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
|
||||
this.year = this.$dayjs(this.curdate || this.today).year();
|
||||
this.month = this.$dayjs(this.curdate || this.today).month() + 1;
|
||||
if (this.vyear) this.year = this.$copy(this.vyear);
|
||||
if (this.vmonth) this.month = this.$copy(this.vmonth);
|
||||
this.getDates();
|
||||
},
|
||||
chooseToday() {
|
||||
this.$emit('date', this.today.replaceAll('/', '-'))
|
||||
this.year = this.$dayjs(this.today).year()
|
||||
this.month = this.$dayjs(this.today).month() + 1
|
||||
this.getDates()
|
||||
this.$emit("date", this.today.replaceAll("/", "-"));
|
||||
this.year = this.$dayjs(this.today).year();
|
||||
this.month = this.$dayjs(this.today).month() + 1;
|
||||
this.getDates();
|
||||
},
|
||||
changeCaption(v) {
|
||||
this.caption = v
|
||||
this.caption = v;
|
||||
},
|
||||
selectMonth(v) {
|
||||
this.month = v
|
||||
this.getDates()
|
||||
this.type = 'days'
|
||||
this.month = v;
|
||||
this.getDates();
|
||||
this.type = "days";
|
||||
},
|
||||
selectYear(v) {
|
||||
this.year = v
|
||||
this.getDates()
|
||||
this.type = 'days'
|
||||
this.year = v;
|
||||
this.getDates();
|
||||
this.type = "days";
|
||||
},
|
||||
getDates() {
|
||||
this.caption = undefined
|
||||
this.dates = this.allDaysInMonth(this.year, this.month)
|
||||
this.dates.map(v=>{
|
||||
let event = this.events? this.$filter(this.events, {isodate: v.date}) : undefined
|
||||
if(event.length>0) v.event = event
|
||||
})
|
||||
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}})
|
||||
this.weeks.map(v=>{
|
||||
v.dates = this.dates.filter(x=>x.week===v.week)
|
||||
})
|
||||
this.caption = undefined;
|
||||
this.dates = this.allDaysInMonth(this.year, this.month);
|
||||
this.dates.map((v) => {
|
||||
let event = this.events ? this.$filter(this.events, { isodate: v.date }) : undefined;
|
||||
if (event.length > 0) v.event = event;
|
||||
});
|
||||
this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
|
||||
return { week: v.week };
|
||||
});
|
||||
this.weeks.map((v) => {
|
||||
v.dates = this.dates.filter((x) => x.week === v.week);
|
||||
});
|
||||
},
|
||||
nextMonth() {
|
||||
let month = this.month + 1
|
||||
if(month>12) {
|
||||
month = 1
|
||||
this.year += 1
|
||||
let month = this.month + 1;
|
||||
if (month > 12) {
|
||||
month = 1;
|
||||
this.year += 1;
|
||||
}
|
||||
this.month = month
|
||||
this.getDates()
|
||||
this.month = month;
|
||||
this.getDates();
|
||||
},
|
||||
previousMonth() {
|
||||
let month = this.month - 1
|
||||
if(month===0) {
|
||||
month = 12
|
||||
this.year -= 1
|
||||
let month = this.month - 1;
|
||||
if (month === 0) {
|
||||
month = 12;
|
||||
this.year -= 1;
|
||||
}
|
||||
this.month = month
|
||||
this.getDates()
|
||||
this.month = month;
|
||||
this.getDates();
|
||||
},
|
||||
nextYear() {
|
||||
if(this.type==='years') return this.action = {name: 'next', id: this.$id()}
|
||||
this.year += 1
|
||||
this.getDates()
|
||||
if (this.type === "years") return (this.action = { name: "next", id: this.$id() });
|
||||
this.year += 1;
|
||||
this.getDates();
|
||||
},
|
||||
previousYear() {
|
||||
if(this.type==='years') return this.action = {name: 'previous', id: this.$id()}
|
||||
this.year -= 1
|
||||
this.getDates()
|
||||
if (this.type === "years") return (this.action = { name: "previous", id: this.$id() });
|
||||
this.year -= 1;
|
||||
this.getDates();
|
||||
},
|
||||
choose(m) {
|
||||
this.$emit('date', m.date.replaceAll('/', '-'))
|
||||
this.$emit("date", m.date.replaceAll("/", "-"));
|
||||
},
|
||||
createDate(v, x, y) {
|
||||
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString())
|
||||
return (
|
||||
v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString())
|
||||
);
|
||||
},
|
||||
allDaysInMonth(year, month) {
|
||||
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
|
||||
let arr = []
|
||||
days.map(v=>{
|
||||
for (let i = 0; i < 7; i++) {
|
||||
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i)
|
||||
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD")
|
||||
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD")
|
||||
let mothCondition = this.$dayjs(date).month() +1
|
||||
let currentMonth = month
|
||||
let found = arr.find(x=>x.date===date)
|
||||
if(!found) {
|
||||
let dayOfWeek = this.$dayjs(date).day()
|
||||
let week = this.$dayjs(date).week()
|
||||
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, mothCondition: mothCondition, currentMonth: currentMonth}
|
||||
arr.push(ele)
|
||||
let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
|
||||
let arr = [];
|
||||
days.map((v) => {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i);
|
||||
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
|
||||
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
|
||||
let mothCondition = this.$dayjs(date).month() + 1;
|
||||
let currentMonth = month;
|
||||
let found = arr.find((x) => x.date === date);
|
||||
if (!found) {
|
||||
let dayOfWeek = this.$dayjs(date).day();
|
||||
let week = this.$dayjs(date).week();
|
||||
let ele = {
|
||||
date: date,
|
||||
week: week,
|
||||
day: v,
|
||||
dayOfWeek: dayOfWeek,
|
||||
dayPrint: dayPrint,
|
||||
mothCondition: mothCondition,
|
||||
currentMonth: currentMonth,
|
||||
};
|
||||
arr.push(ele);
|
||||
}
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
});
|
||||
return arr;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div class="column is-4" v-for="v in months">
|
||||
<EventSummary v-bind="{events: events, mode: 'simple', vyear: v.year, vmonth: v.month}"></EventSummary>
|
||||
<div
|
||||
class="column is-4"
|
||||
v-for="v in months"
|
||||
>
|
||||
<EventSummary
|
||||
v-bind="{
|
||||
events: events,
|
||||
mode: 'simple',
|
||||
vyear: v.year,
|
||||
vmonth: v.month,
|
||||
}"
|
||||
></EventSummary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import EventSummary from '@/components/datepicker/EventSummary'
|
||||
import EventSummary from "@/components/datepicker/EventSummary";
|
||||
export default {
|
||||
components: {
|
||||
EventSummary
|
||||
EventSummary,
|
||||
//EventSummary: () => import('@/components/datepicker/EventSummary')
|
||||
},
|
||||
props: ['events', 'months']
|
||||
}
|
||||
</script>
|
||||
props: ["events", "months"],
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,63 @@
|
||||
<template>
|
||||
<div class="columns mx-0">
|
||||
<div class="column is-narrow" v-if="1<0">
|
||||
<div
|
||||
class="column is-narrow"
|
||||
v-if="1 < 0"
|
||||
>
|
||||
<EventSummary></EventSummary>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="column">
|
||||
<div class="mb-4">
|
||||
<span class="fsb-17 mr-4">{{ `T${vmonth}/${vyear}` }}</span>
|
||||
<a class="mr-2" @click="previous()"><SvgIcon v-bind="{name: 'left1.svg', type: 'dark', size: 18}"></SvgIcon></a>
|
||||
<a class="mr-3" @click="next()"><SvgIcon v-bind="{name: 'right.svg', type: 'dark', size: 18}"></SvgIcon></a>
|
||||
<a @click="refresh()"><SvgIcon v-bind="{name: 'refresh.svg', type: 'dark', size: 18}"></SvgIcon></a>
|
||||
<a
|
||||
class="mr-2"
|
||||
@click="previous()"
|
||||
><SvgIcon v-bind="{ name: 'left1.svg', type: 'dark', size: 18 }"></SvgIcon
|
||||
></a>
|
||||
<a
|
||||
class="mr-3"
|
||||
@click="next()"
|
||||
><SvgIcon v-bind="{ name: 'right.svg', type: 'dark', size: 18 }"></SvgIcon
|
||||
></a>
|
||||
<a @click="refresh()"><SvgIcon v-bind="{ name: 'refresh.svg', type: 'dark', size: 18 }"></SvgIcon></a>
|
||||
</div>
|
||||
<EventDetail v-bind="{events: events, vyear: vyear, vmonth: vmonth}"></EventDetail>
|
||||
</div>
|
||||
<EventDetail v-bind="{ events: events, vyear: vyear, vmonth: vmonth }"></EventDetail>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import EventSummary from '@/components/datepicker/EventSummary'
|
||||
import EventDetail from '@/components/datepicker/EventDetail'
|
||||
import EventSummary from "@/components/datepicker/EventSummary";
|
||||
import EventDetail from "@/components/datepicker/EventDetail";
|
||||
</script>
|
||||
<script>
|
||||
export default {
|
||||
props: ['events', 'year', 'month'],
|
||||
props: ["events", "year", "month"],
|
||||
data() {
|
||||
return {
|
||||
vyear: this.year? this.$copy(this.year) : undefined,
|
||||
vmonth: this.month? this.$copy(this.month) : undefined
|
||||
}
|
||||
vyear: this.year ? this.$copy(this.year) : undefined,
|
||||
vmonth: this.month ? this.$copy(this.month) : undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
next() {
|
||||
let month = this.vmonth + 1
|
||||
if(month>12) {
|
||||
month = 1
|
||||
this.vyear += 1
|
||||
let month = this.vmonth + 1;
|
||||
if (month > 12) {
|
||||
month = 1;
|
||||
this.vyear += 1;
|
||||
}
|
||||
this.vmonth = month
|
||||
this.vmonth = month;
|
||||
},
|
||||
previous() {
|
||||
let month = this.vmonth - 1
|
||||
if(month===0) {
|
||||
month = 12
|
||||
this.vyear -= 1
|
||||
let month = this.vmonth - 1;
|
||||
if (month === 0) {
|
||||
month = 12;
|
||||
this.vyear -= 1;
|
||||
}
|
||||
this.vmonth = month
|
||||
this.vmonth = month;
|
||||
},
|
||||
refresh() {
|
||||
this.$emit('refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
this.$emit("refresh");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,189 +1,261 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field is-grouped mb-4 border-bottom">
|
||||
<div class="control pl-2" v-if="mode!=='simple'">
|
||||
<a class="mr-1" @click="previousYear()">
|
||||
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a @click="previousMonth()" v-if="type==='days'">
|
||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<div
|
||||
class="control pl-2"
|
||||
v-if="mode !== 'simple'"
|
||||
>
|
||||
<a
|
||||
class="mr-1"
|
||||
@click="previousYear()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
@click="previousMonth()"
|
||||
v-if="type === 'days'"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control is-expanded has-text-centered">
|
||||
<span class="fsb-16 hyperlink mr-3" @click="mode==='simple'? false : type='PickMonth'" v-if="type==='days'">{{`Tháng ${month}`}}</span>
|
||||
<span class="fsb-16 hyperlink" @click="mode==='simple'? false : type='PickYear'">{{ caption || year }}</span>
|
||||
<span
|
||||
class="fsb-16 hyperlink mr-3"
|
||||
@click="mode === 'simple' ? false : (type = 'PickMonth')"
|
||||
v-if="type === 'days'"
|
||||
>{{ `Tháng ${month}` }}</span
|
||||
>
|
||||
<span
|
||||
class="fsb-16 hyperlink"
|
||||
@click="mode === 'simple' ? false : (type = 'PickYear')"
|
||||
>{{ caption || year }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="control pr-2" v-if="mode!=='simple'">
|
||||
<a class="mr-1" @click="nextMonth()" v-if="type==='days'">
|
||||
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a @click="nextYear()">
|
||||
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="type==='days'">
|
||||
<div class="columns is-mobile mx-0">
|
||||
<div class="column px-2 py-1 has-text-grey-dark" v-for="(m,h) in dateOfWeek" :key="h">
|
||||
{{ m.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`columns is-mobile mx-0 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
|
||||
<div class="column px-3 fs-14" v-for="(m,h) in v.dates" :key="h" style="padding-top: 1px; padding-bottom: 1px;">
|
||||
<a v-if="m.event && m.currentMonth===m.mothCondition">
|
||||
<Tooltip v-bind="{html: m.event.html, tooltip: m.event.tooltip}"></Tooltip>
|
||||
<div
|
||||
class="control pr-2"
|
||||
v-if="mode !== 'simple'"
|
||||
>
|
||||
<a
|
||||
class="mr-1"
|
||||
@click="nextMonth()"
|
||||
v-if="type === 'days'"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a @click="nextYear()">
|
||||
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<span class="has-background-findata has-text-white px-1 py-1" v-else-if="m.date===today">{{ m.dayPrint }}</span>
|
||||
<span v-else>{{m.dayPrint}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="mode!=='simple'">
|
||||
<div class="border-bottom"></div>
|
||||
<div class="mt-2">
|
||||
<span class="ml-2 mr-2">Hôm nay: </span>
|
||||
<span class="has-text-primary hyperlink" @click="chooseToday()">{{ $dayjs(today).format('DD/MM/YYYY') }}</span>
|
||||
<div v-if="type === 'days'">
|
||||
<div class="columns is-mobile mx-0">
|
||||
<div
|
||||
class="column px-2 py-1 has-text-grey-dark"
|
||||
v-for="(m, h) in dateOfWeek"
|
||||
:key="h"
|
||||
>
|
||||
{{ m.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="`columns is-mobile mx-0 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
|
||||
v-for="(v, i) in weeks"
|
||||
:key="i"
|
||||
>
|
||||
<div
|
||||
class="column px-3 fs-14"
|
||||
v-for="(m, h) in v.dates"
|
||||
:key="h"
|
||||
style="padding-top: 1px; padding-bottom: 1px"
|
||||
>
|
||||
<a v-if="m.event && m.currentMonth === m.mothCondition">
|
||||
<Tooltip v-bind="{ html: m.event.html, tooltip: m.event.tooltip }"></Tooltip>
|
||||
</a>
|
||||
<span
|
||||
class="has-background-findata has-text-white px-1 py-1"
|
||||
v-else-if="m.date === today"
|
||||
>{{ m.dayPrint }}</span
|
||||
>
|
||||
<span v-else>{{ m.dayPrint }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="mode !== 'simple'">
|
||||
<div class="border-bottom"></div>
|
||||
<div class="mt-2">
|
||||
<span class="ml-2 mr-2">Hôm nay: </span>
|
||||
<span
|
||||
class="has-text-primary hyperlink"
|
||||
@click="chooseToday()"
|
||||
>{{ $dayjs(today).format("DD/MM/YYYY") }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="type === 'PickMonth'">
|
||||
<PickMonth @month="selectMonth"></PickMonth>
|
||||
</div>
|
||||
<div v-else-if="type === 'PickYear'">
|
||||
<PickYear
|
||||
v-bind="{ year: year, month: month, action: action }"
|
||||
@year="selectYear"
|
||||
@caption="changeCaption"
|
||||
></PickYear>
|
||||
</div>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
<div v-else-if="type==='PickMonth'">
|
||||
<PickMonth @month="selectMonth"></PickMonth>
|
||||
</div>
|
||||
<div v-else-if="type==='PickYear'">
|
||||
<PickYear v-bind="{year: year, month: month, action: action}" @year="selectYear" @caption="changeCaption"></PickYear>
|
||||
</div>
|
||||
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
PickMonth: () => import('@/components/datepicker/PickMonth'),
|
||||
PickYear: () => import('@/components/datepicker/PickYear')
|
||||
PickMonth: () => import("@/components/datepicker/PickMonth"),
|
||||
PickYear: () => import("@/components/datepicker/PickYear"),
|
||||
},
|
||||
props: ['date', 'events', 'mode', 'vyear', 'vmonth'],
|
||||
props: ["date", "events", "mode", "vyear", "vmonth"],
|
||||
data() {
|
||||
return {
|
||||
dates: [],
|
||||
dateOfWeek: [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"},
|
||||
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}],
|
||||
dateOfWeek: [
|
||||
{ id: 0, text: "CN" },
|
||||
{ id: 1, text: "T2" },
|
||||
{ id: 2, text: "T3" },
|
||||
{ id: 3, text: "T4" },
|
||||
{ id: 4, text: "T5" },
|
||||
{ id: 5, text: "T6" },
|
||||
{ id: 6, text: "T7" },
|
||||
],
|
||||
weeks: [],
|
||||
today: this.$dayjs().format('YYYY/MM/DD'),
|
||||
today: this.$dayjs().format("YYYY/MM/DD"),
|
||||
year: undefined,
|
||||
month: undefined,
|
||||
type: 'days',
|
||||
type: "days",
|
||||
caption: undefined,
|
||||
action: undefined,
|
||||
curdate: undefined,
|
||||
showmodal: undefined
|
||||
}
|
||||
showmodal: undefined,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.showDate()
|
||||
this.showDate();
|
||||
},
|
||||
watch: {
|
||||
date: function(newVal) {
|
||||
if(newVal) this.showDate()
|
||||
date: function (newVal) {
|
||||
if (newVal) this.showDate();
|
||||
},
|
||||
events: function (newVal) {
|
||||
this.showDate();
|
||||
},
|
||||
events: function(newVal) {
|
||||
this.showDate()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDate() {
|
||||
this.curdate = this.date? this.date.replaceAll('-', '/') : undefined
|
||||
this.year = this.$dayjs(this.curdate || this.today).year()
|
||||
this.month = this.$dayjs(this.curdate || this.today).month() + 1
|
||||
if(this.vyear) this.year = this.$copy(this.vyear)
|
||||
if(this.vmonth) this.month = this.$copy(this.vmonth)
|
||||
this.getDates()
|
||||
this.curdate = this.date ? this.date.replaceAll("-", "/") : undefined;
|
||||
this.year = this.$dayjs(this.curdate || this.today).year();
|
||||
this.month = this.$dayjs(this.curdate || this.today).month() + 1;
|
||||
if (this.vyear) this.year = this.$copy(this.vyear);
|
||||
if (this.vmonth) this.month = this.$copy(this.vmonth);
|
||||
this.getDates();
|
||||
},
|
||||
chooseToday() {
|
||||
this.$emit('date', this.today.replaceAll('/', '-'))
|
||||
this.year = this.$dayjs(this.today).year()
|
||||
this.month = this.$dayjs(this.today).month() + 1
|
||||
this.getDates()
|
||||
this.$emit("date", this.today.replaceAll("/", "-"));
|
||||
this.year = this.$dayjs(this.today).year();
|
||||
this.month = this.$dayjs(this.today).month() + 1;
|
||||
this.getDates();
|
||||
},
|
||||
changeCaption(v) {
|
||||
this.caption = v
|
||||
this.caption = v;
|
||||
},
|
||||
selectMonth(v) {
|
||||
this.month = v
|
||||
this.getDates()
|
||||
this.type = 'days'
|
||||
this.month = v;
|
||||
this.getDates();
|
||||
this.type = "days";
|
||||
},
|
||||
selectYear(v) {
|
||||
this.year = v
|
||||
this.getDates()
|
||||
this.type = 'days'
|
||||
this.year = v;
|
||||
this.getDates();
|
||||
this.type = "days";
|
||||
},
|
||||
getDates() {
|
||||
this.caption = undefined
|
||||
this.dates = this.allDaysInMonth(this.year, this.month)
|
||||
this.dates.map(v=>{
|
||||
let event = this.events? this.$find(this.events, {isodate: v.date}) : undefined
|
||||
if(event) v.event = event
|
||||
})
|
||||
this.weeks = this.$unique(this.dates, ['week']).map(v=>{return {week: v.week}})
|
||||
this.weeks.map(v=>{
|
||||
v.dates = this.dates.filter(x=>x.week===v.week)
|
||||
})
|
||||
this.caption = undefined;
|
||||
this.dates = this.allDaysInMonth(this.year, this.month);
|
||||
this.dates.map((v) => {
|
||||
let event = this.events ? this.$find(this.events, { isodate: v.date }) : undefined;
|
||||
if (event) v.event = event;
|
||||
});
|
||||
this.weeks = this.$unique(this.dates, ["week"]).map((v) => {
|
||||
return { week: v.week };
|
||||
});
|
||||
this.weeks.map((v) => {
|
||||
v.dates = this.dates.filter((x) => x.week === v.week);
|
||||
});
|
||||
},
|
||||
nextMonth() {
|
||||
let month = this.month + 1
|
||||
if(month>12) {
|
||||
month = 1
|
||||
this.year += 1
|
||||
let month = this.month + 1;
|
||||
if (month > 12) {
|
||||
month = 1;
|
||||
this.year += 1;
|
||||
}
|
||||
this.month = month
|
||||
this.getDates()
|
||||
this.month = month;
|
||||
this.getDates();
|
||||
},
|
||||
previousMonth() {
|
||||
let month = this.month - 1
|
||||
if(month===0) {
|
||||
month = 12
|
||||
this.year -= 1
|
||||
let month = this.month - 1;
|
||||
if (month === 0) {
|
||||
month = 12;
|
||||
this.year -= 1;
|
||||
}
|
||||
this.month = month
|
||||
this.getDates()
|
||||
this.month = month;
|
||||
this.getDates();
|
||||
},
|
||||
nextYear() {
|
||||
if(this.type==='PickYear') return this.action = {name: 'next', id: this.$id()}
|
||||
this.year += 1
|
||||
this.getDates()
|
||||
if (this.type === "PickYear") return (this.action = { name: "next", id: this.$id() });
|
||||
this.year += 1;
|
||||
this.getDates();
|
||||
},
|
||||
previousYear() {
|
||||
if(this.type==='PickYear') return this.action = {name: 'previous', id: this.$id()}
|
||||
this.year -= 1
|
||||
this.getDates()
|
||||
if (this.type === "PickYear") return (this.action = { name: "previous", id: this.$id() });
|
||||
this.year -= 1;
|
||||
this.getDates();
|
||||
},
|
||||
choose(m) {
|
||||
this.$emit('date', m.date.replaceAll('/', '-'))
|
||||
this.$emit("date", m.date.replaceAll("/", "-"));
|
||||
},
|
||||
createDate(v, x, y) {
|
||||
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString())
|
||||
return (
|
||||
v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString())
|
||||
);
|
||||
},
|
||||
allDaysInMonth(year, month) {
|
||||
let days = Array.from({length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
|
||||
let arr = []
|
||||
days.map(v=>{
|
||||
for (let i = 0; i < 7; i++) {
|
||||
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i)
|
||||
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD")
|
||||
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD")
|
||||
let mothCondition = this.$dayjs(date).month() +1
|
||||
let currentMonth = month
|
||||
let found = arr.find(x=>x.date===date)
|
||||
if(!found) {
|
||||
let dayOfWeek = this.$dayjs(date).day()
|
||||
let week = this.$dayjs(date).week()
|
||||
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint,
|
||||
mothCondition: mothCondition, currentMonth: currentMonth}
|
||||
arr.push(ele)
|
||||
let days = Array.from({ length: this.$dayjs(this.createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
|
||||
let arr = [];
|
||||
days.map((v) => {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
let thedate = this.$dayjs(this.createDate(year, month, v)).weekday(i);
|
||||
let date = this.$dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
|
||||
let dayPrint = this.$dayjs(new Date(thedate.$d)).format("DD");
|
||||
let mothCondition = this.$dayjs(date).month() + 1;
|
||||
let currentMonth = month;
|
||||
let found = arr.find((x) => x.date === date);
|
||||
if (!found) {
|
||||
let dayOfWeek = this.$dayjs(date).day();
|
||||
let week = this.$dayjs(date).week();
|
||||
let ele = {
|
||||
date: date,
|
||||
week: week,
|
||||
day: v,
|
||||
dayOfWeek: dayOfWeek,
|
||||
dayPrint: dayPrint,
|
||||
mothCondition: mothCondition,
|
||||
currentMonth: currentMonth,
|
||||
};
|
||||
arr.push(ele);
|
||||
}
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
});
|
||||
return arr;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,53 +1,91 @@
|
||||
<template>
|
||||
<div class="p-2" style="width: 300px">
|
||||
<div
|
||||
class="p-2"
|
||||
style="width: 300px"
|
||||
>
|
||||
<div class="field is-grouped">
|
||||
<div class="control pl-2">
|
||||
<a class="mr-1" @click="previousYear()">
|
||||
<SvgIcon v-bind="{name: 'doubleleft.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a @click="previousMonth()" v-if="type==='days'">
|
||||
<SvgIcon v-bind="{name: 'left1.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
class="mr-1"
|
||||
@click="previousYear()"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'doubleleft.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a
|
||||
@click="previousMonth()"
|
||||
v-if="type === 'days'"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'left1.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control is-expanded has-text-centered">
|
||||
<a class="font-bold mr-2" @click="type='months'" v-if="type==='days'">{{`T${month}`}}</a>
|
||||
<a class="font-bold" @click="type='years'">{{ caption || year }}</a>
|
||||
<a
|
||||
class="font-bold mr-2"
|
||||
@click="type = 'months'"
|
||||
v-if="type === 'days'"
|
||||
>{{ `T${month}` }}</a
|
||||
>
|
||||
<a
|
||||
class="font-bold"
|
||||
@click="type = 'years'"
|
||||
>{{ caption || year }}</a
|
||||
>
|
||||
</div>
|
||||
<div class="control pr-2">
|
||||
<a class="mr-1" @click="nextMonth()" v-if="type==='days'">
|
||||
<SvgIcon v-bind="{name: 'right.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
<a
|
||||
class="mr-1"
|
||||
@click="nextMonth()"
|
||||
v-if="type === 'days'"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'right.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
<a @click="nextYear()">
|
||||
<SvgIcon v-bind="{name: 'doubleright.svg', type: 'gray', size: 18}"></SvgIcon>
|
||||
<SvgIcon v-bind="{ name: 'doubleright.svg', type: 'gray', size: 18 }"></SvgIcon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-0 mb-5" />
|
||||
<div v-if="type==='days'">
|
||||
<div v-if="type === 'days'">
|
||||
<div class="columns is-mobile mx-0 mb-3">
|
||||
<div
|
||||
<div
|
||||
v-for="(m, h) in dateOfWeek"
|
||||
:key="h"
|
||||
class="fs-14 column p-0 has-text-grey is-flex is-justify-content-center is-align-items-center"
|
||||
class="fs-14 column p-0 has-text-grey is-flex is-justify-content-center is-align-items-center"
|
||||
>
|
||||
{{ m.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`columns is-mobile mx-0 mb-1 ${i===weeks.length-1? 'mb-1' : ''}`" v-for="(v,i) in weeks" :key="i">
|
||||
<div class="column p-0 is-flex is-justify-content-center is-align-items-center" style="width: 40px; height: 32px" v-for="(m,h) in v.dates" :key="h">
|
||||
<span class="fs-13 has-text-grey-80" v-if="m.disable">
|
||||
{{m.dayPrint}}
|
||||
<div
|
||||
:class="`columns is-mobile mx-0 mb-1 ${i === weeks.length - 1 ? 'mb-1' : ''}`"
|
||||
v-for="(v, i) in weeks"
|
||||
:key="i"
|
||||
>
|
||||
<div
|
||||
class="column p-0 is-flex is-justify-content-center is-align-items-center"
|
||||
style="width: 40px; height: 32px"
|
||||
v-for="(m, h) in v.dates"
|
||||
:key="h"
|
||||
>
|
||||
<span
|
||||
class="fs-13 has-text-grey-80"
|
||||
v-if="m.disable"
|
||||
>
|
||||
{{ m.dayPrint }}
|
||||
</span>
|
||||
<span class="fs-13 is-clickable" @click="choose(m)" v-else>
|
||||
<span
|
||||
<span
|
||||
class="fs-13 is-clickable"
|
||||
@click="choose(m)"
|
||||
v-else
|
||||
>
|
||||
<span
|
||||
style="width: 25px; height: 25px"
|
||||
:class="[
|
||||
'p-1 rounded-md is-flex is-justify-content-center is-align-items-center',
|
||||
{
|
||||
{
|
||||
'has-background-primary-50 has-text-white': m.date === curdate,
|
||||
'has-background-success-50 has-text-white': m.date === today,
|
||||
'has-text-grey-70': m.currentMonth !== m.monthCondition
|
||||
}
|
||||
'has-text-grey-70': m.currentMonth !== m.monthCondition,
|
||||
},
|
||||
]"
|
||||
>
|
||||
{{ m.dayPrint }}
|
||||
@@ -58,133 +96,165 @@
|
||||
<hr class="my-1" />
|
||||
<div class="mt-2 fs-14">
|
||||
<span class="ml-2">Hôm nay: </span>
|
||||
<a class="has-text-primary" @click="chooseToday()">{{ $dayjs(today).format('DD/MM/YYYY') }}</a>
|
||||
<a
|
||||
class="has-text-primary"
|
||||
@click="chooseToday()"
|
||||
>{{ $dayjs(today).format("DD/MM/YYYY") }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<PickMonth v-else-if="type==='months'" @month="selectMonth"></PickMonth>
|
||||
<PickYear v-else-if="type==='years'" v-bind="{ year, month, action }" @year="selectYear" @caption="changeCaption"></PickYear>
|
||||
<PickMonth
|
||||
v-else-if="type === 'months'"
|
||||
@month="selectMonth"
|
||||
></PickMonth>
|
||||
<PickYear
|
||||
v-else-if="type === 'years'"
|
||||
v-bind="{ year, month, action }"
|
||||
@year="selectYear"
|
||||
@caption="changeCaption"
|
||||
></PickYear>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import PickMonth from '@/components/datepicker/PickMonth'
|
||||
import PickYear from '@/components/datepicker/PickYear'
|
||||
const { $id, $dayjs, $unique} = useNuxtApp()
|
||||
const emit = defineEmits(['date'])
|
||||
var props = defineProps({
|
||||
date: String,
|
||||
maxdate: String
|
||||
})
|
||||
var dates = []
|
||||
var dateOfWeek = [{id: 0, text: "CN"}, {id: 1, text: "T2"}, {id: 2, text: "T3"}, {id: 3, text: "T4"},
|
||||
{id: 4, text: "T5",}, {id: 5, text: "T6"}, {id: 6, text: "T7"}]
|
||||
var weeks = ref([])
|
||||
var year= ref(undefined)
|
||||
var month = undefined
|
||||
var type = ref('days')
|
||||
var caption = ref(undefined)
|
||||
var action = ref(undefined)
|
||||
var curdate = undefined
|
||||
var today = new Date()
|
||||
function showDate() {
|
||||
curdate = props.date? props.date.replaceAll('-', '/') : undefined
|
||||
year.value = $dayjs(curdate || today).year()
|
||||
month = $dayjs(curdate || today).month() + 1
|
||||
getDates()
|
||||
}
|
||||
function chooseToday() {
|
||||
emit('date', $dayjs(today).format('YYYY-MM-DD') )//today.replaceAll('/', '-'))
|
||||
year.value = $dayjs(today).year()
|
||||
month = $dayjs(today).month() + 1
|
||||
getDates()
|
||||
}
|
||||
function changeCaption(v) {
|
||||
caption.value = v
|
||||
}
|
||||
function selectMonth(v) {
|
||||
month = v
|
||||
getDates()
|
||||
type.value = 'days'
|
||||
}
|
||||
function selectYear(v) {
|
||||
year.value = v
|
||||
getDates()
|
||||
type.value = 'days'
|
||||
}
|
||||
function getDates() {
|
||||
caption.value = undefined
|
||||
dates = allDaysInMonth(year.value, month)
|
||||
weeks.value = $unique(dates, ['week']).map(v=>{return {week: v.week}})
|
||||
weeks.value.map(v=>{
|
||||
v.dates = dates.filter(x=>x.week===v.week)
|
||||
})
|
||||
}
|
||||
function nextMonth() {
|
||||
month = month + 1
|
||||
if(month>12) {
|
||||
month = 1
|
||||
year.value += 1
|
||||
}
|
||||
getDates()
|
||||
}
|
||||
function previousMonth() {
|
||||
month = month - 1
|
||||
if(month===0) {
|
||||
month = 12
|
||||
year.value -= 1
|
||||
}
|
||||
getDates()
|
||||
}
|
||||
function nextYear() {
|
||||
if(type.value==='years') return action.value = {name: 'next', id: $id()}
|
||||
year.value += 1
|
||||
getDates()
|
||||
}
|
||||
function previousYear() {
|
||||
if(type.value==='years') return action.value = {name: 'previous', id: $id()}
|
||||
year.value -= 1
|
||||
getDates()
|
||||
}
|
||||
function choose(m) {
|
||||
emit('date', m.date.replaceAll('/', '-'))
|
||||
}
|
||||
function createDate(v, x, y) {
|
||||
return v + '/' + (x<10? '0' + x.toString() : x.toString()) + '/' + (y<10? '0' + y.toString() : y.toString())
|
||||
}
|
||||
function allDaysInMonth(year, month) {
|
||||
let days = Array.from({length: $dayjs(createDate(year, month, 1)).daysInMonth()}, (_, i) => i + 1)
|
||||
let arr = []
|
||||
days.map(v=>{
|
||||
for (let i = 0; i < 7; i++) {
|
||||
let thedate = $dayjs(createDate(year, month, v)).weekday(i)
|
||||
let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD")
|
||||
let dayPrint = $dayjs(new Date(thedate.$d)).format("DD")
|
||||
let monthCondition = $dayjs(date).month() +1
|
||||
let currentMonth = month
|
||||
let found = arr.find(x=>x.date===date)
|
||||
if(!found) {
|
||||
let dayOfWeek = $dayjs(date).day()
|
||||
let week = $dayjs(date).week()
|
||||
let disable = false
|
||||
if(props.maxdate? $dayjs(props.maxdate).diff(props.date, 'day')>=0 : false) {
|
||||
if($dayjs(props.maxdate).startOf('day').diff(date, 'day')<0) disable = true
|
||||
}
|
||||
let ele = {date: date, week: week, day: v, dayOfWeek: dayOfWeek, dayPrint: dayPrint, monthCondition: monthCondition,
|
||||
currentMonth: currentMonth, disable: disable}
|
||||
arr.push(ele)
|
||||
}
|
||||
import PickMonth from "@/components/datepicker/PickMonth";
|
||||
import PickYear from "@/components/datepicker/PickYear";
|
||||
const { $id, $dayjs, $unique } = useNuxtApp();
|
||||
const emit = defineEmits(["date"]);
|
||||
var props = defineProps({
|
||||
date: String,
|
||||
maxdate: String,
|
||||
});
|
||||
var dates = [];
|
||||
var dateOfWeek = [
|
||||
{ id: 0, text: "CN" },
|
||||
{ id: 1, text: "T2" },
|
||||
{ id: 2, text: "T3" },
|
||||
{ id: 3, text: "T4" },
|
||||
{ id: 4, text: "T5" },
|
||||
{ id: 5, text: "T6" },
|
||||
{ id: 6, text: "T7" },
|
||||
];
|
||||
var weeks = ref([]);
|
||||
var year = ref(undefined);
|
||||
var month = undefined;
|
||||
var type = ref("days");
|
||||
var caption = ref(undefined);
|
||||
var action = ref(undefined);
|
||||
var curdate = undefined;
|
||||
var today = new Date();
|
||||
function showDate() {
|
||||
curdate = props.date ? props.date.replaceAll("-", "/") : undefined;
|
||||
year.value = $dayjs(curdate || today).year();
|
||||
month = $dayjs(curdate || today).month() + 1;
|
||||
getDates();
|
||||
}
|
||||
function chooseToday() {
|
||||
emit("date", $dayjs(today).format("YYYY-MM-DD")); //today.replaceAll('/', '-'))
|
||||
year.value = $dayjs(today).year();
|
||||
month = $dayjs(today).month() + 1;
|
||||
getDates();
|
||||
}
|
||||
function changeCaption(v) {
|
||||
caption.value = v;
|
||||
}
|
||||
function selectMonth(v) {
|
||||
month = v;
|
||||
getDates();
|
||||
type.value = "days";
|
||||
}
|
||||
function selectYear(v) {
|
||||
year.value = v;
|
||||
getDates();
|
||||
type.value = "days";
|
||||
}
|
||||
function getDates() {
|
||||
caption.value = undefined;
|
||||
dates = allDaysInMonth(year.value, month);
|
||||
weeks.value = $unique(dates, ["week"]).map((v) => {
|
||||
return { week: v.week };
|
||||
});
|
||||
weeks.value.map((v) => {
|
||||
v.dates = dates.filter((x) => x.week === v.week);
|
||||
});
|
||||
}
|
||||
function nextMonth() {
|
||||
month = month + 1;
|
||||
if (month > 12) {
|
||||
month = 1;
|
||||
year.value += 1;
|
||||
}
|
||||
getDates();
|
||||
}
|
||||
function previousMonth() {
|
||||
month = month - 1;
|
||||
if (month === 0) {
|
||||
month = 12;
|
||||
year.value -= 1;
|
||||
}
|
||||
getDates();
|
||||
}
|
||||
function nextYear() {
|
||||
if (type.value === "years") return (action.value = { name: "next", id: $id() });
|
||||
year.value += 1;
|
||||
getDates();
|
||||
}
|
||||
function previousYear() {
|
||||
if (type.value === "years") return (action.value = { name: "previous", id: $id() });
|
||||
year.value -= 1;
|
||||
getDates();
|
||||
}
|
||||
function choose(m) {
|
||||
emit("date", m.date.replaceAll("/", "-"));
|
||||
}
|
||||
function createDate(v, x, y) {
|
||||
return v + "/" + (x < 10 ? "0" + x.toString() : x.toString()) + "/" + (y < 10 ? "0" + y.toString() : y.toString());
|
||||
}
|
||||
function allDaysInMonth(year, month) {
|
||||
let days = Array.from({ length: $dayjs(createDate(year, month, 1)).daysInMonth() }, (_, i) => i + 1);
|
||||
let arr = [];
|
||||
days.map((v) => {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
let thedate = $dayjs(createDate(year, month, v)).weekday(i);
|
||||
let date = $dayjs(new Date(thedate.$d)).format("YYYY/MM/DD");
|
||||
let dayPrint = $dayjs(new Date(thedate.$d)).format("DD");
|
||||
let monthCondition = $dayjs(date).month() + 1;
|
||||
let currentMonth = month;
|
||||
let found = arr.find((x) => x.date === date);
|
||||
if (!found) {
|
||||
let dayOfWeek = $dayjs(date).day();
|
||||
let week = $dayjs(date).week();
|
||||
let disable = false;
|
||||
if (props.maxdate ? $dayjs(props.maxdate).diff(props.date, "day") >= 0 : false) {
|
||||
if ($dayjs(props.maxdate).startOf("day").diff(date, "day") < 0) disable = true;
|
||||
}
|
||||
})
|
||||
return arr
|
||||
let ele = {
|
||||
date: date,
|
||||
week: week,
|
||||
day: v,
|
||||
dayOfWeek: dayOfWeek,
|
||||
dayPrint: dayPrint,
|
||||
monthCondition: monthCondition,
|
||||
currentMonth: currentMonth,
|
||||
disable: disable,
|
||||
};
|
||||
arr.push(ele);
|
||||
}
|
||||
}
|
||||
// display
|
||||
showDate()
|
||||
// change date
|
||||
watch(() => props.date, (newVal, oldVal) => {
|
||||
showDate()
|
||||
})
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
// display
|
||||
showDate();
|
||||
// change date
|
||||
watch(
|
||||
() => props.date,
|
||||
(newVal, oldVal) => {
|
||||
showDate();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style scoped>
|
||||
a {
|
||||
color: var(--bulma-link-60);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<div class="columns is-mobile is-multiline mx-0">
|
||||
<span class="column has-text-centered is-4 is-clickable fs-14" v-for="v in months" @click="$emit('month', v)">Tháng {{ v }}</span>
|
||||
<span
|
||||
class="column has-text-centered is-4 is-clickable fs-14"
|
||||
v-for="v in months"
|
||||
@click="$emit('month', v)"
|
||||
>Tháng {{ v }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
months: [1,2,3,4,5,6,7,8,9,10,11,12]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,57 +1,65 @@
|
||||
<template>
|
||||
<div class="columns is-mobile is-multiline mx-0">
|
||||
<span
|
||||
v-for="(v,i) in years"
|
||||
class="column is-4 has-text-centered is-clickable fs-14"
|
||||
:class="i===0 || i===11 ? 'has-text-grey-light' : ''"
|
||||
@click="$emit('year', v)">{{ v }}</span>
|
||||
<span
|
||||
v-for="(v, i) in years"
|
||||
class="column is-4 has-text-centered is-clickable fs-14"
|
||||
:class="i === 0 || i === 11 ? 'has-text-grey-light' : ''"
|
||||
@click="$emit('year', v)"
|
||||
>{{ v }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['year', 'month', 'action'],
|
||||
props: ["year", "month", "action"],
|
||||
data() {
|
||||
return {
|
||||
years: []
|
||||
}
|
||||
years: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.years = [this.year]
|
||||
for(let i = 1; i < 7; i++) {
|
||||
this.years.push(this.year+i)
|
||||
this.years.push(this.year-i)
|
||||
}
|
||||
this.years.sort(function(a, b) {return a - b;})
|
||||
this.years = this.years.slice(0,12)
|
||||
this.$emit('caption', `${this.years[1]}-${this.years[10]}`)
|
||||
this.years = [this.year];
|
||||
for (let i = 1; i < 7; i++) {
|
||||
this.years.push(this.year + i);
|
||||
this.years.push(this.year - i);
|
||||
}
|
||||
this.years.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
this.years = this.years.slice(0, 12);
|
||||
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
|
||||
},
|
||||
watch: {
|
||||
action: function(newVal) {
|
||||
if(newVal.name==='next') this.next()
|
||||
if(newVal.name==='previous') this.previous()
|
||||
}
|
||||
action: function (newVal) {
|
||||
if (newVal.name === "next") this.next();
|
||||
if (newVal.name === "previous") this.previous();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
next() {
|
||||
let year = this.years[this.years.length-1]
|
||||
this.years = []
|
||||
for(let i = 0; i < 12; i++) {
|
||||
this.years.push(year+i)
|
||||
let year = this.years[this.years.length - 1];
|
||||
this.years = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
this.years.push(year + i);
|
||||
}
|
||||
this.years.sort(function(a, b) {return a - b;})
|
||||
this.years = this.years.slice(0,12)
|
||||
this.$emit('caption', `${this.years[1]}-${this.years[10]}`)
|
||||
this.years.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
this.years = this.years.slice(0, 12);
|
||||
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
|
||||
},
|
||||
previous() {
|
||||
let year = this.years[0]
|
||||
this.years = []
|
||||
for(let i = 0; i < 12; i++) {
|
||||
this.years.push(year-i)
|
||||
let year = this.years[0];
|
||||
this.years = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
this.years.push(year - i);
|
||||
}
|
||||
this.years.sort(function(a, b) {return a - b;})
|
||||
this.years = this.years.slice(0,12)
|
||||
this.$emit('caption', `${this.years[1]}-${this.years[10]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.years.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
this.years = this.years.slice(0, 12);
|
||||
this.$emit("caption", `${this.years[1]}-${this.years[10]}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import Template1 from '@/lib/email/templates/Template1.vue';
|
||||
import { render } from '@vue-email/render';
|
||||
import { forEachAsync, isEqual } from 'es-toolkit';
|
||||
import Template1 from "@/lib/email/templates/Template1.vue";
|
||||
import { render } from "@vue-email/render";
|
||||
import { forEachAsync, isEqual } from "es-toolkit";
|
||||
|
||||
const {
|
||||
const {
|
||||
$dayjs,
|
||||
$getdata,
|
||||
$insertapi,
|
||||
@@ -19,9 +19,9 @@ const {
|
||||
const payables = ref(null);
|
||||
const defaultFilter = {
|
||||
status: 1,
|
||||
to_date__gte: $dayjs().format('YYYY-MM-DD'),
|
||||
to_date__gte: $dayjs().format("YYYY-MM-DD"),
|
||||
to_date__lte: undefined,
|
||||
}
|
||||
};
|
||||
const filter = ref(defaultFilter);
|
||||
const activeDateFilter = ref(null);
|
||||
const key = ref(0);
|
||||
@@ -35,35 +35,42 @@ function resetDateFilter() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'duepayables' }, sort: 'index' });
|
||||
const payablesData = await $getdata("bizsetting", undefined, {
|
||||
filter: { classify: "duepayables" },
|
||||
sort: "index",
|
||||
});
|
||||
payables.value = payablesData;
|
||||
});
|
||||
|
||||
watch(activeDateFilter, (val) => {
|
||||
if (!val) {
|
||||
filter.value = defaultFilter;
|
||||
contents.value = null;
|
||||
} else {
|
||||
const cutoffDate = $dayjs().add(val.time, 'day').format('YYYY-MM-DD');
|
||||
const filterField = `to_date__${val.lookup}`;
|
||||
filter.value = {
|
||||
...defaultFilter,
|
||||
[filterField]: cutoffDate,
|
||||
watch(
|
||||
activeDateFilter,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
filter.value = defaultFilter;
|
||||
contents.value = null;
|
||||
} else {
|
||||
const cutoffDate = $dayjs().add(val.time, "day").format("YYYY-MM-DD");
|
||||
const filterField = `to_date__${val.lookup}`;
|
||||
filter.value = {
|
||||
...defaultFilter,
|
||||
[filterField]: cutoffDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const contents = ref(null);
|
||||
const isSending = ref(false);
|
||||
|
||||
function sanitizeContentPayment(text, maxLength = 80) {
|
||||
if (!text) return '';
|
||||
if (!text) return "";
|
||||
|
||||
return text
|
||||
.normalize('NFD') // bỏ dấu tiếng Việt
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ
|
||||
.replace(/\s+/g, ' ')
|
||||
.normalize("NFD") // bỏ dấu tiếng Việt
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, maxLength);
|
||||
}
|
||||
@@ -88,19 +95,19 @@ const buildContentPayment = (data) => {
|
||||
cycle,
|
||||
} = data;
|
||||
|
||||
if (customerType.toLowerCase() === 'cn') {
|
||||
if (customerType.toLowerCase() === "cn") {
|
||||
if (customerName.length < 14) {
|
||||
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
||||
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
} else {
|
||||
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
||||
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
}
|
||||
} else {
|
||||
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
||||
return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
}
|
||||
};
|
||||
|
||||
function replaceTemplateVars(html, paymentScheduleItem) {
|
||||
const {
|
||||
const {
|
||||
txn_detail__transaction__product__trade_code,
|
||||
txn_detail__transaction__customer__code,
|
||||
txn_detail__transaction__customer__fullname,
|
||||
@@ -112,118 +119,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
|
||||
from_date,
|
||||
to_date,
|
||||
remain_amount,
|
||||
cycle
|
||||
cycle,
|
||||
} = paymentScheduleItem;
|
||||
return html
|
||||
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '')
|
||||
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '')
|
||||
.replace(/\[year]/g, new Date().getFullYear() || '')
|
||||
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
|
||||
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
|
||||
.replace(/\[year]/g, new Date().getFullYear() || "")
|
||||
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
|
||||
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
|
||||
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
|
||||
.replace(
|
||||
/\[customer\.name\]/g,
|
||||
`${txn_detail__transaction__customer__type__code.toLowerCase() == 'cn' ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ''}` ||
|
||||
'',
|
||||
)
|
||||
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
|
||||
.replace(
|
||||
/\[customer\.legal_code\]/g,
|
||||
txn_detail__transaction__customer__legal_code || '',
|
||||
`${txn_detail__transaction__customer__type__code.toLowerCase() == "cn" ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ""}` ||
|
||||
"",
|
||||
)
|
||||
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || "")
|
||||
.replace(/\[customer\.legal_code\]/g, txn_detail__transaction__customer__legal_code || "")
|
||||
.replace(
|
||||
/\[customer\.contact_address\]/g,
|
||||
txn_detail__transaction__customer__contact_address ||
|
||||
txn_detail__transaction__customer__address ||
|
||||
'',
|
||||
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
|
||||
)
|
||||
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '')
|
||||
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '')
|
||||
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '')
|
||||
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '')
|
||||
.replace(
|
||||
/\[payment_schedule\.amount_in_word\]/g,
|
||||
$numberToVietnameseCurrency(remain_amount) || '',
|
||||
)
|
||||
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
|
||||
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
|
||||
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
|
||||
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
|
||||
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
|
||||
.replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
|
||||
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
|
||||
.replace(
|
||||
/\[payment_schedule\.cycle-in-words\]/g,
|
||||
`${cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` ||
|
||||
'',
|
||||
`${cycle == 0 ? "đặt cọc" : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || "",
|
||||
)
|
||||
.replace(
|
||||
/\[payment_schedule\.note\]/g,
|
||||
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
|
||||
);
|
||||
.replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
|
||||
}
|
||||
|
||||
function quillToEmailHtml(html) {
|
||||
return html
|
||||
// ALIGN
|
||||
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
||||
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
||||
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
||||
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
||||
return (
|
||||
html
|
||||
// ALIGN
|
||||
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
||||
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
||||
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
||||
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
||||
|
||||
// FONT SIZE
|
||||
.replace(/ql-size-small/g, '')
|
||||
.replace(/ql-size-large/g, '')
|
||||
.replace(/ql-size-huge/g, '')
|
||||
// FONT SIZE
|
||||
.replace(/ql-size-small/g, "")
|
||||
.replace(/ql-size-large/g, "")
|
||||
.replace(/ql-size-huge/g, "")
|
||||
|
||||
// REMOVE EMPTY CLASS
|
||||
.replace(/class=""/g, '')
|
||||
;
|
||||
// REMOVE EMPTY CLASS
|
||||
.replace(/class=""/g, "")
|
||||
);
|
||||
}
|
||||
|
||||
const showmodal = ref(null);
|
||||
|
||||
function openConfirmModal() {
|
||||
showmodal.value = {
|
||||
component: 'dialog/Confirm',
|
||||
title: 'Xác nhận',
|
||||
width: '500px',
|
||||
height: '100px',
|
||||
component: "dialog/Confirm",
|
||||
title: "Xác nhận",
|
||||
width: "500px",
|
||||
height: "100px",
|
||||
vbind: {
|
||||
content: 'Bạn có đồng ý gửi thông báo hàng loạt không?',
|
||||
}
|
||||
}
|
||||
content: "Bạn có đồng ý gửi thông báo hàng loạt không?",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function sendEmails() {
|
||||
isSending.value = true;
|
||||
$snackbar('Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...');
|
||||
$snackbar("Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...");
|
||||
|
||||
const paymentScheduleData = await $getdata(
|
||||
'payment_schedule',
|
||||
undefined,
|
||||
{
|
||||
filter: filter.value,
|
||||
sort: 'to_date',
|
||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||
}
|
||||
);
|
||||
const paymentScheduleData = await $getdata("payment_schedule", undefined, {
|
||||
filter: filter.value,
|
||||
sort: "to_date",
|
||||
values:
|
||||
"penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry",
|
||||
});
|
||||
|
||||
const emailTemplate = await $getdata(
|
||||
'emailtemplate',
|
||||
{ id: activeDateFilter.value.emailTemplate },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
|
||||
|
||||
let message = emailTemplate.content.content;
|
||||
|
||||
contents.value = paymentScheduleData.map(paymentSchedule => {
|
||||
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, '');
|
||||
|
||||
contents.value = paymentScheduleData.map((paymentSchedule) => {
|
||||
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
|
||||
const transfer = {
|
||||
bank: {
|
||||
code: 'MB',
|
||||
name: 'MB Bank',
|
||||
code: "MB",
|
||||
name: "MB Bank",
|
||||
},
|
||||
account: {
|
||||
number: '146768686868',
|
||||
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA',
|
||||
number: "146768686868",
|
||||
name: "CONG TY CO PHAN BAT DONG SAN UTOPIA",
|
||||
},
|
||||
content: 'Thanh toán đơn #xyz',
|
||||
content: "Thanh toán đơn #xyz",
|
||||
};
|
||||
|
||||
transfer.content = buildContentPayment(paymentSchedule);
|
||||
@@ -242,7 +230,7 @@ async function sendEmails() {
|
||||
...emailTemplate.content,
|
||||
content: undefined,
|
||||
message: replaceTemplateVars(message, paymentSchedule),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await forEachAsync(contents.value, async (bigContent, i) => {
|
||||
@@ -251,16 +239,16 @@ async function sendEmails() {
|
||||
content: toRaw(bigContent),
|
||||
previewMode: true,
|
||||
};
|
||||
|
||||
|
||||
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
||||
tempEm.content.message = quillToEmailHtml(message);
|
||||
let emailHtml = await render(Template1, tempEm);
|
||||
|
||||
|
||||
// If no image URL provided, remove image section from HTML
|
||||
if ((imageUrl ?? '').trim() === '') {
|
||||
if ((imageUrl ?? "").trim() === "") {
|
||||
emailHtml = emailHtml
|
||||
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '')
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
||||
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
|
||||
.replace(/\n\s*\n\s*\n/g, "\n\n");
|
||||
}
|
||||
|
||||
// Replace keywords in HTML
|
||||
@@ -268,55 +256,65 @@ async function sendEmails() {
|
||||
if (keyword && keyword.length > 0) {
|
||||
keyword.forEach(({ keyword, value }) => {
|
||||
if (keyword && value) {
|
||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g');
|
||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
|
||||
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const response = await $insertapi(
|
||||
'sendemail',
|
||||
"sendemail",
|
||||
{
|
||||
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
|
||||
content: finalEmailHtml,
|
||||
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || 'Thông báo từ Utopia Villas & Resort',
|
||||
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || "Thông báo từ Utopia Villas & Resort",
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
if (response !== null) {
|
||||
await $insertapi('productnote', {
|
||||
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
||||
user: $store.login.id,
|
||||
detail: `Đã gửi email thông báo nhắc nợ cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format('HH:mm ngày DD/MM/YYYY')}.`
|
||||
}, undefined, false);
|
||||
await $insertapi(
|
||||
"productnote",
|
||||
{
|
||||
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
||||
user: $store.login.id,
|
||||
detail: `Đã gửi email thông báo nhắc nợ cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format("HH:mm ngày DD/MM/YYYY")}.`,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
$snackbar('Thông báo đã được gửi thành công đến các khách hàng.');
|
||||
$snackbar("Thông báo đã được gửi thành công đến các khách hàng.");
|
||||
isSending.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
watch(filter, () => {
|
||||
key.value += 1;
|
||||
}, { deep: true })
|
||||
watch(
|
||||
filter,
|
||||
() => {
|
||||
key.value += 1;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
|
||||
<div class="buttons m-0">
|
||||
<p>Đến hạn:</p>
|
||||
<button
|
||||
<button
|
||||
v-for="payable in payables"
|
||||
:key="payable.id"
|
||||
@click="setDateFilter(payable.detail)"
|
||||
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
|
||||
>
|
||||
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày
|
||||
{{ payable.detail.lookup === "lte" ? "≤" : ">" }}
|
||||
{{ payable.detail.time }} ngày
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
v-if="activeDateFilter"
|
||||
@click="resetDateFilter()"
|
||||
class="button is-white"
|
||||
@@ -324,7 +322,7 @@ watch(filter, () => {
|
||||
Xoá lọc
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
<button
|
||||
v-if="activeDateFilter"
|
||||
@click="openConfirmModal()"
|
||||
:class="['button', 'is-light', { 'is-loading': isSending }]"
|
||||
@@ -342,16 +340,18 @@ watch(filter, () => {
|
||||
params: {
|
||||
filter,
|
||||
sort: 'to_date',
|
||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||
}
|
||||
}" />
|
||||
<Modal
|
||||
v-if="showmodal"
|
||||
v-bind="showmodal"
|
||||
@confirm="sendEmails()"
|
||||
@close="showmodal = undefined"
|
||||
values:
|
||||
'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<!-- <div class="is-flex is-gap-1">
|
||||
<Modal
|
||||
v-if="showmodal"
|
||||
v-bind="showmodal"
|
||||
@confirm="sendEmails()"
|
||||
@close="showmodal = undefined"
|
||||
/>
|
||||
<!-- <div class="is-flex is-gap-1">
|
||||
// debug
|
||||
<Template1
|
||||
v-if="contents"
|
||||
@@ -360,4 +360,4 @@ watch(filter, () => {
|
||||
previewMode
|
||||
/>
|
||||
</div> -->
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import Template1 from '@/lib/email/templates/Template1.vue';
|
||||
import { render } from '@vue-email/render';
|
||||
import { forEachAsync, isEqual } from 'es-toolkit';
|
||||
import Template1 from "@/lib/email/templates/Template1.vue";
|
||||
import { render } from "@vue-email/render";
|
||||
import { forEachAsync, isEqual } from "es-toolkit";
|
||||
|
||||
const {
|
||||
const {
|
||||
$dayjs,
|
||||
$getdata,
|
||||
$insertapi,
|
||||
@@ -19,8 +19,8 @@ const {
|
||||
const payables = ref(null);
|
||||
const defaultFilter = {
|
||||
status: 1,
|
||||
to_date__lt: $dayjs().format('YYYY-MM-DD'),
|
||||
}
|
||||
to_date__lt: $dayjs().format("YYYY-MM-DD"),
|
||||
};
|
||||
const filter = ref(defaultFilter);
|
||||
const activeDateFilter = ref(null);
|
||||
const key = ref(0);
|
||||
@@ -34,36 +34,42 @@ function resetDateFilter() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'overduepayables' }, sort: 'index' });
|
||||
const payablesData = await $getdata("bizsetting", undefined, {
|
||||
filter: { classify: "overduepayables" },
|
||||
sort: "index",
|
||||
});
|
||||
payables.value = payablesData;
|
||||
});
|
||||
|
||||
watch(activeDateFilter, (val) => {
|
||||
if (!val) {
|
||||
filter.value = defaultFilter;
|
||||
contents.value = null;
|
||||
} else {
|
||||
const cutoffDate = $dayjs().subtract(val.time, 'day').format('YYYY-MM-DD');
|
||||
const filterField = `to_date__${val.lookup === 'lte' ? 'gt' :
|
||||
'lte'}`;
|
||||
filter.value = {
|
||||
...defaultFilter,
|
||||
[filterField]: cutoffDate,
|
||||
watch(
|
||||
activeDateFilter,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
filter.value = defaultFilter;
|
||||
contents.value = null;
|
||||
} else {
|
||||
const cutoffDate = $dayjs().subtract(val.time, "day").format("YYYY-MM-DD");
|
||||
const filterField = `to_date__${val.lookup === "lte" ? "gt" : "lte"}`;
|
||||
filter.value = {
|
||||
...defaultFilter,
|
||||
[filterField]: cutoffDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const contents = ref(null);
|
||||
const isSending = ref(false);
|
||||
|
||||
function sanitizeContentPayment(text, maxLength = 80) {
|
||||
if (!text) return '';
|
||||
if (!text) return "";
|
||||
|
||||
return text
|
||||
.normalize('NFD') // bỏ dấu tiếng Việt
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-zA-Z0-9 _-]/g, '') // bỏ ký tự lạ
|
||||
.replace(/\s+/g, ' ')
|
||||
.normalize("NFD") // bỏ dấu tiếng Việt
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, maxLength);
|
||||
}
|
||||
@@ -88,19 +94,19 @@ const buildContentPayment = (data) => {
|
||||
cycle,
|
||||
} = data;
|
||||
|
||||
if (customerType.toLowerCase() === 'cn') {
|
||||
if (customerType.toLowerCase() === "cn") {
|
||||
if (customerName.length < 14) {
|
||||
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
||||
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
} else {
|
||||
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
||||
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
}
|
||||
} else {
|
||||
return `${productCode} ${customerCode} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`;
|
||||
return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
}
|
||||
};
|
||||
|
||||
function replaceTemplateVars(html, paymentScheduleItem) {
|
||||
const {
|
||||
const {
|
||||
txn_detail__transaction__product__trade_code,
|
||||
txn_detail__transaction__customer__code,
|
||||
txn_detail__transaction__customer__fullname,
|
||||
@@ -112,118 +118,99 @@ function replaceTemplateVars(html, paymentScheduleItem) {
|
||||
from_date,
|
||||
to_date,
|
||||
remain_amount,
|
||||
cycle
|
||||
cycle,
|
||||
} = paymentScheduleItem;
|
||||
return html
|
||||
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, '0') || '')
|
||||
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, '0') || '')
|
||||
.replace(/\[year]/g, new Date().getFullYear() || '')
|
||||
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
|
||||
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
|
||||
.replace(/\[year]/g, new Date().getFullYear() || "")
|
||||
.replace(/\[product\.trade_code\]/g, txn_detail__transaction__product__trade_code)
|
||||
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
|
||||
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
|
||||
.replace(
|
||||
/\[customer\.name\]/g,
|
||||
`${txn_detail__transaction__customer__type__code.toLowerCase() == 'cn' ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ''}` ||
|
||||
'',
|
||||
)
|
||||
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
|
||||
.replace(
|
||||
/\[customer\.legal_code\]/g,
|
||||
txn_detail__transaction__customer__legal_code || '',
|
||||
`${txn_detail__transaction__customer__type__code.toLowerCase() == "cn" ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ""}` ||
|
||||
"",
|
||||
)
|
||||
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || "")
|
||||
.replace(/\[customer\.legal_code\]/g, txn_detail__transaction__customer__legal_code || "")
|
||||
.replace(
|
||||
/\[customer\.contact_address\]/g,
|
||||
txn_detail__transaction__customer__contact_address ||
|
||||
txn_detail__transaction__customer__address ||
|
||||
'',
|
||||
txn_detail__transaction__customer__contact_address || txn_detail__transaction__customer__address || "",
|
||||
)
|
||||
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '')
|
||||
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '')
|
||||
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '')
|
||||
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '')
|
||||
.replace(
|
||||
/\[payment_schedule\.amount_in_word\]/g,
|
||||
$numberToVietnameseCurrency(remain_amount) || '',
|
||||
)
|
||||
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
|
||||
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || "")
|
||||
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || "")
|
||||
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || "")
|
||||
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || "")
|
||||
.replace(/\[payment_schedule\.amount_in_word\]/g, $numberToVietnameseCurrency(remain_amount) || "")
|
||||
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || "")
|
||||
.replace(
|
||||
/\[payment_schedule\.cycle-in-words\]/g,
|
||||
`${cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` ||
|
||||
'',
|
||||
`${cycle == 0 ? "đặt cọc" : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` || "",
|
||||
)
|
||||
.replace(
|
||||
/\[payment_schedule\.note\]/g,
|
||||
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
|
||||
);
|
||||
.replace(/\[payment_schedule\.note\]/g, `${cycle == 0 ? "Dat coc" : `Dot ${cycle}`}` || "");
|
||||
}
|
||||
|
||||
function quillToEmailHtml(html) {
|
||||
return html
|
||||
// ALIGN
|
||||
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
||||
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
||||
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
||||
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
||||
return (
|
||||
html
|
||||
// ALIGN
|
||||
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
||||
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
||||
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
||||
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
||||
|
||||
// FONT SIZE
|
||||
.replace(/ql-size-small/g, '')
|
||||
.replace(/ql-size-large/g, '')
|
||||
.replace(/ql-size-huge/g, '')
|
||||
// FONT SIZE
|
||||
.replace(/ql-size-small/g, "")
|
||||
.replace(/ql-size-large/g, "")
|
||||
.replace(/ql-size-huge/g, "")
|
||||
|
||||
// REMOVE EMPTY CLASS
|
||||
.replace(/class=""/g, '')
|
||||
;
|
||||
// REMOVE EMPTY CLASS
|
||||
.replace(/class=""/g, "")
|
||||
);
|
||||
}
|
||||
|
||||
const showmodal = ref(null);
|
||||
|
||||
function openConfirmModal() {
|
||||
showmodal.value = {
|
||||
component: 'dialog/Confirm',
|
||||
title: 'Xác nhận',
|
||||
width: '500px',
|
||||
height: '100px',
|
||||
component: "dialog/Confirm",
|
||||
title: "Xác nhận",
|
||||
width: "500px",
|
||||
height: "100px",
|
||||
vbind: {
|
||||
content: 'Bạn có đồng ý gửi thông báo hàng loạt không?',
|
||||
}
|
||||
}
|
||||
content: "Bạn có đồng ý gửi thông báo hàng loạt không?",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function sendEmails() {
|
||||
isSending.value = true;
|
||||
$snackbar('Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...');
|
||||
$snackbar("Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...");
|
||||
|
||||
const paymentScheduleData = await $getdata(
|
||||
'payment_schedule',
|
||||
undefined,
|
||||
{
|
||||
filter: filter.value,
|
||||
sort: 'to_date',
|
||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||
}
|
||||
);
|
||||
const paymentScheduleData = await $getdata("payment_schedule", undefined, {
|
||||
filter: filter.value,
|
||||
sort: "to_date",
|
||||
values:
|
||||
"penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry",
|
||||
});
|
||||
|
||||
const emailTemplate = await $getdata(
|
||||
'emailtemplate',
|
||||
{ id: activeDateFilter.value.emailTemplate },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
|
||||
|
||||
let message = emailTemplate.content.content;
|
||||
|
||||
contents.value = paymentScheduleData.map(paymentSchedule => {
|
||||
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, '');
|
||||
|
||||
contents.value = paymentScheduleData.map((paymentSchedule) => {
|
||||
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
|
||||
const transfer = {
|
||||
bank: {
|
||||
code: 'MB',
|
||||
name: 'MB Bank',
|
||||
code: "MB",
|
||||
name: "MB Bank",
|
||||
},
|
||||
account: {
|
||||
number: '146768686868',
|
||||
name: 'CONG TY CO PHAN BAT DONG SAN UTOPIA',
|
||||
number: "146768686868",
|
||||
name: "CONG TY CO PHAN BAT DONG SAN UTOPIA",
|
||||
},
|
||||
content: 'Thanh toán đơn #xyz',
|
||||
content: "Thanh toán đơn #xyz",
|
||||
};
|
||||
|
||||
transfer.content = buildContentPayment(paymentSchedule);
|
||||
@@ -242,7 +229,7 @@ async function sendEmails() {
|
||||
...emailTemplate.content,
|
||||
content: undefined,
|
||||
message: replaceTemplateVars(message, paymentSchedule),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await forEachAsync(contents.value, async (bigContent, i) => {
|
||||
@@ -251,16 +238,16 @@ async function sendEmails() {
|
||||
content: toRaw(bigContent),
|
||||
previewMode: true,
|
||||
};
|
||||
|
||||
|
||||
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
||||
tempEm.content.message = quillToEmailHtml(message);
|
||||
let emailHtml = await render(Template1, tempEm);
|
||||
|
||||
|
||||
// If no image URL provided, remove image section from HTML
|
||||
if ((imageUrl ?? '').trim() === '') {
|
||||
if ((imageUrl ?? "").trim() === "") {
|
||||
emailHtml = emailHtml
|
||||
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '')
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
||||
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "")
|
||||
.replace(/\n\s*\n\s*\n/g, "\n\n");
|
||||
}
|
||||
|
||||
// Replace keywords in HTML
|
||||
@@ -268,55 +255,65 @@ async function sendEmails() {
|
||||
if (keyword && keyword.length > 0) {
|
||||
keyword.forEach(({ keyword, value }) => {
|
||||
if (keyword && value) {
|
||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g');
|
||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
|
||||
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const response = await $insertapi(
|
||||
'sendemail',
|
||||
"sendemail",
|
||||
{
|
||||
to: paymentScheduleData[i].txn_detail__transaction__customer__email,
|
||||
content: finalEmailHtml,
|
||||
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || 'Thông báo từ Utopia Villas & Resort',
|
||||
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || "Thông báo từ Utopia Villas & Resort",
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
if (response !== null) {
|
||||
await $insertapi('productnote', {
|
||||
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
||||
user: $store.login.id,
|
||||
detail: `Đã gửi email thông báo quá hạn cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format('HH:mm ngày DD/MM/YYYY')}.`
|
||||
}, undefined, false);
|
||||
await $insertapi(
|
||||
"productnote",
|
||||
{
|
||||
ref: paymentScheduleData[i].txn_detail__transaction__product,
|
||||
user: $store.login.id,
|
||||
detail: `Đã gửi email thông báo quá hạn cho sản phẩm ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format("HH:mm ngày DD/MM/YYYY")}.`,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
$snackbar('Thông báo đã được gửi thành công đến các khách hàng.');
|
||||
$snackbar("Thông báo đã được gửi thành công đến các khách hàng.");
|
||||
isSending.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
watch(filter, () => {
|
||||
key.value += 1;
|
||||
}, { deep: true })
|
||||
watch(
|
||||
filter,
|
||||
() => {
|
||||
key.value += 1;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
|
||||
<div class="buttons m-0">
|
||||
<p>Quá hạn:</p>
|
||||
<button
|
||||
<button
|
||||
v-for="payable in payables"
|
||||
:key="payable.id"
|
||||
@click="setDateFilter(payable.detail)"
|
||||
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
|
||||
>
|
||||
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày
|
||||
{{ payable.detail.lookup === "lte" ? "≤" : ">" }}
|
||||
{{ payable.detail.time }} ngày
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
v-if="activeDateFilter"
|
||||
@click="resetDateFilter()"
|
||||
class="button is-white"
|
||||
@@ -324,7 +321,7 @@ watch(filter, () => {
|
||||
Xoá lọc
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
<button
|
||||
v-if="activeDateFilter"
|
||||
@click="openConfirmModal()"
|
||||
:class="['button', 'is-light', { 'is-loading': isSending }]"
|
||||
@@ -342,16 +339,18 @@ watch(filter, () => {
|
||||
params: {
|
||||
filter,
|
||||
sort: 'to_date',
|
||||
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||
}
|
||||
}" />
|
||||
<Modal
|
||||
v-if="showmodal"
|
||||
v-bind="showmodal"
|
||||
@confirm="sendEmails()"
|
||||
@close="showmodal = undefined"
|
||||
values:
|
||||
'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<!-- <div class="is-flex is-gap-1">
|
||||
<Modal
|
||||
v-if="showmodal"
|
||||
v-bind="showmodal"
|
||||
@confirm="sendEmails()"
|
||||
@close="showmodal = undefined"
|
||||
/>
|
||||
<!-- <div class="is-flex is-gap-1">
|
||||
// debug
|
||||
<Template1
|
||||
v-if="contents"
|
||||
@@ -360,4 +359,4 @@ watch(filter, () => {
|
||||
previewMode
|
||||
/>
|
||||
</div> -->
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user