Initial commit

This commit is contained in:
Viet An
2026-03-02 09:45:33 +07:00
commit d17a9e2588
415 changed files with 92113 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<template>
<div>
<div class="fixed-grid has-3-cols">
<div class="grid">
<div class="cell" v-for="rule in recordRules" :key="rule.code">
<label class="radio">
<input type="radio" name="answer" :value="rule.code" v-model="recordCurrent" />
{{ isVietnamese ? rule.vi : rule.en }}
</label>
</div>
</div>
</div>
</div>
</template>
<script setup>
const { $updateapi, $snackbar, $getdata, $store } = useNuxtApp();
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String,
});
const isVietnamese = computed(() => $store.lang.toLowerCase() === "vi");
const recordRules = ref([]);
const recordCurrent = ref({});
let current = {};
let foundCurrent = await $getdata(props.api, { category: "system", classify: "current", code: "rule" });
if (foundCurrent !== "error" && foundCurrent.length > 0) {
recordCurrent.value = foundCurrent[0].detail;
current.value = foundCurrent[0];
}
let foundRules = await $getdata(props.api, { category: "system", classify: "rules" });
if (foundRules !== "error" && foundRules.length > 0) {
recordRules.value = foundRules;
}
watch(recordCurrent, async (newVal) => {
current.value.detail = newVal;
let rs = await $updateapi(props.api, current.value, null);
if (rs === "error") return $snackbar(rs);
});
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div>
<div class="field is-horizontal">
<div class="field-body">
<div class="field is-narrow">
<label class="label">Code <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.code" id="code">
</p>
</div>
<div class="field">
<label class="label">Name <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.name">
</p>
</div>
</div>
</div>
<div class="field mt-5">
<label class="label">Index</label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.index">
</p>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">Save</button>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
const { $copy, $resetNull, $insertrow, $updaterow, $snackbar} = useNuxtApp()
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String
})
const emit = defineEmits(['close', 'modalevent'])
var record = $copy( props.row || {})
async function update() {
let data = $resetNull(record)
let rs = data.id? await $updaterow(props.api, data, undefined, props.pagename)
: await $insertrow(props.api, data, undefined, props.pagename)
if(rs==='error') return $snackbar(rs)
emit('modalevent', {name: 'dataevent', data: rs})
emit('close')
}
onMounted(()=> {
document.getElementById('code').focus()
})
</script>

View File

@@ -0,0 +1,188 @@
<template>
<div class="columns is-multiline is-mobile mx-0">
<div class="column is-1">
<div class="field">
<label class="label has-text-left">ID<span class="has-text-danger ml-1">*</span></label>
<div class="control">
<input class="input" type="text" v-model="record.id" disabled>
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label has-text-left">Category<span class="has-text-danger ml-1">*</span></label>
<div class="control">
<input class="input" type="text" v-model="record.category">
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label has-text-left">Classify<span class="has-text-danger ml-1">*</span></label>
<div class="control">
<input class="input" type="text" v-model="record.classify">
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label has-text-left">Code<span class="has-text-danger ml-1">*</span></label>
<div class="control">
<input class="input" type="text" v-model="record.code">
</div>
</div>
</div>
<div class="column is-2">
<div class="field">
<label class="label has-text-left">Index</label>
<div class="control">
<input class="input" type="text" v-model="record.index">
</div>
</div>
</div>
<div class="column is-12">
<label class="label has-text-left">VI<span class="has-text-danger ml-1">*</span></label>
<div class="field has-addons">
<div class="control is-expanded">
<textarea class="textarea" placeholder="" rows="3" v-model="record.vi"></textarea>
</div>
<div class="control ml-5">
<a @click="openEditor()">
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 22}"></SvgIcon>
</a>
</div>
</div>
</div>
<!-- <div class="column is-12">
<label class="label has-text-left">EN<span class="has-text-danger ml-1">*</span></label>
<div class="field has-addons">
<div class="control is-expanded">
<textarea class="textarea" placeholder="" rows="3" v-model="record.en"></textarea>
</div>
<div class="control ml-5">
<a @click="openEditor()">
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 22}"></SvgIcon>
</a>
</div>
</div>
</div> -->
<div class="column is-6">
<div class="field">
<label class="label has-text-left">Image</label>
<div class="control">
<input class="input" type="text" v-model="record.image">
</div>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label has-text-left">Link</label>
<div class="control">
<input class="input" type="text" v-model="record.link">
</div>
</div>
</div>
<div class="column is-12">
<label class="label has-text-left">Detail</label>
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" type="text" placeholder="" v-model="record.detail">
</div>
<div class="control">
<button class="button" @click="editDetail('detail')">
<span>
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 17}"></SvgIcon>
</span>
</button>
</div>
<div class="control">
<button class="button" @click="copyDetail('detail')">
Copy
</button>
</div>
<div class="control">
<button class="button" @click="pasteDetail('detail')">
Paste
</button>
</div>
</div>
</div>
<!-- <div class="column is-6">
<label class="label has-text-left">Detail EN</label>
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" type="text" placeholder="" v-model="record.detail_en">
</div>
<div class="control">
<button class="button" @click="editDetail('detail_en')">
<span>
<SvgIcon v-bind="{name: 'pen1.svg', type: 'gray', size: 17}"></SvgIcon>
</span>
</button>
</div>
<div class="control">
<button class="button" @click="copyDetail('detail_en')">
Copy
</button>
</div>
<div class="control">
<button class="button" @click="pasteDetail('detail_en')">
Paste
</button>
</div>
</div>
</div> -->
<div class="column is-12 pt-5">
<a class="button is-primary has-text-white" @click="updateData()">Save</a>
<a class="button is-dark has-text-white ml-5" @click="updateData(true)" v-if="record.id">Create new</a>
</div>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" @texteditor="updateText" @update="updateAttr" v-if="showmodal"></Modal>
</template>
<script setup>
const emit = defineEmits([])
var props = defineProps({
pagename: String,
row: Object,
api: String
})
const { $copy, $resetNull, $insertrow, $updaterow, $copyToClipboard, $empty } = useNuxtApp()
var record = ref(props.row? $copy(props.row) : {})
var showmodal = ref()
var vapi = props.api
var current
var updateText = function(content) {
record.value.vi = content
}
var openEditor = function() {
showmodal.value = {component: 'common/TextEditor', vbind: {content: record.value.vi}, title: 'Text editor', width: '40%', height: '150px'}
}
var editDetail = function(attr) {
current = attr
let detail = record.value[attr]? record.value[attr] : {}
showmodal.value = {component: 'datatable/FieldAttribute', vbind: {field: detail, close: true},
title: 'Change attributes', width: '40%', height: '150px'}
}
var updateAttr = function(detail) {
record.value[current] = detail
}
function copyDetail(attr) {
if($empty(record.value[attr])) return
let val = typeof record.value[attr]=='string'? record.value[attr] : JSON.stringify(record.value[attr])
$copyToClipboard(val)
}
async function pasteDetail(attr) {
let text = await navigator.clipboard.readText()
if($empty(text)) return
record.value[attr] = JSON.parse(text)
}
var updateData = async function(isNew) {
let ele = record.value
if(ele.create_time===null) ele.create_time = new Date()
ele = $resetNull(ele)
if(isNew) delete ele.id
let result = ele.id? await $updaterow(vapi, ele, undefined, props.pagename)
: await $insertrow(vapi, ele, undefined, props.pagename)
if(isNew) emit('close')
}
</script>

