changes
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.delete {
|
.delete {
|
||||||
--bulma-delete-background-alpha: 0.3;
|
--bulma-delete-background-alpha: 0.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-card {
|
.modal-card {
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
id: String,
|
|
||||||
component: String,
|
|
||||||
title: String,
|
|
||||||
width: String,
|
|
||||||
height: String,
|
|
||||||
vbind: Object,
|
|
||||||
onClose: Function,
|
|
||||||
onEvent: Function,
|
|
||||||
});
|
|
||||||
|
|
||||||
const componentFiles = import.meta.glob("@/components/**/*.vue");
|
|
||||||
const resolvedComponent = shallowRef(null);
|
|
||||||
|
|
||||||
function loadDynamicComponent() {
|
|
||||||
if (!props.component) {
|
|
||||||
resolvedComponent.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = `/components/${props.component}.vue`;
|
|
||||||
const componentPath = Object.keys(componentFiles).find((path) => path.endsWith(fullPath));
|
|
||||||
|
|
||||||
if (componentPath) {
|
|
||||||
resolvedComponent.value = defineAsyncComponent(componentFiles[componentPath]);
|
|
||||||
} else {
|
|
||||||
console.error(`Không tìm thấy component tại: ${fullPath}`);
|
|
||||||
resolvedComponent.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theo dõi sự thay đổi của props.component để load lại nếu cần
|
|
||||||
watchEffect(() => {
|
|
||||||
loadDynamicComponent();
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleEvent = (id, eventName, data) => props.onEvent(id, eventName, data);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
:id="id"
|
|
||||||
class="modal is-active has-text-text-20"
|
|
||||||
@click="
|
|
||||||
(e) => {
|
|
||||||
if (e.target.classList.contains('modal-background')) props.onClose(id);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div
|
|
||||||
class="modal-card"
|
|
||||||
:style="{
|
|
||||||
// width: $store.viewport <= 2 ? 'calc(100% - 2rem)' : width || '60%',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<header
|
|
||||||
v-if="title"
|
|
||||||
class="modal-card-head px-4 py-3"
|
|
||||||
>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="field is-grouped is-align-items-center">
|
|
||||||
<div class="control is-expanded has-text-left">
|
|
||||||
<p
|
|
||||||
class="fs-17 font-semibold has-text-primary"
|
|
||||||
v-html="title"
|
|
||||||
></p>
|
|
||||||
</div>
|
|
||||||
<div class="control has-text-right">
|
|
||||||
<button
|
|
||||||
class="delete is-medium"
|
|
||||||
@click="onClose(id)"
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<section
|
|
||||||
class="modal-card-body p-4"
|
|
||||||
:style="{
|
|
||||||
minHeight: height || '750px',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="resolvedComponent"
|
|
||||||
v-bind="vbind"
|
|
||||||
@close="onClose(id)"
|
|
||||||
@event="(e) => handleEvent(id, e.name, e.data)"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot pt-0 px-4 pb-4"></footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -152,8 +152,14 @@ const props = defineProps({
|
|||||||
position: String,
|
position: String,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
vdata: Array,
|
vdata: Array,
|
||||||
clearable: Boolean,
|
clearable: {
|
||||||
placeholder: String,
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
searchfield: Array,
|
searchfield: Array,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,52 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
record: Object,
|
||||||
|
attr: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { $copy, $empty, $errEmail } = useNuxtApp();
|
||||||
|
const emit = defineEmits(["email"]);
|
||||||
|
const email = ref(props.record?.[props.attr] ? $copy(props.record[props.attr]) : undefined);
|
||||||
|
const error = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.record?.[props.attr],
|
||||||
|
(v) => {
|
||||||
|
email.value = v ? $copy(v) : undefined;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function doCheck() {
|
||||||
|
if ($empty(email.value)) {
|
||||||
|
email.value = undefined;
|
||||||
|
error.value = false;
|
||||||
|
return emit("email", null);
|
||||||
|
}
|
||||||
|
error.value = !!$errEmail(email.value);
|
||||||
|
emit("email", email.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input
|
<input
|
||||||
:class="`input ${error ? 'is-danger' : ''} ${disabled ? 'has-text-black' : ''}`"
|
:class="['input', error && 'is-danger', disabled && 'has-text-black']"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="placeholder || ''"
|
v-model="email"
|
||||||
v-model="value"
|
|
||||||
@keyup="doCheck"
|
@keyup="doCheck"
|
||||||
:disabled="disabled || false"
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
/>
|
/>
|
||||||
<span class="icon is-left">
|
<span class="icon is-left">
|
||||||
<SvgIcon v-bind="{ name: 'email.svg', type: 'gray', size: 21 }"></SvgIcon>
|
<Icon
|
||||||
|
name="material-symbols:mail-outline-rounded"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: ["record", "attr", "placeholder", "disabled"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
value: this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined,
|
|
||||||
error: undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
record: function (newVal) {
|
|
||||||
this.value = this.record[this.attr] ? this.$copy(this.record[this.attr]) : undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
doCheck() {
|
|
||||||
if (this.$empty(this.value)) {
|
|
||||||
this.value = undefined;
|
|
||||||
this.error = false;
|
|
||||||
return this.$emit("email", null);
|
|
||||||
}
|
|
||||||
let check = this.$errEmail(this.value);
|
|
||||||
this.error = check ? true : false;
|
|
||||||
this.$emit("email", this.value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,55 +1,59 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
record: Object,
|
||||||
|
attr: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["phone"]);
|
||||||
|
const phoneNum = ref("");
|
||||||
|
const initial = props.record && props.record[props.attr];
|
||||||
|
phoneNum.value = initial ? String(initial) : "";
|
||||||
|
|
||||||
|
watch(phoneNum, (newVal) => {
|
||||||
|
// giữ lại CHỈ chữ số
|
||||||
|
const digits = newVal.replaceAll(/\D/g, "");
|
||||||
|
|
||||||
|
// sync lại UI nếu user nhập ký tự khác số
|
||||||
|
if (digits !== newVal) {
|
||||||
|
phoneNum.value = digits;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit string số hoặc null
|
||||||
|
emit("phone", digits.length ? digits : null);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.record,
|
||||||
|
(newRecord) => {
|
||||||
|
const v = newRecord && newRecord[props.attr];
|
||||||
|
phoneNum.value = v ? String(v) : "";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="placeholder || ''"
|
v-model="phoneNum"
|
||||||
v-model="value"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled || false"
|
:disabled="disabled"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
|
maxlength="15"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
/>
|
/>
|
||||||
<span class="icon is-left">
|
<span class="icon is-left">
|
||||||
<SvgIcon v-bind="{ name: 'phone.png', type: 'gray', size: 20 }"></SvgIcon>
|
<Icon
|
||||||
|
name="material-symbols:call-outline-rounded"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: ["record", "attr", "placeholder", "disabled"],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
value: "",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
const initial = this.record?.[this.attr];
|
|
||||||
this.value = initial ? String(initial) : "";
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
/** giống InputEmail.vue: watch value → emit ngay */
|
|
||||||
value(newVal) {
|
|
||||||
// giữ lại CHỈ chữ số
|
|
||||||
const digits = String(newVal).replace(/\D/g, "");
|
|
||||||
|
|
||||||
// sync lại UI nếu user nhập ký tự khác số
|
|
||||||
if (digits !== newVal) {
|
|
||||||
this.value = digits;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit string số hoặc null
|
|
||||||
this.$emit("phone", digits.length ? digits : null);
|
|
||||||
},
|
|
||||||
|
|
||||||
record(newVal) {
|
|
||||||
const v = newVal?.[this.attr];
|
|
||||||
this.value = v ? String(v) : "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,705 +0,0 @@
|
|||||||
<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", {
|
|
||||||
first: true,
|
|
||||||
filter: { phone: record.value.phone.trim() },
|
|
||||||
});
|
|
||||||
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", {
|
|
||||||
first: true,
|
|
||||||
filter: { email: record.value.email.trim() },
|
|
||||||
});
|
|
||||||
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", { first: true, filter: { legal_code: record.value.legal_code } });
|
|
||||||
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", { first: true, filter: { id: customerId } });
|
|
||||||
|
|
||||||
$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", { first: true, filter: { customer: props.row.id } });
|
|
||||||
individualData.value = ind || { dob: null, sex: 1 };
|
|
||||||
const rows = await $getdata("customerpeople", { filter: { customer: props.row.id } });
|
|
||||||
people.value = rows.length > 0 ? rows : [{}];
|
|
||||||
} else {
|
|
||||||
const org = await $getdata("organization", { first: true, filter: { customer: props.row.id } });
|
|
||||||
organizationData.value = org || { established_date: null };
|
|
||||||
if (org && org.id) {
|
|
||||||
const rows = await $getdata("legalrep", { filter: { 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>
|
|
||||||
190
app/components/customer/CustomerQuickAdd.vue
Normal file
190
app/components/customer/CustomerQuickAdd.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="fixed-grid">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="cell is-col-span-2">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Họ và tên<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="record.fullname"
|
||||||
|
placeholder="Nguyễn Văn A"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="errors.fullname"
|
||||||
|
class="help is-danger"
|
||||||
|
>
|
||||||
|
{{ errors.fullname }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Số điện thoại<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<InputPhone
|
||||||
|
v-bind="{
|
||||||
|
record,
|
||||||
|
attr: 'phone',
|
||||||
|
onPhone: (e) => selected('phone', e),
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="errors.phone"
|
||||||
|
class="help is-danger"
|
||||||
|
>
|
||||||
|
{{ errors.phone }}
|
||||||
|
<a
|
||||||
|
v-if="existedCustomer"
|
||||||
|
class="has-text-primary"
|
||||||
|
@click="showCustomer()"
|
||||||
|
>
|
||||||
|
Chi tiết
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Email<b class="ml-1 has-text-danger">*</b></label>
|
||||||
|
<InputEmail
|
||||||
|
v-bind="{
|
||||||
|
record,
|
||||||
|
attr: 'email',
|
||||||
|
onEmail: (e) => selected('email', e),
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="help is-danger"
|
||||||
|
v-if="errors.email"
|
||||||
|
>
|
||||||
|
{{ errors.email }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 buttons is-right">
|
||||||
|
<button
|
||||||
|
class="button is-white"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
Hủy
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['button is-primary', isLoading && 'is-loading']"
|
||||||
|
@click="createCustomer"
|
||||||
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:add-rounded"
|
||||||
|
:size="20"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Tạo khách hàng</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
v-if="showModal"
|
||||||
|
v-bind="showModal"
|
||||||
|
@close="showModal = undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import InputPhone from "@/components/common/InputPhone.vue";
|
||||||
|
import InputEmail from "@/components/common/InputEmail.vue";
|
||||||
|
import { useStore } from "~/stores/index";
|
||||||
|
import { isNotNil, pickBy } from "es-toolkit";
|
||||||
|
|
||||||
|
const emit = defineEmits(["close", "update", "modalevent"]);
|
||||||
|
const { $getdata, $insertapi, $empty, $errEmail, $errPhone, $resetNull, $snackbar } = useNuxtApp();
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const isVietnamese = computed(() => store.lang === "vi");
|
||||||
|
|
||||||
|
const record = ref({});
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const errors = ref({});
|
||||||
|
const showModal = ref();
|
||||||
|
const existedCustomer = ref();
|
||||||
|
|
||||||
|
function showCustomer() {
|
||||||
|
showModal.value = {
|
||||||
|
component: "customer/CustomerView",
|
||||||
|
width: "60%",
|
||||||
|
height: "600px",
|
||||||
|
title: "Khách hàng",
|
||||||
|
vbind: { row: existedCustomer.value },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = (field, value) => {
|
||||||
|
record.value[field] = value && typeof value === "object" ? value.id : value;
|
||||||
|
if (errors.value[field]) delete errors.value[field];
|
||||||
|
};
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
errors.value.phone = $errPhone(record.value.phone);
|
||||||
|
errors.value.email = $errEmail(record.value.email);
|
||||||
|
|
||||||
|
const realErrors = pickBy(errors.value, isNotNil);
|
||||||
|
return Object.keys(realErrors).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCustomer() {
|
||||||
|
try {
|
||||||
|
if (checkError()) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
if (record.value.phone) {
|
||||||
|
const phoneCheck = await $getdata("customer", {
|
||||||
|
first: true,
|
||||||
|
filter: { phone: record.value.phone.trim() },
|
||||||
|
});
|
||||||
|
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", {
|
||||||
|
first: true,
|
||||||
|
filter: { email: record.value.email.trim() },
|
||||||
|
});
|
||||||
|
if (emailCheck) {
|
||||||
|
existedCustomer.value = emailCheck;
|
||||||
|
errors.value.email = isVietnamese.value ? "Email đã tồn tại." : "Email already exists.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerData = $resetNull({ ...record.value });
|
||||||
|
const res = await $insertapi("customer", customerData, undefined, false);
|
||||||
|
if (!res || res === "error") return;
|
||||||
|
|
||||||
|
const completedData = await $getdata("customer", {
|
||||||
|
first: true,
|
||||||
|
filter: { id: res.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
$snackbar("Khách hàng đã được khởi tạo thành công", "Success");
|
||||||
|
emit("modalevent", { name: "dataevent", data: completedData });
|
||||||
|
emit("update", completedData);
|
||||||
|
setTimeout(() => emit("close"), 100);
|
||||||
|
} catch (e) {
|
||||||
|
isLoading.value = false;
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -7,7 +7,7 @@ const props = defineProps({
|
|||||||
variant: Object,
|
variant: Object,
|
||||||
});
|
});
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { $getdata } = useNuxtApp();
|
const { $getdata, $snackbar } = useNuxtApp();
|
||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close"]);
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
@@ -27,6 +27,7 @@ function toggleSelected(imeiRec) {
|
|||||||
|
|
||||||
function addToCart() {
|
function addToCart() {
|
||||||
store.selectedImeis = [...store.selectedImeis, ...selectedImeis.value];
|
store.selectedImeis = [...store.selectedImeis, ...selectedImeis.value];
|
||||||
|
$snackbar(`Thêm ${selectedImeis.value.length} sản phẩm vào giỏ hàng`, "Success");
|
||||||
emit("close");
|
emit("close");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ProductCard from "@/components/pos/ProductCard.vue";
|
import ProductCard from "@/components/pos/ProductCard.vue";
|
||||||
|
import SearchBox from "@/components/SearchBox.vue";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
function selectProduct() {
|
function openModal() {
|
||||||
store.showmodal = {
|
store.showmodal = {
|
||||||
component: "pos/ProductSelection",
|
component: "pos/ProductSelection",
|
||||||
title: "Chọn sản phẩm",
|
title: "Chọn sản phẩm",
|
||||||
width: "85%",
|
width: "85%",
|
||||||
height: "80%",
|
height: "500px",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customer = ref(null);
|
||||||
|
const paymentMethod = ref(null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<button
|
<button
|
||||||
@click="selectProduct"
|
@click="openModal"
|
||||||
class="button is-primary"
|
class="button is-primary"
|
||||||
>
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
@@ -42,19 +46,29 @@ function selectProduct() {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span>Giỏ hàng</span>
|
<span>Giỏ hàng</span>
|
||||||
|
<span>({{ store.selectedImeis.length }})</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="is-flex is-flex-direction-column is-gap-1">
|
<div
|
||||||
|
v-if="store.selectedImeis.length > 0"
|
||||||
|
class="is-flex is-flex-direction-column is-gap-1"
|
||||||
|
>
|
||||||
<ProductCard
|
<ProductCard
|
||||||
v-for="imei in store.selectedImeis"
|
v-for="imei in store.selectedImeis"
|
||||||
:key="imei.id"
|
:key="imei.id"
|
||||||
:imei="imei"
|
:imei="imei"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
v-else
|
||||||
|
class="py-4 fs-16 has-text-grey has-text-centered"
|
||||||
|
>
|
||||||
|
Không có sản phẩm nào trong giỏ hàng
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell is-col-span-4">
|
<div class="cell is-col-span-4">
|
||||||
<div class="card">
|
<div class="card mb-3">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p class="icon-text fs-17 font-semibold mb-4">
|
<p class="icon-text fs-17 font-semibold mb-4">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
@@ -65,7 +79,51 @@ function selectProduct() {
|
|||||||
</span>
|
</span>
|
||||||
<span>Khách hàng</span>
|
<span>Khách hàng</span>
|
||||||
</p>
|
</p>
|
||||||
<div>customers content</div>
|
<div>
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
api: 'customer',
|
||||||
|
field: 'label',
|
||||||
|
column: ['label'],
|
||||||
|
first: true,
|
||||||
|
clearable: true,
|
||||||
|
placeholder: 'Khách hàng',
|
||||||
|
addon: {
|
||||||
|
component: 'customer/CustomerQuickAdd',
|
||||||
|
width: '50%',
|
||||||
|
height: 'auto',
|
||||||
|
title: 'Tạo khách hàng',
|
||||||
|
},
|
||||||
|
onOption: (e) => (customer = e),
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="icon-text fs-17 font-semibold mb-4">
|
||||||
|
<span class="icon">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:credit-card-outline"
|
||||||
|
:size="18"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Thanh toán</span>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<SearchBox
|
||||||
|
v-bind="{
|
||||||
|
api: 'Payment_Method',
|
||||||
|
field: 'name',
|
||||||
|
column: ['name'],
|
||||||
|
first: true,
|
||||||
|
clearable: true,
|
||||||
|
placeholder: 'Phương thức thanh toán',
|
||||||
|
onOption: (e) => (paymentMethod = e),
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -178,14 +178,15 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetNull = function (obj) {
|
const resetNull = function (obj) {
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
return Object.fromEntries(
|
||||||
if (typeof value === "string") {
|
Object.entries(obj).map(([key, value]) => {
|
||||||
let val = value.trim();
|
if (typeof value === "string") {
|
||||||
if (val === "" || val === "") val = null;
|
const val = value.trim();
|
||||||
obj[key] = val;
|
return [key, val === "" ? null : val];
|
||||||
}
|
}
|
||||||
}
|
return [key, value];
|
||||||
return obj;
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = function (text) {
|
const copyToClipboard = function (text) {
|
||||||
|
|||||||
@@ -448,55 +448,13 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
url_detail: "data-detail/Customer/",
|
url_detail: "data-detail/Customer/",
|
||||||
params: {
|
params: {
|
||||||
values:
|
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",
|
"id,code,fullname,phone,email,type,type__name,creator,creator__fullname,updater,updater__fullname,create_time,update_time",
|
||||||
distinct_values: {
|
distinct_values: {
|
||||||
label: {
|
label: {
|
||||||
type: "Concat",
|
type: "Concat",
|
||||||
field: ["code", "fullname", "phone", "legal_code"],
|
field: ["code", "fullname", "phone"],
|
||||||
},
|
},
|
||||||
order: { type: "RowNumber" },
|
order: { type: "RowNumber" },
|
||||||
image_count: {
|
|
||||||
type: "Count",
|
|
||||||
field: "id",
|
|
||||||
subquery: { model: "Customer_File", column: "ref" },
|
|
||||||
},
|
|
||||||
count_note: {
|
|
||||||
type: "Count",
|
|
||||||
field: "id",
|
|
||||||
subquery: { model: "Customer_Note", column: "ref" },
|
|
||||||
},
|
|
||||||
count_product: {
|
|
||||||
type: "Count",
|
|
||||||
field: "id",
|
|
||||||
subquery: {
|
|
||||||
model: "Product_Booked",
|
|
||||||
column: "transaction__customer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sum_product: {
|
|
||||||
type: "Sum",
|
|
||||||
field: "transaction__sale_price",
|
|
||||||
subquery: {
|
|
||||||
model: "Product_Booked",
|
|
||||||
column: "transaction__customer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sum_receiver: {
|
|
||||||
type: "Sum",
|
|
||||||
field: "transaction__amount_received",
|
|
||||||
subquery: {
|
|
||||||
model: "Product_Booked",
|
|
||||||
column: "transaction__customer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sum_remain: {
|
|
||||||
type: "Sum",
|
|
||||||
field: "transaction__amount_remain",
|
|
||||||
subquery: {
|
|
||||||
model: "Product_Booked",
|
|
||||||
column: "transaction__customer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
summary: "annotate",
|
summary: "annotate",
|
||||||
filter: { deleted: 0 },
|
filter: { deleted: 0 },
|
||||||
@@ -1130,6 +1088,12 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
url_detail: "data-detail/Product_Image/",
|
url_detail: "data-detail/Product_Image/",
|
||||||
params: {},
|
params: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Payment_Method",
|
||||||
|
url: "data/Payment_Method/",
|
||||||
|
url_detail: "data-detail/Payment_Method/",
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const { $copy, $clone, $updateSeriesFields, $snackbar, $store, $remove, $dialog } = nuxtApp;
|
const { $copy, $clone, $updateSeriesFields, $snackbar, $store, $remove, $dialog } = nuxtApp;
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ import MenuParam from "@/components/menu/MenuParam.vue";
|
|||||||
import MenuAdd from "@/components/menu/MenuAdd.vue";
|
import MenuAdd from "@/components/menu/MenuAdd.vue";
|
||||||
import MenuCollab from "@/components/menu/MenuCollab.vue";
|
import MenuCollab from "@/components/menu/MenuCollab.vue";
|
||||||
import MenuNote from "@/components/menu/MenuNote.vue";
|
import MenuNote from "@/components/menu/MenuNote.vue";
|
||||||
|
import MenuFile from "@/components/menu/MenuFile.vue";
|
||||||
import MenuPayment from "@/components/menu/MenuPayment.vue";
|
import MenuPayment from "@/components/menu/MenuPayment.vue";
|
||||||
import ScrollBox from "@/components/datatable/ScrollBox.vue";
|
import ScrollBox from "@/components/datatable/ScrollBox.vue";
|
||||||
import Reservation from "@/components/modal/Reservation.vue";
|
import Reservation from "@/components/modal/Reservation.vue";
|
||||||
@@ -88,13 +89,12 @@ import MenuAccount from "@/components/menu/MenuAccount.vue";
|
|||||||
import ImageLayout from "@/components/media/ImageLayout.vue";
|
import ImageLayout from "@/components/media/ImageLayout.vue";
|
||||||
|
|
||||||
import CountdownTimer from "@/components/common/CountdownTimer.vue";
|
import CountdownTimer from "@/components/common/CountdownTimer.vue";
|
||||||
import CustomerInfo from "@/components/customer/CustomerInfo.vue";
|
|
||||||
import CustomerForm from "@/components/customer/CustomerForm.vue";
|
import CustomerForm from "@/components/customer/CustomerForm.vue";
|
||||||
import MenuFile from "@/components/menu/MenuFile.vue";
|
import CustomerQuickAdd from "@/components/customer/CustomerQuickAdd.vue";
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
PivotDataView,
|
PivotDataView,
|
||||||
CustomerInfo,
|
CustomerQuickAdd,
|
||||||
CustomerForm,
|
CustomerForm,
|
||||||
CountdownTimer,
|
CountdownTimer,
|
||||||
InternalEntry,
|
InternalEntry,
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
modules: ["@pinia/nuxt", "pinia-plugin-persistedstate/nuxt", "@nuxt/image", "nuxt-qrcode", "@nuxt/icon"],
|
modules: ["@pinia/nuxt", "pinia-plugin-persistedstate/nuxt", "@nuxt/image", "nuxt-qrcode", "@nuxt/icon"],
|
||||||
compatibilityDate: "2025-10-01",
|
compatibilityDate: "2026-06-01",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user