This commit is contained in:
Viet An
2026-05-25 15:23:12 +07:00
parent 5f2a98977b
commit 6685a43360
48 changed files with 732 additions and 2901 deletions

View File

@@ -196,7 +196,10 @@ if (props.first) {
}
}
} else if (props.optionid) {
selected.value = await $getdata(props.api, { id: props.optionid }, undefined, true);
selected.value = await $getdata(props.api, {
filter: { id: props.optionid },
first: true,
});
}
if (selected.value) doSelect(selected.value);
@@ -282,7 +285,7 @@ function findObject(val) {
async function getData() {
isLoading.value = true;
params.value.filter = props.filter;
const data = await $getdata(props.api, undefined, params.value);
const data = await $getdata(props.api, { params: params.value });
isLoading.value = false;
return data;
}
@@ -301,7 +304,7 @@ async function getSuggestions(val) {
params.value.filter_or = filter_or;
if (props.filter) params.value.filter = $copy(props.filter);
isLoading.value = true;
suggestions.value = await $getdata(props.api, undefined, params.value);
suggestions.value = await $getdata(props.api, { params: params.value });
isLoading.value = false;
}

View File

@@ -249,7 +249,10 @@ var image = ref();
var file = ref();
var link = ref();
var tab = ref("message");
var record = await $getdata("useraction", { user: 1, action: props.row.id }, undefined, true);
var record = await $getdata("useraction", {
first: true,
filter: { user: 1, action: props.row.id },
});
var option = ref(record ? "your" : "system");
function getValue() {
if (option.value === "your") {

View File

@@ -26,10 +26,16 @@ const props = defineProps({
row: Object,
pagename: String,
});
var record = await $getdata("application", { id: props.row.id }, undefined, true);
let record = await $getdata("application", {
filter: { id: props.row.id },
first: true,
});
async function save() {
await $updateapi("application", record);
record = await $getdata("application", { id: props.row.id }, undefined, true);
record = await $getdata("application", {
filter: { id: props.row.id },
first: true,
});
$updatepage(props.pagename, record);
emit("close");
}

View File

@@ -95,7 +95,7 @@ export default {
},
async created() {
if (!this.row) return;
this.data = await this.$getdata(this.api, { ref: this.row.id });
this.data = await this.$getdata(this.api, { filter: { ref: this.row.id } });
},
computed: {
login: {

View File

@@ -256,7 +256,10 @@ 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);
const obj = await $getdata("company", {
first: true,
filter: { phone: record.value.phone },
});
if (obj) {
existedCustomer = obj;
errors.phone = "Số điện thoại đã tồn tại trong hệ thống.";
@@ -271,7 +274,10 @@ async function update() {
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>`, "Success");
record.value.id = rs.id;
let ele = await $getdata("company", { id: rs.id }, null, true);
let ele = await $getdata("company", {
first: true,
filter: { id: rs.id },
});
emit("update", ele);
emit("modalevent", { name: "dataevent", data: ele });
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,699 +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"
>&nbsp;</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>

View File

@@ -326,15 +326,22 @@ export default {
},
},
async created() {
this.record = await this.$getdata("customer", { id: this.row.customer || this.row.id }, undefined, true);
this.record = await this.$getdata("customer", { first: true, filter: { id: this.row.customer || this.row.id } });
if (this.isIndividual) {
this.relatedPeople = await this.$getdata("customerpeople", {
filter: {
customer: this.row.customer || this.row.id,
},
});
} else {
const org = await this.$getdata("organization", { customer: this.row.customer || this.row.id }, undefined, true);
const org = await this.$getdata("organization", {
first: true,
filter: { customer: this.row.customer || this.row.id },
});
this.relatedPeople = await this.$getdata("legalrep", {
filter: {
organization: org.id,
},
});
}
},
@@ -373,17 +380,19 @@ export default {
// refetch relatedPeople
if (this.isIndividual) {
this.relatedPeople = await this.$getdata("customerpeople", {
filter: {
customer: this.row.customer || this.row.id,
},
});
} else {
const org = await this.$getdata(
"organization",
{ customer: this.row.customer || this.row.id },
undefined,
true,
);
const org = await this.$getdata("organization", {
first: true,
filter: { customer: this.row.customer || this.row.id },
});
this.relatedPeople = await this.$getdata("legalrep", {
filter: {
organization: org.id,
},
});
}
},
@@ -398,7 +407,10 @@ export default {
};
},
async openRelatedPerson(peopleId) {
const peopleRow = await this.$getdata("people", { id: peopleId }, undefined, true);
const peopleRow = await this.$getdata("people", {
first: true,
filter: { id: peopleId },
});
this.showmodal = {
component: "people/PeopleView",
vbind: { row: peopleRow },

View File

@@ -145,7 +145,10 @@ export default {
},
methods: {
async openEvent(event) {
let row = await this.$getdata("sale", { id: event.sale }, undefined, true);
let row = await this.$getdata("sale", {
first: true,
filter: { id: event.sale },
});
this.showmodal = {
title: "Bán hàng",
height: "500px",

View File

@@ -34,7 +34,10 @@ export default {
if (!this.approvalcode && this.$store.state.login.approval_code)
this.approvalcode = this.$store.state.login.approval_code;
if (!this.approvalcode) {
let user = await this.$getdata("user", { id: this.$store.state.login.id }, undefined, true);
const user = await this.$getdata("user", {
first: true,
filter: { id: this.$store.state.login.id },
});
this.approvalcode = user.approval_code;
}
},

View File

@@ -43,7 +43,7 @@ export default {
let id = this.vbind.row.id;
let result;
if (this.setdeleted) {
let record = await this.$getdata(name, { id: id }, undefined, true);
let record = await this.$getdata(name, { first: true, filter: { id } });
record.deleted = 1;
result = await this.$updateapi(name, record);
} else result = await this.$deleteapi(name, id);

View File

@@ -1,31 +1,9 @@
<script setup>
import Products from "@/components/imports/Products.vue";
const { $snackbar } = useNuxtApp();
</script>
<template>
<div class="fixed-grid has-12-cols">
<div class="grid">
<div class="cell is-col-span-12">
<button @click="$snackbar('heyo', 'Success')">click</button>
<hr />
<div class="is-flex">
<div class="size-16 has-background-black"></div>
<div class="size-16 has-background-black-bis"></div>
<div class="size-16 has-background-black-ter"></div>
<div class="size-16 has-background-grey-darker"></div>
<div class="size-16 has-background-grey-dark"></div>
<div class="size-16 has-background-grey"></div>
<div class="size-16 has-background-grey-light"></div>
<div class="size-16 has-background-grey-lighter"></div>
<div class="size-16 has-background-grey-lightest"></div>
<div class="size-16 has-background-white-ter"></div>
<div class="size-16 has-background-white-bis"></div>
<div class="size-16 has-background-white"></div>
</div>
<div class="is-flex">
<div class="size-16 has-background-grey"></div>
</div>
</div>
<div class="cell is-col-span-12">
<Products />
</div>

View File

@@ -30,16 +30,14 @@ const body = ref($copy(defaultBody.value));
const productVariant = ref();
onMounted(async () => {
if (props.variantId) {
const variant = await $getdata(
"Product_Variant",
undefined,
{
const variant = await $getdata("Product_Variant", {
first: true,
params: {
filter: { id: props.variantId },
values:
"id,code,product,product__name,product__os,product__manufacturer,product__battery,product__screen,product__cpu,product__gpu,product__camera_system,product__sim,product__network_technology,product__charging_technology,product__external_storage,product__ip_rating,product__design,color,color__code,color__name,color__hex_code,ram,ram__code,ram__capacity,internal_storage,internal_storage__code,internal_storage__capacity,image,image__path,price,note,create_time,update_time",
},
true,
);
});
productVariant.value = variant;
@@ -64,6 +62,10 @@ onMounted(async () => {
}
});
watch(body, (newVal) => {
console.dir("body changed", newVal);
});
const isDirty = computed(() => !isEqual(body.value, defaultBody.value));
function selected(field, data) {

View File

@@ -27,16 +27,14 @@ const productVariant = ref();
onMounted(async () => {
// if props.variant (edit), fetch data to fill in body
if (props.variantId) {
const variant = await $getdata(
"Product_Variant",
undefined,
{
const variant = await $getdata("Product_Variant", {
first: true,
params: {
filter: { id: props.variantId },
values:
"id,code,product,product__name,product__os,product__manufacturer,product__battery,product__screen,product__cpu,product__gpu,product__camera_system,product__sim,product__network_technology,product__charging_technology,product__external_storage,product__ip_rating,product__design,color,color__code,color__name,color__hex_code,ram,ram__code,ram__capacity,internal_storage,internal_storage__code,internal_storage__capacity,image,image__path,price,note,create_time,update_time",
},
true,
);
});
productVariant.value = variant;

View File

@@ -55,7 +55,7 @@ const isVietnamese = computed(() => $store.lang.toLowerCase() === "vi");
const paymentScheduleItem = ref(null);
const contentPaymentQR = ref("");
const emailTemplate = await $getdata("emailtemplate", { id: props.idEmailTemplate }, undefined, false);
const emailTemplate = await $getdata("emailtemplate", { filter: { id: props.idEmailTemplate }, first: true });
templateEmailContent.value = emailTemplate[0] ?? null;

View File

@@ -18,8 +18,10 @@ export default {
let obj;
if (this.attr === "image_count") {
let rs = await this.$getdata(this.api, {
filter: {
ref: this.row.id,
file__type: 2,
},
});
if (rs.length > 0) {
obj = {

View File

@@ -40,8 +40,10 @@ export default {
},
async created() {
this.files = await this.$getdata(this.api, {
filter: {
ref: this.row.id,
file__type: 1,
},
});
if (this.files.length > 0)
this.vbind = {

View File

@@ -41,7 +41,7 @@ export default {
};
},
async created() {
this.record = await this.$getdata("file", { id: this.row.file || this.row.id }, undefined, true);
this.record = await this.$getdata("file", { filter: { id: this.row.file || this.row.id }, first: true });
},
methods: {
async save() {

View File

@@ -23,8 +23,10 @@ export default {
async created() {
this.pagedata = this.$getpage();
this.files = await this.$getdata(this.api, {
filter: {
ref: this.row.id,
file__type: 1,
},
});
this.vbind = {
pagename: this.pagename1,
@@ -62,7 +64,7 @@ export default {
});
if (this.pagename) {
let vapi = this.api.replace("file", "");
let ele = await this.$getdata(vapi, { id: this.row.id }, undefined, true);
let ele = await this.$getdata(vapi, { filter: { id: this.row.id }, first: true });
this.$updatepage(this.pagename, ele);
}
},

View File

@@ -67,7 +67,7 @@ export default {
},
},
async created() {
this.files = await this.$getdata(this.api, { ref: this.row.id }); //file__type: 2
this.files = await this.$getdata(this.api, { filter: { ref: this.row.id } }); //file__type: 2
},
methods: {
async remove(v, i) {
@@ -85,7 +85,7 @@ export default {
this.files = this.files.concat(rs);
if (this.pagename) {
let vapi = this.vapi ? this.vapi : this.api.replace("file", "");
let ele = await this.$getdata(vapi, { id: this.row.id }, undefined, true);
let ele = await this.$getdata(vapi, { filter: { id: this.row.id }, first: true });
this.$updatepage(this.pagename, ele);
}
},

View File

@@ -206,10 +206,12 @@ const layoutMode = ref(layoutModes.GRID);
const convertToWebp = ref(false);
const webpQuality = ref(80);
const project = await $getdata("project", { id: props.projectId }, undefined, true);
const project = await $getdata("project", { filter: { id: props.projectId }, first: true });
const images = ref([]);
const isForProduct = computed(() => props.productId !== undefined);
const product = isForProduct.value ? await $getdata("product", { id: props.productId }, undefined, true) : undefined;
const product = isForProduct.value
? await $getdata("product", { filter: { id: props.productId }, first: true })
: undefined;
const showmodal = ref();
const imgshow = ref([]);
const hashtag = ref();
@@ -219,17 +221,21 @@ async function loadImages() {
const values =
"id,file__hashtag,file__code,file,create_time,file__code,file__type,file__doc_type,file__name,file__file,file__size,file__caption,file__user__fullname,";
const projectImages = await $getdata("projectfile", undefined, {
const projectImages = await $getdata("projectfile", {
params: {
filter: { project: project.id, file__type: 2 },
values: values + "project",
sort: "-create_time",
},
});
if (isForProduct.value) {
const productImages = await $getdata("productfile", undefined, {
params: {
filter: { product: props.productId, file__type: 2 },
values: values + "product",
sort: "-create_time",
},
});
const sameTemplateImages = projectImages.filter((img) => img.file__name.startsWith(product.template_name));

View File

@@ -63,7 +63,7 @@ export default {
},
async created() {
let arr = [];
let files = await this.$getdata("file", { file__in: this.image });
let files = await this.$getdata("file", { filter: { file__in: this.image } });
this.image.map((v) => {
let found = this.$find(files, { file: v });
arr.push({ image: `${this.$getpath()}download/?name=${v}`, file: found });

View File

@@ -298,9 +298,9 @@ export default {
this.timer = setTimeout(() => this.startSearch(e.target.value), 150);
},
async startSearch(value) {
let filter = { user: this.login.id };
const filter = { user: this.login.id };
if (!this.$empty(value)) filter.name__icontains = value.toLowerCase();
this.data = await this.$getdata("file", filter);
this.data = await this.$getdata("file", { filter });
},
displayInput() {
this.showUrl = true;

View File

@@ -15,7 +15,10 @@ const props = defineProps({
row: Object,
});
async function open() {
let record = await $getdata("collaborator", { code: props.row.collaborator__code }, undefined, true);
const record = await $getdata("collaborator", {
first: true,
filter: { code: props.row.collaborator__code },
});
emit("clickevent", {
name: "dataevent",
data: {

View File

@@ -319,7 +319,10 @@ export default {
if (parsedItem && parsedItem.code) {
try {
const entryData = await $getdata("internalentry", { code: parsedItem.code }, undefined, true);
const entryData = await $getdata("internalentry", {
first: true,
filter: { code: parsedItem.code },
});
if (entryData) {
this.entries.push(entryData);
}

View File

@@ -154,12 +154,10 @@ var contractData = ref(null);
const loadContract = async () => {
if (record.value.transaction__code) {
try {
contractData.value = await $getdata(
"contract",
{ transaction__code: record.value.transaction__code },
undefined,
true,
);
contractData.value = await $getdata("contract", {
first: true,
filter: { transaction__code: record.value.transaction__code },
});
} catch (error) {
console.error("Error loading contract:", error);
contractData.value = null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -163,7 +163,7 @@ export default {
};
},
async created() {
this.setting = await this.$getdata("importsetting", { code: this.code }, undefined, true);
this.setting = await this.$getdata("importsetting", { first: true, filter: { code: this.code } });
this.requireFields = this.setting.detail;
},
beforeUnmount() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -339,7 +339,10 @@ async function update() {
if (!record.value.id) {
if (record.value.phone) {
record.value.phone = record.value.phone.trim();
let obj = await $getdata("people", { phone: record.value.phone }, undefined, true);
let obj = await $getdata("people", {
first: true,
filter: { phone: record.value.phone },
});
if (obj) {
existedPeople = obj;
errors.value.phone = "Số điện thoại đã tồn tại.";
@@ -368,7 +371,7 @@ async function update() {
record.image.map((v) => arr.push({ ref: record.value.id, file: v }));
await $insertapi("customerfile", arr, undefined, false);
}
let ele = await $getdata("people", { id: rs.id }, undefined, true);
let ele = await $getdata("people", { first: true, filter: { id: rs.id } });
emit("update", ele);
emit("modalevent", { name: "dataevent", data: ele });
if (props.pagename) $updatepage(props.pagename, ele);

View File

@@ -194,7 +194,10 @@ export default {
};
},
async created() {
this.record = await this.$getdata("people", { id: this.row.people || this.row.id }, undefined, true);
this.record = await this.$getdata("people", {
first: true,
filter: { id: this.row.people || this.row.id },
});
},
methods: {
findLang(code) {

View File

@@ -51,7 +51,7 @@ export default {
methods: {
async confirm() {
if (this.checkError()) return this.$snackbar("Mã phê duyệt gồm 4 số từ 0-9");
let user = await this.$getdata("user", undefined, { filter: { id: this.$store.state.login.id } }, true);
let user = await this.$getdata("user", { first: true, filter: { id: this.$store.state.login.id } });
user.approval_code = this.code;
await this.$updateapi("user", user);
},

View File

@@ -117,7 +117,10 @@ export default {
};
},
async created() {
this.record = await this.$getdata("user", undefined, { filter: { id: this.row.id } }, true);
this.record = await this.$getdata("user", {
first: true,
filter: { id: this.row.id },
});
this.radioBlocked = this.record.blocked ? 1 : 0;
},
methods: {
@@ -158,12 +161,14 @@ export default {
this.$set(this.check, v.code, this.check[v.code] ? false : true);
},
async setTokenExpiry() {
let rows = await this.$getdata("token", {
const rows = await this.$getdata("token", {
filter: {
user: this.record.id,
expiry: 0,
},
});
if (rows.length === 0) return;
rows.map((v) => (v.expiry = 1));
rows.forEach((v) => (v.expiry = 1));
this.$insertapi("token", rows, undefined, true);
},
},

View File

@@ -321,7 +321,7 @@ export default {
if (this.checkError()) return;
let rs = await this.$insertapi("gethash", { text: this.password });
this.hash = rs.rows[0];
let data = await this.$getdata("user", { username: this.username }, undefined, true);
let data = await this.$getdata("user", { first: true, filter: { username: this.username } });
if (data) {
return this.errors.push({
name: "username",

View File

@@ -104,7 +104,10 @@ export default {
methods: {
async changePassword() {
if (!this.checkPassword()) return;
let user = await this.$getdata("user", undefined, { filter: { id: this.row.id } }, true);
let user = await this.$getdata("user", {
first: true,
params: { filter: { id: this.row.id } },
});
let rs = await this.$insertapi("gethash", { text: this.password });
user.password = rs.rows[0];
let rs1 = await this.$updateapi("user", user);

View File

@@ -64,15 +64,13 @@ export default {
};
},
async created() {
this.record = await this.$getdata(
"user",
undefined,
{
this.record = await this.$getdata("user", {
first: true,
params: {
filter: { id: this.userId || this.store.login.id },
values: "id,username,fullname,type,type__name,create_time",
},
true,
);
});
},
};
</script>

View File

@@ -57,15 +57,13 @@ function getViewport() {
async function checkRedirect() {
if (route.query.username && route.query.token) {
let row = await $getdata(
"user",
null,
{
let row = await $getdata("user", {
first: true,
params: {
filter: { username: route.query.username },
values: "id,avatar,username,fullname,type,type__code,type__name,is_admin",
},
true,
);
});
if (row === "error" || row === undefined) return; /* $requestLogin(); */
else {
row.token = route.query.token;
@@ -77,8 +75,8 @@ async function checkRedirect() {
async function checkLogin() {
if ($store.login ? $store.login.token : false) {
$store.commit("rights", await $getdata("grouprights", { group: $store.login.type }));
let authtoken = await $getdata("token", { token: $store.login.token }, undefined, true);
$store.commit("rights", await $getdata("grouprights", { filter: { group: $store.login.type } }));
let authtoken = await $getdata("token", { first: true, filter: { token: $store.login.token } });
if (authtoken ? authtoken.expiry : true) return; /* $requestLogin(); */
authorized.value = true;
}

View File

@@ -1169,9 +1169,9 @@ export default defineNuxtPlugin((nuxtApp) => {
// get data
const getapi = async function (list) {
try {
let arr = list.map((v) => {
let found = apis.find((api) => api.name === v.name);
let url = (v.path ? paths.find((x) => x.name === v.path).url : path) + (v.url ? v.url : found.url);
const arr = list.map((v) => {
const found = apis.find((api) => api.name === v.name);
const url = (v.path ? paths.find((x) => x.name === v.path).url : path) + (v.url ? v.url : found.url);
let params = v.params ? v.params : found.params === undefined ? {} : found.params;
params.login = $store.login ? $store.login.id : undefined;
return { url, params };
@@ -1365,21 +1365,21 @@ export default defineNuxtPlugin((nuxtApp) => {
return copy;
};
const getdata = async function (name, filter, params, first) {
let found = findapi(name);
const getdata = async function (name, { filter, params, first = false } = {}) {
const found = findapi(name);
if (params) found.params = params;
else if (filter) found.params.filter = filter;
let rs = await getapi([found]);
let data = rs[0].data;
const rs = await getapi([found]);
const { data } = rs[0];
if (data) {
if (data.rows) {
return first ? (data.rows.length > 0 ? data.rows[0] : undefined) : data.rows;
return first ? data.rows[0] : data.rows;
} else {
let rows = Array.isArray(data) ? data : [data];
const rows = Array.isArray(data) ? data : [data];
return first ? (rows.length > 0 ? rows[0] : data) : data;
}
} else {
let rows = Array.isArray(data) ? data : [data];
const rows = Array.isArray(data) ? data : [data];
return first ? (rows.length > 0 ? rows[0] : data) : data;
}
};

View File

@@ -87,12 +87,12 @@ import MenuAccount from "@/components/menu/MenuAccount.vue";
import ImageLayout from "@/components/media/ImageLayout.vue";
import CountdownTimer from "@/components/common/CountdownTimer.vue";
import CustomerInfo2 from "@/components/customer/CustomerInfo2.vue";
import CustomerInfo from "@/components/customer/CustomerInfo.vue";
import MenuFile from "@/components/menu/MenuFile.vue";
const components = {
PivotDataView,
CustomerInfo2,
CustomerInfo,
CountdownTimer,
InternalEntry,
ViewList,