View File

@@ -0,0 +1,73 @@
<template>
<div>
<div class="field is-horizontal">
<div class="field-body">
<div class="field is-narrow">
<label class="label"> <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.code" id="code">
</p>
</div>
<div class="field">
<label class="label">Tên <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.name">
</p>
</div>
</div>
</div>
<div class="field is-horizontal mt-5">
<div class="field-body">
<div class="field is-narrow">
<label class="label">Điện thoại <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.phone" id="code">
</p>
</div>
<div class="field">
<label class="label">Email <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.email">
</p>
</div>
</div>
</div>
<div class="field is-horizontal mt-5">
<div class="field-body">
<div class="field">
<label class="label">Địa chỉ <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.address" id="code">
</p>
</div>
</div>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">Lưu lại</button>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
const { $copy, $resetNull, $insertrow, $updaterow, $snackbar, $updatepage} = useNuxtApp()
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String
})
const emit = defineEmits(['close', 'modalevent'])
var record = $copy( props.row || {})
async function update() {
let data = $resetNull(record)
let rs = data.id? await $updaterow(props.api, data, undefined, props.pagename)
: await $insertrow(props.api, data, undefined, props.pagename)
$updatepage(props.pagename, rs)
if(rs==='error') return $snackbar(rs)
emit('modalevent', {name: 'dataevent', data: rs})
emit('close')
}
onMounted(()=> {
document.getElementById('code').focus()
})
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div class="content-delete-cart">
<div class="container is-fluid px-4">
<div v-if="productRes.length">
<p>
Không thể thực hiện thao tác giỏ hàng vẫn còn sản phẩm. Vui lòng xóa toàn bộ sản phẩm trong giỏ hàng trước
khi tiếp tục.
</p>
</div>
<div v-else>
<p>
Bạn chắc chắn muốn xóa giỏ hàng [{{ props.row.code.toUpperCase() }} {{ props.row.name.toUpperCase() }}]
không?
</p>
<div class="action mt-3">
<button class="button is-light" @click="handleCancel">Hủy</button>
<button class="button is-primary" @click="handleDeleteCart">Đồng ý</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
const { $snackbar, $getdata, $deleteapi, $copy } = useNuxtApp();
const emit = defineEmits(["close"]);
const props = defineProps({
row: Object,
});
const productRes = await $getdata("product", { cart: props.row.id });
const cartRes = await $getdata("cart");
$copy("cart", cartRes);
const handleDeleteCart = async () => {
const res = await $deleteapi("cart", props.row.id);
if (res) {
emit("close");
$snackbar("Xóa giỏ hàng thành công");
}
};
const handleCancel = () => {
emit("close");
};
</script>
<style>
.content-delete-cart .action .button + .button {
margin-left: 20px;
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div class="content-delete-cart">
<div class="container is-fluid px-4">
<div>
<p>
Bạn chắc chắn muốn xóa lịch công nợ thời gian: {{ detail.time }} ngày - mẫu: [{{
detail.name?.toUpperCase()
}}] không?
</p>
<div class="action mt-3">
<button class="button is-light" @click="handleCancel">Hủy</button>
<button class="button is-primary" @click="handleDeleteCart">Đồng ý</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
const { $snackbar, $deleteapi } = useNuxtApp();
const emit = defineEmits(["close"]);
const props = defineProps({
row: Object,
});
const detail = JSON.parse(props.row?.detail || null);
const handleDeleteCart = async () => {
const res = await $deleteapi("bizsetting", props.row.id);
if (res) {
emit("close");
$snackbar("Xóa lịch công nợ thành công");
}
};
const handleCancel = () => {
emit("close");
};
</script>
<style>
.content-delete-cart .action .button + .button {
margin-left: 20px;
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div>
<div class="field is-horizontal">
<div class="field-body">
<div class="field is-narrow">
<label class="label"> chiết khấu <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.code" id="code" autocomplete="off" />
</p>
</div>
<div class="field">
<label class="label">Tên chiết khấu<span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.name" autocomplete="off" />
</p>
</div>
</div>
</div>
<div class="mt-5 columns">
<div class="column">
<label class="label">Giá trị chiết khấu <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.value" autocomplete="off" />
</p>
</div>
<div class="column">
<label class="label">Loại chiết khấu <span class="has-text-danger"> * </span> </label>
<SearchBox
v-bind="{ api: 'valuetype', field: 'name', column: ['name'], first: true, optionid: record.type }"
:disabled="record.type"
@option="documentSelected('type', $event)"
></SearchBox>
</div>
<div class="column">
<label class="label">Phương thức chiết khấu<span class="has-text-danger"> * </span> </label>
<SearchBox
v-bind="{ api: 'discountmethod', field: 'name', column: ['name'], first: true, optionid: record.method }"
:disabled="record.method"
@option="documentSelected('method', $event)"
></SearchBox>
</div>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">
{{ record.id ? "Cập nhật" : "Tạo mới" }}
</button>
</div>
</div>
</template>
<script setup>
import { onMounted } from "vue";
const { $copy, $resetNull, $insertrow, $updaterow, $snackbar, $getdata } = useNuxtApp();
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String,
});
const emit = defineEmits(["close", "modalevent"]);
var record = $copy(props.row || {});
console.log("record", record);
function documentSelected(attr, obj, v) {
record[attr] = obj.id;
}
async function update() {
let data = $resetNull(record);
let rs = data.id
? await $updaterow(props.api, data, undefined, props.pagename)
: await $insertrow(props.api, data, undefined, props.pagename);
if (rs === "error") return $snackbar(rs);
emit("modalevent", { name: "dataevent", data: rs });
emit("close");
}
onMounted(() => {
document.getElementById("code").focus();
});
</script>

View File

