Initial commit
This commit is contained in:
175
app/components/customer/Company.vue
Normal file
175
app/components/customer/Company.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<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="`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="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 = {}
|
||||
}
|
||||
}
|
||||
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>
|
||||
136
app/components/customer/Customer.vue
Normal file
136
app/components/customer/Customer.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<!-- Nội dung chính - chỉ hiển thị form cá nhân (màn hình chọn đã được xử lý ở SearchBox) -->
|
||||
<div class="columns mx-0 px-0 py-2">
|
||||
<div
|
||||
:class="`column is-narrow p-0 pr-4 ${viewport === 1 ? 'px-0' : ''}`"
|
||||
:style="`${viewport === 1 ? '' : 'border-right: 1px solid #B0B0B0;'}`"
|
||||
>
|
||||
<template v-if="viewport > 1">
|
||||
<div
|
||||
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;"
|
||||
@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 style="width: 130px">
|
||||
<div :class="`py-3 px-3 ${getStyle(v)}`">
|
||||
{{ isVietnamese ? v.name : v.en }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['column', { 'px-0': viewport === 1 }]">
|
||||
<CustomerInfo2
|
||||
v-if="tab === 'info' && record !== undefined"
|
||||
v-bind="{ row: record, pagename, application }"
|
||||
@update="update"
|
||||
@close="emit('close')"
|
||||
/>
|
||||
<template v-if="record">
|
||||
<ImageGallery
|
||||
v-bind="{
|
||||
row: record,
|
||||
pagename: pagename,
|
||||
show: ['delete'],
|
||||
api: 'customerfile',
|
||||
}"
|
||||
@update="update"
|
||||
v-if="tab === 'image'"
|
||||
></ImageGallery>
|
||||
<CustomerView
|
||||
v-bind="{ row: record, pagename: pagename }"
|
||||
@update="update"
|
||||
@close="emit('close')"
|
||||
v-if="tab === 'print'"
|
||||
></CustomerView>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal @close="handleModalClose" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { useNuxtApp } from "#app";
|
||||
import CustomerInfo2 from "~/components/customer/CustomerInfo2";
|
||||
import CustomerView from "~/components/customer/CustomerView";
|
||||
import Modal from "~/components/Modal.vue";
|
||||
import { useStore } from "@/stores/index";
|
||||
const nuxtApp = useNuxtApp();
|
||||
const { $dialog } = nuxtApp;
|
||||
|
||||
const store = useStore();
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
row: Object,
|
||||
application: Object,
|
||||
isEditMode: Boolean,
|
||||
handleCustomer: Function,
|
||||
});
|
||||
|
||||
const lang = computed(() => store.lang);
|
||||
const isVietnamese = computed(() => lang.value === "vi");
|
||||
const emit = defineEmits(["modalevent", "close"]);
|
||||
var viewport = 5;
|
||||
var tabs = [
|
||||
{ code: "info", name: "1. Thông tin", en: "1. Information", active: true },
|
||||
{ code: "image", name: "2. Hình ảnh", en: "2. Images", active: false },
|
||||
{ code: "print", name: "3. Bản in", en: "3. Print", active: false },
|
||||
];
|
||||
var tab = ref("info");
|
||||
var record = props.row || null;
|
||||
var showmodal = ref();
|
||||
|
||||
function getStyle(v) {
|
||||
let check = record ? record.id : false;
|
||||
// let check = props.isEditMode;
|
||||
if (v.tab === "info") check = true;
|
||||
return v.code === tab.value
|
||||
? "has-background-primary has-text-white"
|
||||
: `has-background-light ${check ? "" : "has-text-grey"}`;
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
tab.value = v.code;
|
||||
}
|
||||
|
||||
function handleModalClose() {
|
||||
showmodal.value = undefined;
|
||||
}
|
||||
|
||||
function update(v) {
|
||||
record = {
|
||||
...v,
|
||||
label: `${v.code} / ${v.fullname} / ${v.phone || ""}`,
|
||||
}
|
||||
emit("modalevent", { name: "dataevent", data: record });
|
||||
if (!props.isEditMode) emit("close");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
font-family: "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif !important;
|
||||
}
|
||||
.button.is-large.is-fullwidth:hover {
|
||||
color: white !important;
|
||||
background-color: rgb(75, 114, 243) !important;
|
||||
.title {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
783
app/components/customer/CustomerInfo.vue
Normal file
783
app/components/customer/CustomerInfo.vue
Normal file
@@ -0,0 +1,783 @@
|
||||
<template v-if="isLoaded">
|
||||
<div v-if="record && isLoaded">
|
||||
<div class="columns is-multiline mx-0">
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input has-text-black"
|
||||
disabled
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="" />
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.fullname">
|
||||
{{ errors.fullname }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<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
|
||||
class="has-text-primary"
|
||||
v-if="existedCustomer"
|
||||
@click="showCustomer()"
|
||||
>Chi tiết</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-3 ${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-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
|
||||
>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: store.sex,
|
||||
api: 'sex',
|
||||
field: isVietnamese ? 'name' : 'en',
|
||||
column: ['name', 'en'],
|
||||
first: true,
|
||||
optionid: record.sex,
|
||||
}"
|
||||
@option="selected('sex', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<Datepicker
|
||||
v-bind="{ record: record, attr: 'dob', maxdate: new Date() }"
|
||||
@date="selected('dob', $event)"
|
||||
>
|
||||
</Datepicker>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: store.country,
|
||||
api: 'country',
|
||||
field: isVietnamese ? 'name' : 'en',
|
||||
column: ['name', 'en'],
|
||||
first: true,
|
||||
optionid: record.country,
|
||||
}"
|
||||
@option="selected('country', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.province"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.district"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<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 :class="`column is-5 px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{
|
||||
dataLang && findFieldName("company")[lang]
|
||||
}}</label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'company',
|
||||
field: 'fullname',
|
||||
column: ['fullname'],
|
||||
first: true,
|
||||
optionid: record.company,
|
||||
addon: companyAddon,
|
||||
viewaddon: companyviewAddon,
|
||||
}"
|
||||
@option="selected('company', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: store.legaltype,
|
||||
api: 'legaltype',
|
||||
field: isVietnamese ? 'name' : 'en',
|
||||
column: ['name', 'en'],
|
||||
first: true,
|
||||
optionid: record.legal_type,
|
||||
}"
|
||||
@option="selected('legal_type', $event)"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :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
|
||||
>
|
||||
<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-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
|
||||
>
|
||||
<div class="control">
|
||||
<Datepicker
|
||||
v-bind="{
|
||||
record: record,
|
||||
attr: 'issued_date',
|
||||
maxdate: new Date(),
|
||||
position: 'is-top-left',
|
||||
}"
|
||||
@date="selected('issued_date', $event)"
|
||||
></Datepicker>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.issued_date">
|
||||
{{ errors.issued_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.issued_place"
|
||||
/>
|
||||
</div>
|
||||
<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' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Zalo</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.zalo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`column is-3 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Facebook</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder=""
|
||||
v-model="record.facebook"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{
|
||||
dataLang && findFieldName("note")[lang]
|
||||
}}</label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
class="textarea"
|
||||
:placeholder="isVietnamese ? 'Nhập ghi chú...' : 'Enter note...'"
|
||||
v-model="record.note"
|
||||
rows="1"
|
||||
></textarea>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.note">{{ errors.note }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 mt-5">
|
||||
<Caption
|
||||
v-bind="{
|
||||
title: findFieldName('related_person')[lang],
|
||||
type: 'has-text-warning',
|
||||
}"
|
||||
></Caption>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
position: 'top',
|
||||
api: 'people',
|
||||
field: 'label',
|
||||
column: ['code', 'fullname', 'phone'],
|
||||
first: true,
|
||||
optionid: v.people,
|
||||
addon: peopleAddon,
|
||||
viewaddon: peopleviewAddon,
|
||||
position: 'is-top-left',
|
||||
}"
|
||||
@option="selectPeople($event, v, i)"
|
||||
>
|
||||
</SearchBox>
|
||||
<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
|
||||
>
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
vdata: store.relation,
|
||||
position: 'top',
|
||||
api: 'relation',
|
||||
field: store.lang === 'en' ? 'en' : 'name',
|
||||
column: ['code', 'name', 'en'],
|
||||
first: true,
|
||||
optionid: v.relation,
|
||||
position: 'is-top-left',
|
||||
}"
|
||||
@option="selectRelation($event, v, i)"
|
||||
>
|
||||
</SearchBox>
|
||||
</div>
|
||||
<div :class="`column ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<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>
|
||||
<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()">
|
||||
{{ store.lang === "en" ? "Save" : "Lưu lại" }}
|
||||
</button>
|
||||
</div>
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, defineEmits } from "vue";
|
||||
import { useNuxtApp } from "#app";
|
||||
import InputPhone from "~/components/common/InputPhone";
|
||||
import InputEmail from "~/components/common/InputEmail";
|
||||
import SearchBox from "~/components/SearchBox";
|
||||
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;
|
||||
var props = defineProps({
|
||||
pagename: String,
|
||||
row: Object,
|
||||
application: Object,
|
||||
});
|
||||
|
||||
console.log("props", props);
|
||||
|
||||
const store = useStore();
|
||||
const lang = computed(() => store.lang);
|
||||
const isVietnamese = computed(() => lang.value === "vi");
|
||||
const dataLang = ref(store.common);
|
||||
var viewport = store.viewport;
|
||||
var errors = ref({});
|
||||
var record = ref({});
|
||||
const isLoaded = ref(false);
|
||||
const dataCustomer = ref(props.row);
|
||||
|
||||
if (!props.row && props.application) {
|
||||
//create customer by copying information form application
|
||||
let copy = $copy(props.application);
|
||||
let image = await $getdata("applicationfile", {
|
||||
ref: copy.id,
|
||||
file__doc_type__code__in: ["cccd-mt", "cccd-ms", "avatar"],
|
||||
});
|
||||
copy.image = image.map((v) => v.file);
|
||||
delete copy.id;
|
||||
dataCustomer.value = copy;
|
||||
}
|
||||
|
||||
// const formData = ref({
|
||||
// // fullname: dataCustomer.value.fullname || "",
|
||||
// phone: dataCustomer.value.phone || "",
|
||||
// email: dataCustomer.value.email || "",
|
||||
// dob: dataCustomer.value.dob || null,
|
||||
// country: dataCustomer.value.country || 1,
|
||||
// province: dataCustomer.value.province || null,
|
||||
// district: dataCustomer.value.district || "",
|
||||
// address: dataCustomer.value.address || "",
|
||||
// sex: dataCustomer.value.sex || 1,
|
||||
// note: dataCustomer.value.note || "",
|
||||
// legal_type: dataCustomer.value.legal_type || null,
|
||||
// legal_code: dataCustomer.value.legal_code || "",
|
||||
// issued_date: dataCustomer.value.issue_date || null,
|
||||
// issued_place: dataCustomer.value.issue_place || "",
|
||||
// branch: dataCustomer.value.branch || null,
|
||||
// user: dataCustomer.value.user || null,
|
||||
// collaborator: dataCustomer.value.collaborator || null,
|
||||
// updater: dataCustomer.value.updater || store.login.id,
|
||||
// creator: store.login.id,
|
||||
// image: dataCustomer.value.image || null,
|
||||
// });
|
||||
|
||||
var showmodal = ref();
|
||||
var existedCustomer = undefined;
|
||||
var people = ref([]);
|
||||
var peopleAddon = {
|
||||
component: "people/People",
|
||||
with: "65%",
|
||||
height: "600px",
|
||||
title: store.lang === "en" ? "Related persion" : "Người liên quan",
|
||||
};
|
||||
var peopleviewAddon = {
|
||||
component: "people/People",
|
||||
width: "65%",
|
||||
height: "600px",
|
||||
title: store.lang === "en" ? "Related person" : "Người liên quan",
|
||||
};
|
||||
var companyAddon = {
|
||||
component: "customer/Company",
|
||||
with: "55%",
|
||||
height: "400px",
|
||||
title: store.lang === "en" ? "Company" : "Công ty",
|
||||
};
|
||||
var companyviewAddon = {
|
||||
component: "customer/Company",
|
||||
width: "55%",
|
||||
height: "400px",
|
||||
title: store.lang === "en" ? "Company" : "Công ty",
|
||||
};
|
||||
|
||||
async function initData() {
|
||||
try {
|
||||
if (dataCustomer.value.customer) {
|
||||
record.value = { ...dataCustomer.value.customer, ...formData.value };
|
||||
people.value = [{}];
|
||||
} else if (dataCustomer.value.id) {
|
||||
record.value = props.row;
|
||||
let rows = await $getdata("customerpeople", {
|
||||
customer: dataCustomer.value.id,
|
||||
});
|
||||
people.value = rows.length > 0 ? rows : [{}];
|
||||
} else {
|
||||
const customerCode = await $getdata("getcodeCustomer");
|
||||
record.value = { ...formData.value, code: customerCode };
|
||||
isLoaded.value = true;
|
||||
people.value = [{}];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error in initData:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function findFieldName(code) {
|
||||
const field = dataLang.value.find((item) => item.code === code);
|
||||
return field || { vi: "Không tìm thấy trường", en: "Field not found" };
|
||||
}
|
||||
|
||||
function showCustomer() {
|
||||
showmodal.value = {
|
||||
component: "customer/CustomerView",
|
||||
width: "60%",
|
||||
height: "600px",
|
||||
title: store.lang === "en" ? "Customer" : "Khách hàng",
|
||||
vbind: { row: existedCustomer },
|
||||
};
|
||||
}
|
||||
|
||||
const selected = (fieldName, value) => {
|
||||
const finalValue =
|
||||
value !== null && typeof value === "object"
|
||||
? value.id || value.index
|
||||
: value;
|
||||
record.value[fieldName] = finalValue;
|
||||
|
||||
if (errors.value[fieldName]) {
|
||||
delete errors.value[fieldName];
|
||||
}
|
||||
};
|
||||
|
||||
function checkError() {
|
||||
errors.value = {};
|
||||
|
||||
if ($empty(record.value.fullname)) {
|
||||
errors.value.fullname = "Họ tên không được bỏ trống";
|
||||
}
|
||||
|
||||
if ($empty(record.value.phone)) {
|
||||
errors.value.phone = "Số điện thoại không được bỏ trống";
|
||||
} else {
|
||||
let text = $errPhone(record.value.phone);
|
||||
if (text) errors.value.phone = text;
|
||||
}
|
||||
|
||||
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";
|
||||
} else {
|
||||
if (record.value.dob > new Date()) {
|
||||
errors.value.dob = isVietnamese.value
|
||||
? "Ngày sinh không được lớn hơn ngày hiện tại"
|
||||
: "Date of birth cannot be greater than the current date";
|
||||
}
|
||||
if ($dayjs(new Date()).diff(new Date(record.value.dob), "year") < 18) {
|
||||
errors.value.dob = isVietnamese.value
|
||||
? "Khách hàng phải đủ 18 tuổi trở lên"
|
||||
: "Customer must be at least 18 years old";
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
return Object.keys(errors.value).length > 0;
|
||||
}
|
||||
function selectPeople(option, v, i) {
|
||||
let copy = $copy(v);
|
||||
copy.people = option.id;
|
||||
people.value[i] = copy;
|
||||
people.value = $copy(people.value);
|
||||
}
|
||||
function selectRelation(option, v, i) {
|
||||
let copy = $copy(v);
|
||||
copy.relation = option ? option.id : null;
|
||||
people.value[i] = copy;
|
||||
people.value = $copy(people.value);
|
||||
}
|
||||
function add() {
|
||||
people.value.push({});
|
||||
}
|
||||
async function remove(v, i) {
|
||||
if (v.id) await $deleteapi("customerpeople", v.id);
|
||||
people.value.splice(i, 1);
|
||||
if (people.value.length === 0) {
|
||||
setTimeout(() => (people.value = [{}]));
|
||||
}
|
||||
}
|
||||
async function update() {
|
||||
try {
|
||||
if (checkError()) return;
|
||||
|
||||
var isNewCustomer =
|
||||
!dataCustomer.value?.customer ||
|
||||
dataCustomer.value.customer === null ||
|
||||
dataCustomer.value.customer === undefined ||
|
||||
!record.value.id;
|
||||
if (record.value.id) isNewCustomer = false;
|
||||
if (isNewCustomer) {
|
||||
try {
|
||||
if (record.value.phone) {
|
||||
record.value.phone = record.value.phone.trim();
|
||||
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.";
|
||||
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
|
||||
);
|
||||
if (emailCheck) {
|
||||
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
|
||||
);
|
||||
if (legalCheck) {
|
||||
errors.value.legal_code = isVietnamese.value
|
||||
? "Số CMND/CCCD đã tồn tại."
|
||||
: "ID card number already exists.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error checking duplicates:", error);
|
||||
}
|
||||
}
|
||||
|
||||
let dataToSend = $resetNull({ ...record.value });
|
||||
|
||||
if (isNewCustomer) {
|
||||
delete dataToSend.id;
|
||||
}
|
||||
|
||||
if (!dataToSend.note || dataToSend.note.trim() === "") {
|
||||
dataToSend.note = null;
|
||||
}
|
||||
|
||||
if (!dataToSend.creator) dataToSend.creator = store.login.id;
|
||||
dataToSend.updater = store.login.id;
|
||||
dataToSend.update_time = new Date();
|
||||
|
||||
let rs;
|
||||
if (isNewCustomer) {
|
||||
rs = await $insertapi("customer", dataToSend, undefined, false);
|
||||
} else {
|
||||
rs = await $updateapi("customer", dataToSend);
|
||||
}
|
||||
|
||||
if (rs === "error") {
|
||||
$snackbar("Có lỗi xảy ra khi lưu dữ liệu", "Lỗi", "Error");
|
||||
return;
|
||||
}
|
||||
record.value.id = rs.id;
|
||||
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"
|
||||
} <b>${rs.code}</b>`,
|
||||
"Thành công",
|
||||
"Success"
|
||||
);
|
||||
} else {
|
||||
$snackbar(
|
||||
`${
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
record.value = { ...record.value, ...customerData };
|
||||
emit("update", customerData);
|
||||
|
||||
//Xử lý people relationships nếu có
|
||||
if (people.value && people.value.length > 0) {
|
||||
let filter = people.value.filter((v) => v.people);
|
||||
if (filter.length > 0) {
|
||||
filter.map((v) => (v.customer = rs.id));
|
||||
await $insertapi("customerpeople", filter);
|
||||
}
|
||||
}
|
||||
|
||||
//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 })
|
||||
);
|
||||
await $insertapi("customerfile", arr, undefined, false);
|
||||
}
|
||||
emit("update", customerData);
|
||||
emit("close");
|
||||
} catch (error) {
|
||||
console.error("Error in update:", error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await initData();
|
||||
isLoaded.value = true;
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
476
app/components/customer/CustomerInfo2.vue
Normal file
476
app/components/customer/CustomerInfo2.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<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>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6">
|
||||
<button
|
||||
:disabled="!$getEditRights('edit', { code: 'individual', category: 'submenu' })"
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 120px"
|
||||
@click="selectCustomerType(1)"
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<div>
|
||||
<SvgIcon v-bind="{ name: 'user.svg', type: 'black', size: 40 }"></SvgIcon>
|
||||
</div>
|
||||
<div class="title is-5 mb-0">
|
||||
{{ isVietnamese ? "Cá nhân" : "Individual" }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<button
|
||||
:disabled="!$getEditRights('edit', { code: 'org', category: 'submenu' })"
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 120px"
|
||||
@click="selectCustomerType(2)"
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<div>
|
||||
<SvgIcon v-bind="{ name: 'building.svg', type: 'black', size: 40 }"></SvgIcon>
|
||||
</div>
|
||||
<div class="title is-5 mb-0">
|
||||
{{ isVietnamese ? "Tổ chức" : "Organization" }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="isLoaded">
|
||||
<div v-if="record && isLoaded">
|
||||
<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>
|
||||
<div class="control">
|
||||
<input class="input" type="text" v-model="record.fullname" />
|
||||
</div>
|
||||
<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">
|
||||
{{ errors.phone }}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-multiline">
|
||||
<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" />
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<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="{
|
||||
api: 'people',
|
||||
field: 'label',
|
||||
column: ['code', 'fullname', 'phone'],
|
||||
first: true,
|
||||
optionid: v.people,
|
||||
position: 'is-top-left',
|
||||
addon: peopleAddon,
|
||||
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="{
|
||||
api: 'relation',
|
||||
field: store.lang === 'en' ? 'en' : 'name',
|
||||
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] }
|
||||
}"
|
||||
@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()">
|
||||
<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)">
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</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="update()">{{ isVietnamese ? 'Lưu lại' : 'Save' }}</button>
|
||||
</div>
|
||||
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useNuxtApp } from "#app";
|
||||
import InputPhone from "~/components/common/InputPhone";
|
||||
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';
|
||||
|
||||
const emit = defineEmits(["close", "update", "modalevent"]);
|
||||
const { $getdata, $patchapi, $insertapi, $deleteapi, $empty, $errPhone, $resetNull, $snackbar, $copy } = useNuxtApp();
|
||||
|
||||
const props = defineProps({
|
||||
pagename: String,
|
||||
row: Object,
|
||||
application: Object,
|
||||
customerType: Number,
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const lang = computed(() => store.lang);
|
||||
const isVietnamese = computed(() => lang.value === "vi");
|
||||
const dataLang = ref(store.common);
|
||||
|
||||
const errors = ref({});
|
||||
const record = ref({});
|
||||
const individualData = ref({});
|
||||
const organizationData = ref({});
|
||||
const isLoaded = ref(false);
|
||||
const isNewCustomer = ref(true);
|
||||
const selectedCustomerType = ref(null);
|
||||
const showmodal = ref();
|
||||
const existedCustomer = ref(undefined);
|
||||
const people = ref([]);
|
||||
const localPeople = ref([]); // { id?: number; people: number; relation: number }[]
|
||||
|
||||
const isIndividual = computed(() => selectedCustomerType.value === 1);
|
||||
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);
|
||||
});
|
||||
|
||||
const peopleAddon = {
|
||||
component: "people/People",
|
||||
width: "65%",
|
||||
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 };
|
||||
if (typeNum === 1) {
|
||||
individualData.value = { dob: null, sex: 1 };
|
||||
organizationData.value = {};
|
||||
} else {
|
||||
individualData.value = {};
|
||||
organizationData.value = { established_date: null };
|
||||
}
|
||||
people.value = [{}];
|
||||
isLoaded.value = true;
|
||||
}
|
||||
|
||||
function findFieldName(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 } };
|
||||
}
|
||||
|
||||
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 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 })
|
||||
|
||||
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.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.issued_date)) errors.value.issued_date = "Ngày cấp không được bỏ trống";
|
||||
|
||||
return Object.keys(errors.value).length > 0;
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (checkError()) return;
|
||||
|
||||
if (isNewCustomer.value) {
|
||||
if (record.value.phone) {
|
||||
const phoneCheck = await $getdata("customer", { phone: record.value.phone.trim() }, undefined, true);
|
||||
if (phoneCheck) {
|
||||
existedCustomer.value = phoneCheck;
|
||||
errors.value.phone = isVietnamese.value ? "Số điện thoại đã tồn tại." : "Phone already exists.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (record.value.email) {
|
||||
const emailCheck = await $getdata("customer", { email: record.value.email.trim() }, undefined, true);
|
||||
if (emailCheck) {
|
||||
existedCustomer.value = emailCheck;
|
||||
errors.value.email = isVietnamese.value ? "Email đã tồn tại." : "Email already exists.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
let customerData = $resetNull({ ...record.value });
|
||||
customerData.type = selectedCustomerType.value;
|
||||
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);
|
||||
if (!res || res === "error") return;
|
||||
|
||||
const customerId = res.id;
|
||||
let organizationId = organizationData.value?.id;
|
||||
|
||||
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);
|
||||
else await $insertapi("individual", indPayload, undefined, false);
|
||||
} else if (isOrganization.value) {
|
||||
let orgPayload = $resetNull({ ...organizationData.value });
|
||||
orgPayload.customer = customerId;
|
||||
let orgRes;
|
||||
if (organizationData.value.id) {
|
||||
orgRes = await $patchapi("organization", { ...orgPayload, id: organizationData.value.id }, undefined, false);
|
||||
} else {
|
||||
orgRes = await $insertapi("organization", orgPayload, undefined, false);
|
||||
}
|
||||
if (orgRes && orgRes.id) {
|
||||
organizationId = orgRes.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Người liên quan / Người đại diện
|
||||
const apiName = isIndividual.value ? "customerpeople" : "legalrep";
|
||||
let commonPayload = {};
|
||||
if (isIndividual.value) {
|
||||
commonPayload = { customer: customerId };
|
||||
}
|
||||
if (isOrganization.value && organizationId) {
|
||||
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']));
|
||||
|
||||
// 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 }
|
||||
|
||||
if (!match) {
|
||||
$patchapi(apiName, payload);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. if localPeople has and people doesn't -> insert
|
||||
validLocalPeople.forEach(lp => {
|
||||
if (!lp.id) {
|
||||
const payload = { ...lp, ...commonPayload };
|
||||
$insertapi(apiName, payload);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
if (!match) {
|
||||
$deleteapi(apiName, cp.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ảnh
|
||||
if (record.value.image && record.value.image.length > 0) {
|
||||
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); }
|
||||
}
|
||||
|
||||
async function initData() {
|
||||
if (props.row && props.row.id) {
|
||||
isNewCustomer.value = false;
|
||||
selectedCustomerType.value = Number(props.row.type);
|
||||
record.value = { ...props.row };
|
||||
if (isIndividual.value) {
|
||||
const ind = await $getdata("individual", { customer: props.row.id }, undefined, true);
|
||||
individualData.value = ind || { dob: null, sex: 1 };
|
||||
const rows = await $getdata("customerpeople", { customer: props.row.id });
|
||||
people.value = rows.length > 0 ? rows : [{}];
|
||||
} else {
|
||||
const org = await $getdata("organization", { customer: props.row.id }, undefined, true);
|
||||
organizationData.value = org || { established_date: null };
|
||||
if (org && org.id) {
|
||||
const rows = await $getdata("legalrep", { organization: org.id });
|
||||
people.value = rows.length > 0 ? rows : [{}];
|
||||
} else {
|
||||
people.value = [{}];
|
||||
}
|
||||
}
|
||||
isLoaded.value = true;
|
||||
} else if (props.application && props.application.id) {
|
||||
const copyData = $copy(props.application);
|
||||
const type = props.customerType || copyData.type || 1;
|
||||
selectCustomerType(type);
|
||||
record.value = { ...record.value, ...copyData, id: undefined, code: undefined };
|
||||
individualData.value = { ...individualData.value, ...copyData };
|
||||
} else if (props.customerType) {
|
||||
selectCustomerType(props.customerType);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => initData());
|
||||
</script>
|
||||
69
app/components/customer/CustomerTypeSelector.vue
Normal file
69
app/components/customer/CustomerTypeSelector.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div 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> -->
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6">
|
||||
<button
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 100px"
|
||||
@click="selectType('individual')"
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<div class="">
|
||||
<SvgIcon v-bind="{ name: 'user.svg', type: 'black', size: 40 }"></SvgIcon>
|
||||
</div>
|
||||
<div class="title is-6 mb-0">
|
||||
{{ isVietnamese ? "Cá nhân" : "Individual" }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<button
|
||||
class="button is-large is-fullwidth"
|
||||
style="height: 100px"
|
||||
@click="selectType('company')"
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<div class="">
|
||||
<SvgIcon
|
||||
v-bind="{ name: 'building.svg', type: 'black', size: 40 }"
|
||||
></SvgIcon>
|
||||
</div>
|
||||
<div class="title is-6 mb-0">
|
||||
{{ isVietnamese ? "Doanh nghiệp" : "Company" }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
const store = useStore();
|
||||
const lang = computed(() => store.lang);
|
||||
const isVietnamese = computed(() => lang.value === "vi");
|
||||
|
||||
const emit = defineEmits(["select", "modalevent"]);
|
||||
|
||||
const props = defineProps({
|
||||
application: Object,
|
||||
});
|
||||
|
||||
function selectType(type) {
|
||||
// Emit event qua modalevent để Modal có thể forward
|
||||
emit("modalevent", { name: "select", data: type });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.button.is-large.is-fullwidth:hover {
|
||||
background-color: #3292ec !important;
|
||||
}
|
||||
</style>
|
||||
301
app/components/customer/CustomerView.vue
Normal file
301
app/components/customer/CustomerView.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<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">
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Tên khách hàng</label>
|
||||
<div class="control">
|
||||
{{ record.fullname }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Điện thoại</label>
|
||||
<div class="control">
|
||||
<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">
|
||||
{{ record.email || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Giấy tờ</label>
|
||||
<div class="control">
|
||||
{{ record.legal_type__name || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Mã số giấy tờ</label>
|
||||
<div class="control">
|
||||
{{ record.legal_code || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Ngày cấp</label>
|
||||
<div class="control">
|
||||
{{ record.issued_date ? $dayjs(record.issued_date).format("DD/MM/YYYY") : "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Nơi cấp</label>
|
||||
<div class="control">
|
||||
{{ record.issued_place__name || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Loại khách hàng</label>
|
||||
<div class="control">
|
||||
{{ record.type__name || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Quốc gia</label>
|
||||
<div class="control">
|
||||
{{ isVietnamese ? record.country__name : record.country__en || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Địa chỉ liên hệ</label>
|
||||
<div class="control">
|
||||
{{ record.contact_address || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Địa chỉ thường trú</label>
|
||||
<div class="control">
|
||||
{{ record.address || "/" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">Thời gian tạo</label>
|
||||
<div class="control">
|
||||
<span
|
||||
>{{ $dayjs(record.create_time).format("DD/MM/YYYY") }}
|
||||
{{ $dayjs(record.create_time).format("HH:mm") }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3 pb-1 px-0">
|
||||
<div class="field">
|
||||
<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") : '/' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Caption v-bind="{ title: isVietnamese ? 'Hình ảnh' : 'Images' }"></Caption>
|
||||
<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>
|
||||
<div class="mt-2">
|
||||
<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"
|
||||
@click="openRelatedPerson(relatedPerson.people)"
|
||||
>{{ 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>
|
||||
</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' } }
|
||||
},
|
||||
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>
|
||||
<Modal @close="showmodal = undefined" v-bind="showmodal" @dataevent="changeInfo" v-if="showmodal"></Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useStore } from "@/stores/index";
|
||||
export default {
|
||||
setup() {
|
||||
const store = useStore();
|
||||
return { store };
|
||||
},
|
||||
props: ["row", "pagename"],
|
||||
data() {
|
||||
return {
|
||||
record: undefined,
|
||||
relatedPeople: undefined,
|
||||
errors: {},
|
||||
showmodal: undefined,
|
||||
docid: this.$id(),
|
||||
data: this.store.common,
|
||||
isEditMode: this.isEditMode,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lang() {
|
||||
return this.store.lang;
|
||||
},
|
||||
isVietnamese() {
|
||||
return this.store.lang === "vi";
|
||||
},
|
||||
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 });
|
||||
} 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 });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findFieldName(code) {
|
||||
let field = this.data.find((v) => v.code === code);
|
||||
return field;
|
||||
},
|
||||
copy(value) {
|
||||
this.$copyToClipboard(value);
|
||||
this.$snackbar("Đã copy vào clipboard.", "Copy", "Success");
|
||||
},
|
||||
openPhone() {
|
||||
this.showmodal = {
|
||||
title: "Điện thoại",
|
||||
height: "180px",
|
||||
width: "400px",
|
||||
component: "common/Phone",
|
||||
vbind: { row: this.row, pagename: this.pagename },
|
||||
};
|
||||
},
|
||||
selected(attr, obj) {
|
||||
this.record[attr] = obj;
|
||||
},
|
||||
edit() {
|
||||
this.showmodal = {
|
||||
component: "customer/Customer",
|
||||
width: "80%",
|
||||
height: "600px",
|
||||
title: this.store.lang === "en" ? "Edit Customer" : "Chỉnh sửa khách hàng",
|
||||
vbind: { row: this.record, isEditMode: true },
|
||||
};
|
||||
},
|
||||
async changeInfo(v) {
|
||||
this.record = this.$copy(v);
|
||||
// refetch relatedPeople
|
||||
if (this.isIndividual) {
|
||||
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 });
|
||||
}
|
||||
},
|
||||
openUser(userId) {
|
||||
if (!userId) return;
|
||||
this.showmodal = {
|
||||
component: "user/UserInfo",
|
||||
width: "50%",
|
||||
height: "200px",
|
||||
title: "User",
|
||||
vbind: { userId: userId },
|
||||
};
|
||||
},
|
||||
async openRelatedPerson(peopleId) {
|
||||
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',
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user