@@ -0,0 +1,94 @@
<template>
<div class="due-payable">
<div class="mt-5 columns">
<div class="column is-2">
<label class="label"
>{{ isVietnamese ? "Thời gian (ngày)" : "Duration (Days)" }} <span class="has-text-danger"> * </span>
</label>
<p class="control">
<input class="input" type="number" placeholder="15,30,60" v-model="record.time" autocomplete="off" />
</p>
</div>
<div class="column is-2">
<label class="label"
>{{ isVietnamese ? "So sánh" : "Lookup Field" }} <span class="has-text-danger"> * </span>
</label>
<p class="control">
<input class="input" placeholder="gt,lt,gte,lte" v-model="record.lookup" autocomplete="off" />
</p>
</div>
<div class="column">
<label class="label"
>{{ isVietnamese ? "Mẫu email" : "Email Template" }} <span class="has-text-danger"> * </span>
</label>
<SearchBox
v-bind="{
api: 'Email_Template',
field: 'name',
column: ['name'],
first: true,
optionid: Number(record.emailTemplate),
}"
@option="documentSelected('emailTemplate', $event)"
></SearchBox>
</div>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">
{{ dataTemp.id ? "Cập nhật" : "Tạo mới" }}
</button>
</div>
</div>
</template>
<script setup>
const { $copy, $insertrow, $updaterow, $snackbar, $store } = useNuxtApp();
const isVietnamese = computed(() => $store.lang.toLowerCase() === "vi");
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String,
});
const emit = defineEmits(["close", "modalevent"]);
const dataTemp = $copy(props.row || {});
let record = $copy(props.row?.detail || {});
function documentSelected(attr, obj, v) {
record[attr] = obj.id;
record.name = obj.name;
}
async function update() {
const detail = record;
let rs;
if (dataTemp.id) {
const dataUpdate = {
...dataTemp,
detail,
};
console.log('detail', detail)
console.log('dataTemp', dataTemp)
console.log('dataUpdate', dataUpdate)
rs = await $updaterow(props.api, dataUpdate, undefined, props.pagename);
} else {
const dataInsert = {
detail,
category: "system",
classify: "duepayables",
code: `due-${record.lookup}-${record.time}d-${record.emailTemplate}`,
vi: "Lịch đến hạn thanh toán",
};
rs = await $insertrow(props.api, dataInsert, undefined, props.pagename);
}
if (rs === "error") return $snackbar(rs);
emit("modalevent", { name: "dataevent", data: rs });
emit("close");
}
</script>
<!-- <div>{{ row.detail ? JSON.parse(row.detail)?.time : '' }}</div>
<div>{{ await $getdata('Email_Template', {id:JSON.parse(row.detail)?.emailTemplate})}}</div> -->

View File

@@ -0,0 +1,60 @@
<template>
<div>
<div class="field is-horizontal">
<div class="field-body">
<div class="field is-narrow">
<label class="label"> quà tặng <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.code" id="code" autocomplete="off" />
</p>
</div>
<div class="field">
<label class="label">Tên quà tặng<span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.name" autocomplete="off" />
</p>
</div>
</div>
</div>
<div class="mt-5 columns">
<div class="column">
<label class="label">Chi tiết quà tặng<span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.detail" autocomplete="off"/>
</p>
</div>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">
{{ record.id ? "Cập nhật" : "Tạo mới" }}
</button>
</div>
</div>
</template>
<script setup>
import { onMounted } from "vue";
const { $copy, $resetNull, $insertrow, $updaterow, $snackbar, $getdata } = useNuxtApp();
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String,
});
const emit = defineEmits(["close", "modalevent"]);
var record = $copy(props.row || {});
async function update() {
let data = $resetNull(record);
let rs = data.id
? await $updaterow(props.api, data, undefined, props.pagename)
: await $insertrow(props.api, data, undefined, props.pagename);
if (rs === "error") return $snackbar(rs);
emit("modalevent", { name: "dataevent", data: rs });
emit("close");
}
onMounted(() => {
document.getElementById("code").focus();
});
</script>

View File

@@ -0,0 +1,549 @@
<template>
<div class="field is-grouped">
<div class="control" id="ignore">
<label class="file-label">
<input class="file-input" type="file" id="divfile" name="resume" @change="inputFile" />
<span class="file-cta px-2 has-background-primary">
<span class="icon-text is-clickable">
<SvgIcon v-bind="{ name: 'attach-file.svg', type: 'white', size: 22 }"></SvgIcon>
<span class="has-text-white">Chọn file</span>
</span>
</span>
</label>
</div>
<div class="control">
<a class="button is-primary" @click="download()"> Tải template </a>
<a class="button is-light ml-6" v-if="datafile" @click="exportExcel()">Xuất Excel</a>
</div>
</div>
<div class="columns mb-0" v-if="msgInfo.length > 0">
<div class="column is-7 mb-0">
<div class="notification is-white py-2 mb-2">
<button class="delete" @click="msgInfo = []"></button>
<div style="max-height: 250px; overflow-y: auto;">
<article class="media py-0 my-0" v-for="(ele, key) in msgInfo" :key="key">
<figure class="media-left">
<i :class="classinfo.find(v => v.type === ele.type).icon_class"> </i>
</figure>
<div class="media-content">
<div class="content">
<p :class="classinfo.find(v => v.type == ele.type).text_class"> {{ ele.message }} </p>
</div>
</div>
</article>
</div>
</div>
<p v-if="result">
<span class="has-text-primary fsb-18">{{ `Thành công: ${result.success_count || 0}` }} / {{ total || ''
}}</span>
<span class="has-text-danger fsb-18 ml-5">{{ `Lỗi: ${result.error_count || 0}` }} / {{ total || '' }}</span>
</p>
</div>
<div class="column has-text-centered mb-0" v-if="ready">
<a :class="`button is-primary ${isloading ? 'is-loading' : ''}`" @click="insert()" v-if="countNew > 0">Thêm mới
({{ countNew }}/{{ total }})</a>
<a :class="`button is-dark ${isloading ? 'is-loading' : ''} ml-2`" @click="update()" v-if="countUdt > 0">Cập nhật
({{ countUdt }}/{{ total }})</a>
</div>
</div>
<DataTable v-bind="{ pagename: pagename }" v-if="pagedata" />
</template>
<script>
export default {
props: ['code'],
data() {
return {
classinfo: [
{ type: 'success', icon_class: 'mdi mdi-check', text_class: 'has-text-primary' },
{ type: 'error', icon_class: 'mdi mdi-close has-text-danger', text_class: 'has-text-danger' },
{ type: 'warning', icon_class: 'mdi mdi-alert has-text-warning', text_class: 'has-text-warning' },
{ type: 'waiting', icon_class: 'mdi mdi-timer-sand has-text-primary', text_class: 'has-text-primary' }
],
msgInfo: [],
isloading: false,
datafile: undefined,
requireFields: [],
pagename: 'importdata',
file: undefined,
ready: false,
data: [],
setting: undefined,
countUdt: undefined,
countNew: undefined,
total: undefined,
fileInfo: undefined,
fileLog: undefined,
result: undefined
}
},
async created() {
this.setting = await this.$getdata('importsetting', { code: this.code }, undefined, true)
this.requireFields = this.setting.detail
},
beforeUnmount() {
this.pagedata = undefined
},
computed: {
pagedata: {
get: function () { return this.$store[this.pagename] },
set: function (val) { this.$store.commit(this.pagename, val) }
}
},
methods: {
download() {
window.open(`${this.$getpath()}static/excelform/${this.setting.template}`, '_blank')
},
async inputFile(v) {
this.isloading = true
this.msgInfo = []
let fileList = document.getElementById('divfile').files
let files = Array.from(fileList)
if (files.length === 0) return
var thefile = this.$upload(files[0], 'file', 1)
if (thefile.error) {
this.$buefy.toast.open({ message: thefile.text, type: 'is-danger' })
return this.isloading = false
}
if (!(thefile.name.search('.xls') > 0 || thefile.name.search('.xlsx') > 0)) {
let text = this.$find(this.$store.syspara, { category: 'inform', classify: 'import', code: 'file-type-invalid' }, 'value')
this.msgInfo.push({ message: text, type: 'error' })
return this.isloading = false
}
let result = await this.$insertapi('upload', thefile.form, undefined, false)
if (result === 'error') {
let text = this.$find(this.$store.syspara, { category: 'inform', classify: 'import', code: 'file-upload-fail' }, 'value')
this.msgInfo.push({ message: text, type: 'error' })
} else {
this.fileLog = result
this.fileInfo = thefile
this.fillData()
}
this.isloading = false
// Clear file
document.getElementById("divfile").value = ''
},
async fillData() {
let found = this.$findapi('readexcel')
found.params = { name: this.fileInfo.filename }
let result = await this.$getapi([found])
this.datafile = JSON.parse(result[0].data)
let fields = []
this.datafile.schema.fields.forEach(ele => {
let field = this.$createField(ele.name, ele.name, 'string', true)
if (field.name !== 'index') fields.push(field)
})
let copy = this.pagedata ? this.$copy(this.pagedata) : this.$getpage()
let data = this.datafile.data
data.map(v => v.updater = this.$store.login.id)
copy.update = { fields: fields, data: data }
copy.data = data
copy.fields = fields
this.pagedata = copy
this.checkRequireFields(fields)
},
checkRequireFields(fields) {
let misslist = []
Object.keys(this.requireFields).map(v => {
let found = fields.find(x => x.name === v)
if (!found) misslist.push(v)
})
if (misslist.length > 0) {
let text = `Thiếu các cột bắt buộc: ${misslist.join(', ')}`
this.msgInfo.push({ message: text, type: 'error' })
} else {
this.validateFontend()
}
},
validateFontend() {
let self = this
var checkValid = function (name, obj, data) {
var error = false
if (obj.empty === 'no' || !self.$empty(obj.api)) {
data.map(x => {
if (self.$empty(x[name])) {
x['error'] = `${name} không được bỏ trống`
error = true
}
})
}
if (!error && obj.type === 'number') {
data.map(x => {
if (self.$empty(x[name])) x[name] = null
else {
if (self.$isNumber(x[name])) x[name] = self.$formatNumber(x[name])
else {
x['error'] = `${name} không phải là số`
error = true
}
}
})
}
if (!error && obj.type === 'date') {
data.map(x => {
if (self.$empty(x[name])) x[name] = null
else {
if (self.$dayjs(x[name], 'YYYY/MM/DD').isValid()) x[name] = self.$dayjs(x[name]).format('YYYY-MM-DD')
else {
x['error'] = `${name} không phải ngày hợp lệ`
error = true
}
}
})
}
if (!error && obj.type === 'string') {
data.map(x => x[name] = self.$empty(x[name]) ? null : x[name])
}
return [error, data]
}
let err = false
let data = this.$copy(this.pagedata.data)
Object.keys(this.requireFields).map(v => {
let obj = this.requireFields[v]
let result = checkValid(v, obj, data)
data = result[1]
if (result[0] === true) err = true
})
if (err) this.showError(data)
else {
if (data.length > 5000) {
this.msgInfo.push({ message: `Tổng số bản ghi là ${data.length}. Chỉ hiển thị 5000 trong bảng dữ liệu`, type: 'warning' })
} else {
this.msgInfo.push({ message: `Tổng số bản ghi là ${data.length}`, type: 'success' })
}
let copy = this.$copy(this.pagedata)
copy.update = { data: data.length > 5000 ? data.slice(0, 5000) : data }
this.pagedata = copy
this.data = data
this.findKey(data)
}
},
showError(data) {
data = data.filter(v => v.error)
this.msgInfo.push({ message: 'Dữ liệu có lỗi. Hãy kiểm tra lại', type: 'error' })
let field = this.$createField('error', 'error', 'string', true, true)
field.color = 'red'
let fields = this.$copy(this.pagedata.fields)
fields = [field].concat(fields)
let copy = this.$clone(this.pagedata)
copy.data = data
copy.fields = fields
copy.update = { data: data, fields: fields }
this.pagedata = copy
},
// ✅ METHOD CHÍNH: TÌM KEY VÀ KIỂM TRA UPDATE PERMISSION
async findKey(rows) {
this.msgInfo.push({ message: 'Bắt đầu kiểm tra dữ liệu', type: 'waiting' });
let keys = [];
let related = [];
// 1. Tạo dữ liệu gửi lên: Dùng chính tên cột Excel để chứa giá trị
let mappedRows = rows.map(row => {
let newRow = { ...row };
for (const [excelColName, config] of Object.entries(this.requireFields)) {
if (excelColName in row) {
newRow[excelColName] = row[excelColName];
}
}
return newRow;
});
// 2. Cấu hình Mapping cho Backend
for (const [excelColName, value] of Object.entries(this.requireFields)) {
if (value.key === 'yes') {
keys.push({
key: excelColName,
value: value
});
}
// Nếu là trường Lookup
if (value.api) {
related.push({
key: excelColName,
value: value
});
}
}
let conn = this.$findapi(this.setting.api);
let payload = {
name: conn.url.replace('data/', '').replace(/\//g, ''),
data: mappedRows,
keys: keys,
related: related
};
try {
let rs = await this.$insertapi('findkey', payload);
if (rs && rs.data) {
this.data = rs.data;
this.total = this.data.length;
// ✅ KIỂM TRA CẤU HÌNH UPDATE
this.checkUpdatePermission();
// 1. Lọc ra danh sách các lỗi
let errors = this.data
.map((v, i) => (v.error ? { line: i + 1, msg: v.error } : null))
.filter(v => v !== null);
this.countNew = this.data.filter(v => !v.id && !v.error).length;
this.countUdt = this.data.filter(v => v.id && !v.error).length;
this.ready = true;
// 2. Nếu có lỗi, hiển thị chi tiết từng dòng vào msgInfo
if (errors.length > 0) {
errors.forEach(err => {
this.msgInfo.push({
message: `Dòng ${err.line}: ${err.msg}`,
type: 'error'
});
});
this.msgInfo.push({
message: `Phát hiện tổng cộng ${errors.length} lỗi. Vui lòng sửa file và upload lại.`,
type: 'warning'
});
} else {
this.msgInfo.push({
message: `Kiểm tra hoàn tất: Toàn bộ ${this.total} dòng dữ liệu hợp lệ.`,
type: 'success'
});
}
this.showStatus();
}
} catch (e) {
this.isloading = false;
this.msgInfo.push({ message: 'Lỗi hệ thống khi kiểm tra', type: 'error' });
}
},
checkUpdatePermission() {
// Tìm các trường key có update = "no"
let noUpdateFields = [];
Object.entries(this.requireFields).forEach(([excelColName, config]) => {
// Nếu là trường key và update = "no"
// Mặc định nếu không khai báo update thì coi như "yes"
if (config.key === 'yes' && config.update === 'no') {
noUpdateFields.push({
name: excelColName,
field: config.field || excelColName
});
}
});
// Nếu không có trường nào cấm update thì bỏ qua
if (noUpdateFields.length === 0) {
return;
}
// Kiểm tra từng bản ghi
this.data.forEach((row, index) => {
// Nếu bản ghi đã tồn tại (có id) và chưa có lỗi
if (row.id && !row.error) {
// Lấy danh sách các trường key bị trùng
let duplicatedFields = [];
noUpdateFields.forEach(field => {
// Kiểm tra xem trường này có giá trị không
if (row[field.name]) {
duplicatedFields.push(field.name);
}
});
// Nếu có trường nào bị trùng, đánh dấu lỗi
if (duplicatedFields.length > 0) {
row.error = `Bản ghi đã tồn tại với ${duplicatedFields.join(', ')}. Không được phép cập nhật.`;
}
}
});
},
showStatus() {
let field = this.$createField('record_status', 'Status', 'string', true, true)
field.color = 'blue'
let fields = this.$copy(this.pagedata.fields)
fields.push(field)
this.$store.commit('updateState', { name: this.pagename, key: 'fields', data: fields })
let data = this.$copy(this.pagedata.data)
data.map((v, i) => v.record_status = this.data[i].id ? 'Tồn tại' : 'Mới')
this.$store.commit('updateState', { name: this.pagename, key: 'data', data: data })
let copy = this.$clone(this.pagedata)
this.pagedata = undefined
setTimeout(() => this.pagedata = copy, 10)
},
//Helper: Xóa trường lookup (giữ lại id của mapping)
cleanupLookupFields(data) {
return data.map(row => {
let newRow = {};
Object.entries(this.requireFields).forEach(([excelColName, config]) => {
let dbFieldName = config.field || config.field;
if (dbFieldName && row[excelColName] !== undefined) {
newRow[dbFieldName] = row[excelColName];
}
});
Object.keys(row).forEach(key => {
if (key !== 'index' && !this.requireFields[key]) {
newRow[key] = row[key];
}
});
return newRow;
});
},
async insert() {
this.isloading = true
this.result = undefined
let conn = this.$findapi(this.setting.api)
let obj = { code: this.$id(), model: conn.url.replace('data/', '').replace('/', ''), file: this.fileInfo.name, fields: this.setting.detail }
await this.$insertapi('importlog', obj)
var interval = setInterval(() => this.getResult(obj.code), 2000)
//Lọc bản ghi KHÔNG có id (bản ghi mới)
let filter = this.data.filter(v => !v.id && !v.error)
//Xóa trường lookup trước khi gửi
filter = this.cleanupLookupFields(filter)
filter = filter.map(v => this.$resetNull(v))
this.total = filter.length
//Gửi lên API - Backend sẽ INSERT
let result
if (this.setting.call_api) {
result = await this.$insertapi(this.setting.call_api, { data: filter, user: this.$store.login.id }, undefined, obj.code)
} else {
result = await this.$insertapi(this.setting.api, filter, undefined, obj.code)
}
this.isloading = false
clearInterval(interval)
this.getResult(obj.code)
// Xử lý result/response mới
if (result === 'error') {
this.msgInfo.push({ message: 'Có lỗi hệ thống khi thực hiện', type: 'error' })
return
}
let hasError = false
let errorMessages = []
// Kiểm tra entries có lỗi không
if (result.entries && Array.isArray(result.entries)) {
result.entries.forEach(entry => {
if (entry.error) {
hasError = true
errorMessages.push(entry.error)
}
})
}
// Nếu có message chứa từ khóa lỗi (dự phòng)
if (result.message && (result.message.toLowerCase().includes('lỗi') || result.message.toLowerCase().includes('không tồn tại'))) {
hasError = true
errorMessages.push(result.message)
}
if (hasError) {
// Hiển thị lỗi chi tiết
errorMessages.forEach(msg => {
this.msgInfo.push({ message: msg, type: 'error' })
})
this.msgInfo.push({
message: 'Có lỗi trong quá trình xử lý. Vui lòng kiểm tra và thử lại.',
type: 'warning'
})
} else {
// Thành công thật sự
const successMsg = this.countNew
? `Thêm mới thành công: ${this.countNew} bản ghi`
: `Cập nhật thành công: ${this.countUdt} bản ghi`
this.msgInfo.push({ message: successMsg, type: 'success' })
this.msgInfo.push({ message: result.message || 'Hoàn tất.', type: 'success' })
// Chỉ emit close khi KHÔNG có lỗi
setTimeout(() => {
this.$emit('close')
}, 1500)
}
// Luôn cập nhật count về undefined để nút biến mất
this.countNew = undefined
this.countUdt = undefined
},
async update() {
this.isloading = true
this.result = undefined
let conn = this.$findapi(this.setting.api)
let obj = { code: this.$id(), model: conn.url.replace('data/', '').replace('/', ''), file: this.fileInfo.name, fields: this.setting.detail }
await this.$insertapi('importlog', obj)
var interval = setInterval(() => this.getResult(obj.code), 2000)
//Lọc bản ghi CÓ id (bản ghi tồn tại) VÀ KHÔNG CÓ LỖI
let filter = this.data.filter(v => v.id && !v.error)
//Xóa trường lookup trước khi gửi
filter = this.cleanupLookupFields(filter)
filter = filter.map(v => this.$resetNull(v))
this.total = filter.length
//Gửi lên API - Backend sẽ UPDATE
let result = await this.$insertapi(this.setting.call_api || this.setting.api, filter, undefined, obj.code)
this.isloading = false
clearInterval(interval)
this.getResult(obj.code)
if (result === 'error') {
return this.msgInfo.push({ message: 'Có lỗi xảy ra', type: 'error' })
}
let hasError = false
let errorMessages = []
// Kiểm tra có lỗi trong result không
if (Array.isArray(result)) {
result.forEach(item => {
if (item.error) {
hasError = true
errorMessages.push(item.error || JSON.stringify(item.note))
}
})
}
if (hasError) {
errorMessages.forEach(msg => {
this.msgInfo.push({ message: msg, type: 'error' })
})
} else {
this.msgInfo.push({ message: `Cập nhật thành công: ${this.countUdt} bản ghi`, type: 'success' })
this.countUdt = undefined
this.$emit('modalevent', { name: 'reload' })
}
},
async getResult(logcode) {
let found = this.$findapi('importlog')
found.params.filter = { code: logcode }
let rs = await this.$getapi([found])
this.result = rs[0].data.rows[0]
},
exportExcel() {
let dataType = {}
this.pagedata.fields.map(v => dataType[v.label.indexOf('>') >= 0 ? v.name : v.label] = 'String')
let data = this.pagedata.dataFilter || this.pagedata.data
this.$exportExcel(data, 'data-export', this.pagedata.fields.map(v => v.name), dataType)
}
}
}
</script>

View File

@@ -0,0 +1,153 @@
<template>
<div class="columns">
<div class="column is-3">
<div class="field">
<label class="label">Code</label>
<div class="control">
<input class="input" type="text" placeholder="" v-model="current.code">
</div>
</div>
<div class="field mt-4">
<label class="label">Name</label>
<div class="control">
<input class="input" type="text" placeholder="" v-model="current.name">
</div>
</div>
<div class="field mt-4">
<label class="label">Api</label>
<div class="control">
<input class="input" type="text" placeholder="" v-model="current.api">
</div>
</div>
<div class="mt-5 py-2 border-bottom">
<a @click="updateFromModel()">Thêm cài đặt cột từ Model</a>
</div>
<div class="py-2 border-bottom">
<a @click="createUserSetting()">Tạo thiết lập (user setting)</a>
</div>
<div class="buttons mt-5">
<button class="button is-primary" @click="saveData()">Lưu lại</button>
</div>
</div>
<div class="column is-5">
<DataTable v-bind="{pagename: pagename1}" @delete="remove" @open="showAttr()" v-if="pagedata1" />
</div>
<div class="column is-4">
<DataTable v-bind="{pagename: pagename2}" v-if="pagedata2" />
</div>
</div>
<Modal @close="showmodal=undefined" @update="updateAttr" v-bind="showmodal" v-if="showmodal" />
</template>
<script>
export default {
props: ['row'],
data() {
return {
data: [],
current: {},
showmodal: undefined,
report: undefined,
setting: {},
pagename: 'page0',
pagename1: 'page1',
pagename2: 'page2',
pagename3: 'page3'
}
},
async created() {
let conn1 = this.$findapi('usersetting')
conn1.params.filter = {name__in: ['import-setting', 'model-fields']}
let rs = await this.$getapi([conn1])
let obj = this.$find(rs, {name: 'usersetting'})
let found = this.$find(obj.data.rows, {name: 'import-setting'})
this.pagedata1 = this.$getpage()
this.$setpage(this.pagename1, found)
this.pagedata2 = this.$getpage()
found = this.$find(obj.data.rows, {name: 'model-fields'})
this.$setpage(this.pagename2, found)
this.doClick(this.row)
},
computed: {
pagedata1: {
get: function() {return this.$store[this.pagename1]},
set: function(val) {this.$store.commit(this.pagename1, val)}
},
pagedata2: {
get: function() {return this.$store[this.pagename2]},
set: function(val) {this.$store.commit(this.pagename2, val)}
},
pagedata3: {
get: function() {return this.$store[this.pagename3]},
set: function(val) {this.$store.commit(this.pagename3, val)}
}
},
methods: {
updateAttr(v) {
this.current.detail = v
this.showSetting()
this.showmodal = undefined
},
createUserSetting() {
this.pagedata3 = this.$getpage()
let fields = []
let arr = ['AutoField', 'ForeignKey', 'FloatField', 'IntegerField']
this.pagedata2.data.map(v=>{
let field = this.$createField(v.name, v.name, arr.findIndex(x=>x==v.datatype)>=0? 'number' : 'string', true, true)
fields.push(field)
})
this.$store.commit(this.pagename3, {update: {'fields': fields}})
this.showmodal = {component: 'menu/MenuSave', title: 'Lưu thiết lập', width: '600px', height: '300px',
vbind: {pagename: this.pagename3, classify: 3}}
},
remove(v) {
let copy = this.$copy(this.current.detail)
delete copy[v.key]
this.current.detail = copy
this.doClick(this.current)
},
async insertData() {
let copy = this.$copy(this.current)
copy.id = undefined
let rs = await this.$insertrow('importsetting', copy, undefined, this.pagename)
},
async saveData() {
if(!this.row) return this.insertData()
let rs = await this.$updateapi('importsetting', this.current, undefined, this.pagename)
},
updateFromModel() {
let filter = this.$filter(this.pagedata2.data, {datatype: ['FloatField', 'IntegerField']})
let copy = this.$copy(this.current.detail)
filter.map(v=>{
copy[v.name] = {type: 'number', empty: v.null? 'yes' : 'no'}
})
this.current.detail = copy
this.showSetting()
},
showAttr() {
this.showmodal = {component: 'datatable/FieldAttribute', width: '40%', height: '300px', title: 'Danh sách thuộc tính', vbind: {field: this.current.detail}}
},
async doClick(v) {
this.current = this.$copy(v)
this.showSetting()
let found = this.$findapi(v.api)
let conn = this.$findapi('modelfields')
conn.url += found.url.replace('data/', '')
let rs = await this.$getapi([conn])
let copy = this.$copy(this.pagedata2)
copy.update = {data: rs[0].data}
this.pagedata2 = copy
},
showSetting() {
let arr = []
let i = 0
for(let key in this.current.detail) {
i += 1
arr.push({id: i, key: key, value: this.current.detail[key], text: JSON.stringify(this.current.detail[key])})
}
let copy = this.$copy(this.pagedata1)
copy.update = {data: arr}
this.pagedata1 = copy
}
}
}
</script>

View File

@@ -0,0 +1,175 @@
<template>
<div class="container is-fluid px-4">
<div class="field is-horizontal">
<div class="field-body">
<!-- giỏ hàng -->
<div class="field is-narrow">
<label class="label">
<span class="has-text-danger">*</span>
</label>
<div class="control">
<input
class="input"
type="text"
placeholder="Nhập mã"
v-model="record.code"
id="code"
/>
</div>
</div>
<!-- Tên giỏ hàng -->
<div class="field">
<label class="label">
Tên giỏ hàng <span class="has-text-danger">*</span>
</label>
<div class="control">
<input
class="input"
type="text"
placeholder="Nhập tên giỏ hàng"
v-model="record.name"
/>
</div>
</div>
<!-- Index -->
<div class="field" style="width: 120px;">
<label class="label">
Index <span class="has-text-danger">*</span>
</label>
<div class="control">
<input
class="input"
type="number"
placeholder="0"
v-model.number="record.index"
/>
</div>
</div>
</div>
</div>
<!-- Đại -->
<div class="field mt-5">
<label class="label">Đại </label>
<div class="control">
<SearchBox
v-bind="{
api: 'dealer',
field: 'code',
column: 'code',
optionid: record.dealer || null,
first: true,
viewaddon: viewAddon,
addon: newAddon
}"
@option="dealerSelected"
/>
</div>
</div>
<!-- Nút lưu -->
<div class="field is-grouped is-grouped-right mt-6">
<div class="control">
<button class="button is-primary has-text-white" @click="update">
{{ record.id ? 'Cập nhật' : 'Thêm mới' }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
const {
$copy,
$resetNull,
$snackbar,
$getdata,
$insertapi,
$updateapi
} = useNuxtApp()
// Chỉ nhận id (không nhận row)
const props = defineProps({
prefix: String,
id: [Number, String, null]
})
const emit = defineEmits(['close', 'modalevent'])
const record = ref({
code: '',
name: '',
index: 0,
dealer: null
})
const viewAddon = {}
const newAddon = {}
function dealerSelected(obj) {
record.value.dealer = obj ? obj.id : null
}
async function loadCart() {
if (!props.id) {
record.value = {
code: '',
name: '',
index: 0,
dealer: null
}
return
}
try {
const row = await $getdata('cart', { id: props.id }, null, true)
if (row) {
record.value = $copy(row)
} else {
$snackbar('Không tìm thấy giỏ hàng này')
emit('close')
}
} catch (error) {
console.error('Lỗi khi tải dữ liệu giỏ hàng:', error)
$snackbar('Lỗi tải dữ liệu')
emit('close')
}
}
// Lưu dữ liệu
async function update() {
const data = $resetNull(record.value)
try {
let rs
if (data.id) {
rs = await $updateapi('cart', data)
} else {
rs = await $insertapi('cart', data)
}
if (rs === 'error') {
$snackbar('Lỗi khi lưu dữ liệu')
return
}
$snackbar('Lưu thành công!')
emit('modalevent', { name: 'dataevent', data: rs })
emit('close')
} catch (err) {
$snackbar('Có lỗi xảy ra khi lưu')
console.error(err)
}
}
// Khởi tạo khi mở modal
onMounted(async () => {
await loadCart()
await nextTick()
document.getElementById('code')?.focus()
})
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div class="over-due-payable">
<div class="mt-5 columns">
<div class="column">
<label class="label"
>{{ isVietnamese ? "Thời gian (ngày)" : "Duration (Days)" }} <span class="has-text-danger"> * </span>
</label>
<p class="control">
<input class="input" type="number" placeholder="" v-model="record.time" autocomplete="off" />
</p>
</div>
<div class="column">
<label class="label"
>{{ isVietnamese ? "Mẫu email" : "Email Template" }} <span class="has-text-danger"> * </span>
</label>
<SearchBox
v-bind="{
api: 'Email_Template',
field: 'name',
column: ['name'],
first: true,
optionid: Number(record.emailTemplate),
}"
@option="documentSelected('emailTemplate', $event)"
></SearchBox>
</div>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">
{{ dataTemp.id ? "Cập nhật" : "Tạo mới" }}
</button>
</div>
</div>
</template>
<script setup>
const { $copy, $insertrow, $updaterow, $snackbar, $store } = useNuxtApp();
const isVietnamese = computed(() => $store.lang.toLowerCase() === "vi");
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String,
});
const emit = defineEmits(["close", "modalevent"]);
const dataTemp = $copy(props.row || {});
let record = $copy(props.row?.detail || {});
function documentSelected(attr, obj, v) {
record[attr] = obj.id;
record.name = obj.name;
}
async function update() {
const detail = record;
let rs;
if (dataTemp.id) {
const dataUpdate = {
...dataTemp,
detail,
};
rs = await $updaterow(props.api, dataUpdate, undefined, props.pagename);
} else {
const dataInsert = {
detail,
category: "system",
classify: "overduepayables",
code: `overdue-${record.lookup}-${record.time}-${record.emailTemplate}`,
vi: "Lịch quá hạn thanh toán",
};
rs = await $insertrow(props.api, dataInsert, undefined, props.pagename);
}
if (rs === "error") return $snackbar(rs);
emit("modalevent", { name: "dataevent", data: rs });
emit("close");
}
</script>

View File

@@ -0,0 +1,355 @@
<template>
<div>
<!-- Thông tin Sale Policy -->
<div class="m-0">
<Caption v-bind="{ title: 'Thông tin chính sách', type: 'has-text-warning' }"></Caption>
</div>
<div class="columns is-multiline">
<div class="column is-1">
<div class="field">
<label class="label">Thứ tự</label>
<div class="control">
<input class="input" type="number" v-model.number="record.index" placeholder="Thứ tự" />
</div>
</div>
</div>
<div class="column is-5">
<div class="field">
<label class="label"> <span class="has-text-danger">*</span></label>
<div class="control">
<input class="input" type="text" v-model="record.code" id="code" placeholder="Mã CS" />
</div>
<p class="help is-danger" v-if="errors.code">{{ errors.code }}</p>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label">Tên chính sách <span class="has-text-danger">*</span></label>
<div class="control">
<input class="input" type="text" v-model="record.name" placeholder="Tên" />
</div>
<p class="help is-danger" v-if="errors.name">{{ errors.name }}</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">Đặt cọc <span class="has-text-danger">*</span></label>
<div class="control">
<InputNumber v-bind="{
record: record,
attr: 'deposit',
defaultValue: true,
}" @number="selected('deposit', $event)" />
</div>
<p class="help is-danger" v-if="errors.deposit">{{ errors.deposit }}</p>
</div>
</div>
<div class="column is-4">
<div class="field">
<label class="label">PT thanh toán <span class="has-text-danger">*</span></label>
<div class="control">
<SearchBox v-bind="{
api: 'paymentmethod',
field: 'name',
column: ['name'],
first: true,
optionid: record.method
}" @option="selected('method', $event)" />
</div>
<p class="help is-danger" v-if="errors.method">{{ errors.method }}</p>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label">% Phân bổ HĐ</label>
<div class="control">
<input class="input" type="number" v-model.number="record.contract_allocation_percentage" placeholder="%" min="0" max="100" />
</div>
</div>
</div>
<div class="column is-1">
<div class="field">
<label class="label">
Kích hoạt
<span class="has-text-danger">*</span>
</label>
<div class="control">
<div class="is-flex is-align-items-center is-clickable is-gap-2" @click="record.enable = !record.enable">
<SvgIcon v-bind="{
name: record.enable ? 'checked.svg' : 'uncheck.svg',
type: record.enable ? 'primary' : 'twitter',
size: 22
}" />
<span>Cho phép</span>
</div>
</div>
<p class="help is-danger" v-if="errors.enable">
{{ errors.enable }}
</p>
</div>
</div>
</div>
<!-- Payment Plan -->
<div class="m-0">
<Caption v-bind="{ title: 'Kế hoạch thanh toán', type: 'has-text-warning' }"></Caption>
</div>
<div class="p-3" v-for="(plan, i) in paymentPlans" :key="i" style="position: relative;">
<a v-if="paymentPlans.length > 1" class="has-text-danger is-size-7"
style="position: absolute; top: 0.5rem; right: 0.75rem; cursor: pointer;" @click="removePlan(plan, i)">
Xóa
</a>
<div class="columns is-multiline is-mobile mb-0" style="border-top: 1px solid #dbdbdb;">
<!-- Đợt -->
<div class="column is-3-desktop is-2-mobile pb-0">
<div class="field">
<label class="label">Đợt</label>
<div class="control">
<input class="input has-text-centered has-text-weight-bold" type="text" :value="i + 1" disabled
style="background-color: #f5f5f5;" />
</div>
</div>
</div>
<!-- Value -->
<div class="column is-3-desktop is-5-mobile pb-0">
<div class="field">
<label class="label">Giá trị <span class="has-text-danger">*</span></label>
<div class="control">
<input class="input" type="number" v-model.number="plan.value" placeholder="Giá trị" min="0"
step="0.01" />
</div>
</div>
</div>
<!-- Type -->
<div class="column is-3-desktop is-5-mobile pb-0">
<div class="field">
<label class="label">Loại <span class="has-text-danger">*</span></label>
<div class="control">
<SearchBox v-bind="{
api: 'valuetype',
field: 'name',
column: ['name'],
first: true,
optionid: plan.type
}" @option="planSelected('type', $event, plan)" />
</div>
</div>
</div>
<!-- Days -->
<div class="column is-3-desktop is-4-mobile pb-0">
<div class="field">
<label class="label">Số ngày</label>
<div class="control">
<input class="input" type="number" v-model.number="plan.days" placeholder="Ngày" min="0" />
</div>
</div>
</div>
<!-- Payment Note -->
<div class="column is-6-desktop is-6-mobile pb-0">
<div class="field">
<label class="label">Ghi chú TT</label>
<div class="control">
<input class="input" type="text" v-model="plan.payment_note" placeholder="Ghi chú thanh toán" />
</div>
</div>
</div>
<!-- Due Note -->
<div class="column is-6-desktop is-10-mobile pb-0">
<div class="field mb-0">
<label class="label">Ghi chú hạn</label>
<div class="control">
<input class="input" type="text" v-model="plan.due_note" placeholder="Ghi chú hạn thanh toán" />
</div>
</div>
</div>
</div>
</div>
<!-- Nút thêm -->
<button class="button is-pulled-right is-info is-light mb-4" @click="addPlan">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" height="12" width="12" viewBox="0 0 448 512">
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z" />
</svg>
</span>
</button>
<!-- Action buttons -->
<div class="buttons">
<button :class="`button has-text-white is-primary ${isSubmitting ? 'is-loading' : ''}`" @click="update()"
:disabled="isSubmitting">
<span>Lưu</span>
</button>
<button class="button has-text-white is-danger" @click="emit('close')" :disabled="isSubmitting">
<span>Hủy</span>
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
const { $getdata, $copy, $resetNull, $insertapi, $updateapi, $snackbar, $remove, $deleteapi, $empty } = useNuxtApp();
const props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String,
});
const emit = defineEmits(["close", "modalevent"]);
const record = ref($copy(props.row || { enable: true }));
const paymentPlans = ref([{}]);
const errors = ref({});
const isSubmitting = ref(false);
if (props.row?.id) {
const plans = await $getdata("paymentplan", { policy: props.row.id });
if (plans && plans.length > 0) {
paymentPlans.value = plans;
}
}
function validateForm() {
errors.value = {};
if ($empty(record.value.code)) {
errors.value.code = "Mã không được để trống";
}
if ($empty(record.value.name)) {
errors.value.name = "Tên không được để trống";
}
if ($empty(record.value.deposit)) {
errors.value.deposit = "Tiền đặt cọc không được để trống";
}
if ($empty(record.value.method)) {
errors.value.method = "Phương thức thanh toán không được để trống";
}
return Object.keys(errors.value).length === 0;
}
async function update() {
if (isSubmitting.value) return;
if (!validateForm()) {
$snackbar("Vui lòng kiểm tra lại dữ liệu");
return;
}
try {
isSubmitting.value = true;
let policyData = $resetNull(record.value);
if (policyData.method && typeof policyData.method === 'object') {
policyData.method = policyData.method.id;
}
const policyResult = policyData.id
? await $updateapi("salepolicy", policyData, undefined, false)
: await $insertapi("salepolicy", policyData, undefined, false);
if (policyResult === "error") {
throw new Error("Failed to save policy");
}
const plans = paymentPlans.value.filter(v => {
return !$empty(v.value) && !$empty(v.type);
});
plans.forEach((plan, index) => {
plan.policy = policyResult.id;
plan.cycle = index + 1;
if (plan.type) {
plan.type = Number(plan.type);
}
});
if (plans.length > 0) {
if (props.row?.id) {
const oldPlans = await $getdata("paymentplan", { policy: policyResult.id });
if (oldPlans && oldPlans.length > 0) {
for (const oldPlan of oldPlans) {
await $deleteapi("paymentplan", oldPlan.id);
}
}
}
await $insertapi("paymentplan", plans, undefined, false);
}
$snackbar("Lưu dữ liệu thành công!");
emit("modalevent", { name: "dataevent", data: policyResult });
emit("close");
} catch (error) {
$snackbar("Lưu dữ liệu thất bại");
} finally {
isSubmitting.value = false;
}
}
function selected(attr, obj) {
if (obj !== null && typeof obj === "object") {
record.value[attr] = obj.id || obj;
} else {
record.value[attr] = obj;
}
}
function planSelected(attr, obj, plan) {
if (attr === 'type') {
if (obj && typeof obj === 'object') {
plan.type = obj.id;
plan._type = obj;
} else {
plan.type = obj;
}
} else if (attr === 'value') {
plan.value = Number(obj);
} else {
plan[attr] = obj;
}
}
function addPlan() {
paymentPlans.value.push({});
}
async function removePlan(plan, index) {
if (plan.id) {
await $deleteapi("paymentplan", plan.id);
}
$remove(paymentPlans.value, index);
if (paymentPlans.value.length === 0) {
paymentPlans.value = [{}];
}
}
onMounted(() => {
document.getElementById("code")?.focus();
});
</script>

View File

@@ -0,0 +1,87 @@
<template>
<div>
<div class="field is-horizontal mt-6 pt-4">
<div class="field-body">
<div class="field is-narrow">
<label class="label">Code <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.code" id="code">
</p>
</div>
<div class="field is-narrow">
<label class="label">Name <span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="" v-model="record.name" id="code">
</p>
</div>
</div>
</div>
<div class="columns mx-0">
<div class="column is-8">
<div class="mt-5 mb-3">
<Caption v-bind="{title: 'Required documents', type: 'has-text-primary'}"></Caption>
</div>
<div class="field is-grouped" v-for="(v,i) in array">
<div class="control">
<SearchBox v-bind="{api:'documenttype', field:'name', column:['name'], first:true, optionid:v.doctype, position: 'is-top-left'}"
@option="documentSelected('_doctype', $event, v)"></SearchBox>
</div>
<div class="control pl-5">
<a class="mr-4" @click="add()">
<SvgIcon v-bind="{name: 'add1.png', type: 'dark', size: 20}"></SvgIcon>
</a>
<a @click="remove(v, i)">
<SvgIcon v-bind="{name: 'bin1.svg', type: 'dark', size: 20}"></SvgIcon>
</a>
</div>
</div>
</div>
</div>
<div class="mt-5">
<button class="button is-primary has-text-white" @click="update()">Save</button>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
const { $getdata, $copy, $resetNull, $insertrow, $updaterow, $snackbar, $remove, $deleteapi} = useNuxtApp()
var props = defineProps({
api: String,
pagename: String,
row: Object,
prefix: String
})
const emit = defineEmits(['close', 'modalevent'])
var array = ref([{}])
var record = $copy( props.row || {})
if(props.row) {
let arr = await $getdata('phasedoctype', {phase: props.row.id})
if(arr.length>0) array.value = arr
}
async function update() {
let data = $resetNull(record)
let rs = data.id? await $updaterow(props.api, data, undefined, props.pagename)
: await $insertrow(props.api, data, undefined, props.pagename)
if(rs==='error') return $snackbar(rs)
let arr = array.value.filter(v=>v.doctype)
arr.map(v=>v.phase = rs.id)
await $insertrow('phasedoctype', arr)
emit('modalevent', {name: 'dataevent', data: rs})
emit('close')
}
function documentSelected(attr, obj, v) {
v[attr] = obj
if(obj) v.doctype = obj.id
}
function add() {
array.value.push({})
}
async function remove(v, i) {
if(v.id) await $deleteapi('phasedoctype', v.id)
$remove(array.value, i)
if(array.value.length===0) array.value = [{}]
}
onMounted(()=> {
document.getElementById('code').focus()
})
</script>