chore: install prettier

This commit is contained in:
Viet An
2026-05-04 15:22:27 +07:00
parent 93d29ca7d8
commit bd58e2b847
267 changed files with 22950 additions and 13581 deletions

View File

@@ -3,13 +3,13 @@ import { useStore } from "~/stores/index";
import { useAdvancedWorkflow } from "~/composables/useAdvancedWorkflow";
import Contract from "~/components/application/Contract.vue";
import Datepicker from "~/components/datepicker/Datepicker.vue";
import dayjs from 'dayjs';
import dayjs from "dayjs";
const props = defineProps({
transactionId: Number,
});
const emit = defineEmits(['close', 'modalevent']);
const emit = defineEmits(["close", "modalevent"]);
const { $getdata, $snackbar } = useNuxtApp();
const store = useStore();
@@ -23,7 +23,7 @@ const showContract = ref(false);
const contractId = ref(null);
// Contract Date
const initialContractDate = dayjs().format('YYYY-MM-DD');
const initialContractDate = dayjs().format("YYYY-MM-DD");
const contractDate = ref(initialContractDate);
const dateRecord = ref({ contractDate: initialContractDate });
@@ -31,7 +31,7 @@ const customerViewAddon = {
component: "customer/CustomerView",
width: "70%",
height: "600px",
title: "Chi tiết khách hàng"
title: "Chi tiết khách hàng",
};
const customerViewAdd = {
@@ -43,9 +43,9 @@ const customerViewAdd = {
onMounted(async () => {
if (props.transactionId) {
transaction.value = await $getdata('transaction', { id: props.transactionId }, undefined, true);
transaction.value = await $getdata("transaction", { id: props.transactionId }, undefined, true);
if (transaction.value && transaction.value.customer) {
currentCustomer.value = await $getdata('customer', { id: transaction.value.customer }, undefined, true);
currentCustomer.value = await $getdata("customer", { id: transaction.value.customer }, undefined, true);
}
}
});
@@ -59,7 +59,7 @@ function updateTransactionCustomerDisplay(result) {
if (result?.success) {
const contract = result?.contract;
if (contract && contract.id) {
$snackbar(`Đổi khách hàng thành công. Hợp đồng mới được tạo với ID: ${contract.id}`, { type: 'is-success' });
$snackbar(`Đổi khách hàng thành công. Hợp đồng mới được tạo với ID: ${contract.id}`, { type: "is-success" });
contractId.value = contract.id;
showContract.value = true;
}
@@ -68,23 +68,19 @@ function updateTransactionCustomerDisplay(result) {
async function confirmChangeCustomer() {
if (!newCustomer.value) {
$snackbar('Vui lòng chọn khách hàng mới.', { type: 'is-warning' });
$snackbar("Vui lòng chọn khách hàng mới.", { type: "is-warning" });
return;
}
workflowResult.value = null;
try {
const result = await updateTransactionCustomer(
props.transactionId,
newCustomer.value.id,
contractDate.value
);
const result = await updateTransactionCustomer(props.transactionId, newCustomer.value.id, contractDate.value);
workflowResult.value = result;
updateTransactionCustomerDisplay(result);
} catch (error) {
console.error('Workflow execution error:', error);
$snackbar('Có lỗi xảy ra khi thực thi workflow.', { type: 'is-danger' });
console.error("Workflow execution error:", error);
$snackbar("Có lỗi xảy ra khi thực thi workflow.", { type: "is-danger" });
}
}
</script>
@@ -103,12 +99,19 @@ async function confirmChangeCustomer() {
</div>
<div>
<p class="has-text-grey is-size-7">Tên</p>
<p class="is-size-6">{{ currentCustomer.name || currentCustomer.fullname }}</p>
<p class="is-size-6">
{{ currentCustomer.name || currentCustomer.fullname }}
</p>
</div>
<div>
<p class="has-text-grey is-size-7">Loại</p>
<p class="is-size-6"><span class="tag" :class="currentCustomer.type === 1 ? 'is-info' : 'is-warning'">{{
currentCustomer.type === 1 ? 'Cá nhân' : 'Tổ chức' }}</span></p>
<p class="is-size-6">
<span
class="tag"
:class="currentCustomer.type === 1 ? 'is-info' : 'is-warning'"
>{{ currentCustomer.type === 1 ? "Cá nhân" : "Tổ chức" }}</span
>
</p>
</div>
<div>
<p class="has-text-grey is-size-7">Điện thoại</p>
@@ -124,20 +127,30 @@ async function confirmChangeCustomer() {
<div class="field mb-3">
<label class="label">Chọn khách hàng mới<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<SearchBox v-bind="{
api: 'customer',
field: 'label',
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
column: ['code', 'fullname', 'phone', 'legal_code'],
first: true,
viewaddon: customerViewAddon,
addon: customerViewAdd,
placeholder: 'Tìm khách hàng theo mã, tên, số điện thoại hoặc CMND'
}" @option="(customer) => { newCustomer = customer }" />
<SearchBox
v-bind="{
api: 'customer',
field: 'label',
searchfield: ['code', 'fullname', 'phone', 'legal_code'],
column: ['code', 'fullname', 'phone', 'legal_code'],
first: true,
viewaddon: customerViewAddon,
addon: customerViewAdd,
placeholder: 'Tìm khách hàng theo mã, tên, số điện thoại hoặc CMND',
}"
@option="
(customer) => {
newCustomer = customer;
}
"
/>
</div>
</div>
<div v-if="newCustomer" class="mb-4">
<div
v-if="newCustomer"
class="mb-4"
>
<h4 class="is-size-6 has-text-weight-bold mb-2">Khách hàng mới được chọn</h4>
<div class="is-flex is-flex-wrap-wrap is-gap-4">
<div>
@@ -146,12 +159,19 @@ async function confirmChangeCustomer() {
</div>
<div>
<p class="has-text-grey is-size-7">Tên</p>
<p class="is-size-6">{{ newCustomer.name || newCustomer.fullname }}</p>
<p class="is-size-6">
{{ newCustomer.name || newCustomer.fullname }}
</p>
</div>
<div>
<p class="has-text-grey is-size-7">Loại</p>
<p class="is-size-6"><span class="tag" :class="newCustomer.type === 1 ? 'is-info' : 'is-warning'">{{
newCustomer.type === 1 ? 'Cá nhân' : 'Tổ chức' }}</span></p>
<p class="is-size-6">
<span
class="tag"
:class="newCustomer.type === 1 ? 'is-info' : 'is-warning'"
>{{ newCustomer.type === 1 ? "Cá nhân" : "Tổ chức" }}</span
>
</p>
</div>
<div>
<p class="has-text-grey is-size-7">Điện thoại</p>
@@ -166,30 +186,45 @@ async function confirmChangeCustomer() {
<div class="field mb-4">
<label class="label">Ngày ký hợp đồng mới<b class="ml-1 has-text-danger">*</b></label>
<div class="control" style="max-width: 200px;">
<Datepicker
:record="dateRecord"
attr="contractDate"
@date="updateContractDate"
position="is-top-left"
<div
class="control"
style="max-width: 200px"
>
<Datepicker
:record="dateRecord"
attr="contractDate"
@date="updateContractDate"
position="is-top-left"
/>
</div>
</div>
<div class="is-flex is-justify-content-flex-end is-gap-2">
<button class="button" @click="$emit('close')">Hủy</button>
<button class="button is-primary" :disabled="!newCustomer" :class="{ 'is-loading': isLoading }"
@click="confirmChangeCustomer">
<button
class="button"
@click="$emit('close')"
>
Hủy
</button>
<button
class="button is-primary"
:disabled="!newCustomer"
:class="{ 'is-loading': isLoading }"
@click="confirmChangeCustomer"
>
Xác nhận
</button>
</div>
</div>
<div v-else class="is-flex is-justify-content-center is-align-items-center" style="min-height: 200px;">
<div
v-else
class="is-flex is-justify-content-center is-align-items-center"
style="min-height: 200px"
>
<div class="loader"></div>
</div>
</div>
<div v-else>
<Contract :contractId="contractId" />
</div>
</template>
</template>

View File

@@ -6,78 +6,159 @@
<div class="column is-half">
<div class="mb-5">
<p class="title is-5 mb-3">Thông tin Giao dịch</p>
<p><strong>Loại giao dịch:</strong> {{ phaseInfo.name || '-' }}</p>
<p><strong>Chính sách tài chính:</strong> {{ selectedPolicy.name || '-' }}</p>
<p><strong>Loại giao dịch:</strong> {{ phaseInfo.name || "-" }}</p>
<p>
<strong>Chính sách tài chính:</strong>
{{ selectedPolicy.name || "-" }}
</p>
<p><strong>Giá gốc:</strong> {{ $numtoString(originPrice) }}</p>
<p><strong>Số tiền đặt cọc:</strong> <span class="has-text-primary has-text-weight-bold">{{
$numtoString(depositAmount) }}</span></p>
<p><strong>Ngày hợp đồng:</strong> <span class="has-text-primary has-text-weight-bold">{{
formatDate(initialContractDate) }}</span></p>
<p><strong>Ngày hết hạn GD:</strong> <span class="has-text-primary has-text-weight-bold">{{
formatDate(editableDueDate) }}</span></p>
<p>
<strong>Số tiền đặt cọc:</strong>
<span class="has-text-primary has-text-weight-bold">{{ $numtoString(depositAmount) }}</span>
</p>
<p>
<strong>Ngày hợp đồng:</strong>
<span class="has-text-primary has-text-weight-bold">{{ formatDate(initialContractDate) }}</span>
</p>
<p>
<strong>Ngày hết hạn GD:</strong>
<span class="has-text-primary has-text-weight-bold">{{ formatDate(editableDueDate) }}</span>
</p>
</div>
<hr>
<hr />
<div>
<p class="title is-5 mb-3">Thông tin Khách hàng</p>
<div class="columns">
<!-- Original Customer Column -->
<div class="column">
<p><strong>{{ isIndividual ? 'Khách hàng' : 'Tổ chức' }}</strong></p>
<p><strong> KH:</strong> {{ selectedCustomer.code || '-' }}</p>
<p><strong>{{ isIndividual ? 'Họ và tên:' : 'Tên tổ chức:' }}</strong> {{ selectedCustomer.fullname ||
'-' }}</p>
<p><strong>Số điện thoại:</strong> {{ selectedCustomer.phone || '-' }}</p>
<p><strong>{{ isIndividual ? 'CCCD:' : 'GPKD/Mã số thuế:' }}</strong> {{ selectedCustomer.legal_code ||
'-' }}</p>
<p>
<strong>{{ isIndividual ? "Khách hàng" : "Tổ chức" }}</strong>
</p>
<p><strong> KH:</strong> {{ selectedCustomer.code || "-" }}</p>
<p>
<strong>{{ isIndividual ? "Họ và tên:" : "Tên tổ chức:" }}</strong>
{{ selectedCustomer.fullname || "-" }}
</p>
<p>
<strong>Số điện thoại:</strong>
{{ selectedCustomer.phone || "-" }}
</p>
<p>
<strong>{{ isIndividual ? "CCCD:" : "GPKD/Mã số thuế:" }}</strong>
{{ selectedCustomer.legal_code || "-" }}
</p>
</div>
<!-- Co-owner Column -->
<div class="column" v-if="coOwner && isIndividual">
<div
class="column"
v-if="coOwner && isIndividual"
>
<p class="is-flex is-justify-content-space-between">
<strong>Đồng sở hữu</strong>
<button type="button" class="delete is-small" @click.stop="clearCoOwner()"></button>
<button
type="button"
class="delete is-small"
@click.stop="clearCoOwner()"
></button>
</p>
<p><strong>:</strong> {{ coOwner.people__code || '-' }}</p>
<p><strong>Họ tên:</strong> {{ coOwner.people__fullname || '-' }}</p>
<p><strong>Số điện thoại:</strong> {{ coOwner.people__phone || '-' }}</p>
<p><strong>CCCD:</strong> {{ coOwner.people__legal_code || '-' }}</p>
<p><strong>:</strong> {{ coOwner.people__code || "-" }}</p>
<p>
<strong>Họ tên:</strong>
{{ coOwner.people__fullname || "-" }}
</p>
<p>
<strong>Số điện thoại:</strong>
{{ coOwner.people__phone || "-" }}
</p>
<p><strong>CCCD:</strong> {{ coOwner.people__legal_code || "-" }}</p>
</div>
</div>
<!-- Co-owner SearchBox -->
<div v-if="isIndividual && relatedPeople.length > 0" class="mt-2">
<div
v-if="isIndividual && relatedPeople.length > 0"
class="mt-2"
>
<label class="label">Đồng sở hữu</label>
<SearchBox :key="coOwner ? coOwner.id : 'empty'"
v-bind="{ vdata: relatedPeople, field: 'people__fullname', column: ['people__code', 'people__fullname'], first: true, optionid: coOwner ? coOwner.id : null }"
@option="selectCoOwnerByPerson" />
<SearchBox
:key="coOwner ? coOwner.id : 'empty'"
v-bind="{
vdata: relatedPeople,
field: 'people__fullname',
column: ['people__code', 'people__fullname'],
first: true,
optionid: coOwner ? coOwner.id : null,
}"
@option="selectCoOwnerByPerson"
/>
</div>
<div v-else-if="isIndividual && relatedPeople.length === 0" class="mt-2">
<div
v-else-if="isIndividual && relatedPeople.length === 0"
class="mt-2"
>
<p class="has-text-grey-light is-size-7">Không có người liên quan nào được thêm</p>
</div>
</div>
</div>
<!-- Right Column: Product Info -->
<div class="column is-half" style="border-left: 1px solid #dbdbdb; padding-left: 2rem;">
<div
class="column is-half"
style="border-left: 1px solid #dbdbdb; padding-left: 2rem"
>
<div class="mb-5">
<p class="title is-5 mb-3">Thông tin Sản phẩm</p>
<div class="columns is-multiline is-mobile">
<div class="column is-half"><strong> thương mại:</strong> {{ productData.trade_code || '-' }}</div>
<div class="column is-half"><strong>Số tờ thửa:</strong> {{ productData.land_lot_code || '-' }}</div>
<div class="column is-half"><strong> quy hoạch:</strong> {{ productData.zone_code || '-' }}</div>
<div class="column is-half"><strong>Phân khu:</strong> {{ productData.zone_type__name || '-' }}</div>
<div class="column is-half"><strong>Loại sản phẩm:</strong> {{ productData.type__name || '-' }}</div>
<div class="column is-half"><strong>Hướng cửa:</strong> {{ productData.direction__name || '-' }}</div>
<div class="column is-half"><strong>Kích thước :</strong> {{ productData.land_lot_size || '-' }}</div>
<div class="column is-half"><strong>Diện tích đất:</strong> {{ productData.lot_area ? $numtoString(productData.lot_area)
+ ' m²' : '-' }}</div>
<div class="column is-half"><strong>DT xây dựng:</strong> {{ productData.building_area ?
$numtoString(productData.building_area) + ' m²' : '-' }}</div>
<div class="column is-half"><strong>DT sàn:</strong> {{ productData.total_built_area ?
$numtoString(productData.total_built_area) + ' m²' : '-' }}</div>
<div class="column is-half"><strong>Tầng cao:</strong> {{ productData.number_of_floors || '-' }}</div>
<div class="column is-half"><strong>Trạng thái:</strong> {{ productData.status__name || '-' }}</div>
<div class="column is-half"><strong>Dự án:</strong> {{ productData.project__name || '-' }}</div>
<div class="column is-half">
<strong>Mã thương mại:</strong>
{{ productData.trade_code || "-" }}
</div>
<div class="column is-half">
<strong>Số tờ thửa:</strong>
{{ productData.land_lot_code || "-" }}
</div>
<div class="column is-half">
<strong>Mã quy hoạch:</strong>
{{ productData.zone_code || "-" }}
</div>
<div class="column is-half">
<strong>Phân khu:</strong>
{{ productData.zone_type__name || "-" }}
</div>
<div class="column is-half">
<strong>Loại sản phẩm:</strong>
{{ productData.type__name || "-" }}
</div>
<div class="column is-half">
<strong>Hướng cửa:</strong>
{{ productData.direction__name || "-" }}
</div>
<div class="column is-half">
<strong>Kích thước lô:</strong>
{{ productData.land_lot_size || "-" }}
</div>
<div class="column is-half">
<strong>Diện tích đất:</strong>
{{ productData.lot_area ? $numtoString(productData.lot_area) + " m²" : "-" }}
</div>
<div class="column is-half">
<strong>DT xây dựng:</strong>
{{ productData.building_area ? $numtoString(productData.building_area) + " m²" : "-" }}
</div>
<div class="column is-half">
<strong>DT sàn:</strong>
{{ productData.total_built_area ? $numtoString(productData.total_built_area) + " m²" : "-" }}
</div>
<div class="column is-half">
<strong>Tầng cao:</strong>
{{ productData.number_of_floors || "-" }}
</div>
<div class="column is-half">
<strong>Trạng thái:</strong>
{{ productData.status__name || "-" }}
</div>
<div class="column is-half"><strong>Dự án:</strong> {{ productData.project__name || "-" }}</div>
</div>
</div>
</div>
@@ -89,24 +170,41 @@
<div class="field">
<label class="label">Số tiền thanh toán</label>
<div class="control">
<InputNumber :record="paymentRecord" attr="paymentAmount" placeholder="Nhập số tiền"
:disabled="isPaymentInputDisabled" @number="updatePaymentAmount"
:class="{ 'is-danger': !isPaymentAmountValid }"></InputNumber>
<InputNumber
:record="paymentRecord"
attr="paymentAmount"
placeholder="Nhập số tiền"
:disabled="isPaymentInputDisabled"
@number="updatePaymentAmount"
:class="{ 'is-danger': !isPaymentAmountValid }"
></InputNumber>
</div>
<p v-if="!isPaymentAmountValid" class="help is-danger">{{ paymentAmountError }}</p>
<p
v-if="!isPaymentAmountValid"
class="help is-danger"
>
{{ paymentAmountError }}
</p>
</div>
</div>
<div class="column">
<div class="field">
<label class="label">Ngày ký hợp đồng</label>
<p class="control has-text-weight-bold pt-2">{{ formatDate(initialContractDate) }}</p>
<p class="control has-text-weight-bold pt-2">
{{ formatDate(initialContractDate) }}
</p>
</div>
</div>
<div class="column">
<div class="field">
<label class="label">Ngày hết hạn giao dịch</label>
<Datepicker :record="dateRecord" attr="dueDate" @date="updateDueDate" position="is-top-right" />
<Datepicker
:record="dateRecord"
attr="dueDate"
@date="updateDueDate"
position="is-top-right"
/>
</div>
</div>
@@ -116,18 +214,35 @@
<div class="control">
<div class="field has-addons">
<div class="control is-expanded">
<input class="input" type="text" placeholder="Nhập mã" v-model="userInputCaptcha">
<input
class="input"
type="text"
placeholder="Nhập mã"
v-model="userInputCaptcha"
/>
</div>
<div class="control">
<a class="button is-static has-text-weight-bold has-background-grey-lighter"
style=" font-family: 'Courier New', monospace; letter-spacing: 2px;">
<a
class="button is-static has-text-weight-bold has-background-grey-lighter"
style="font-family: &quot;Courier New&quot;, monospace; letter-spacing: 2px"
>
{{ captchaCode }}
</a>
</div>
<div class="control">
<button class="button" @click="generateCaptcha" :title="'Tạo mã mới'">
<button
class="button"
@click="generateCaptcha"
:title="'Tạo mã mới'"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'refresh.svg', type: 'primary', size: 23 }"></SvgIcon>
<SvgIcon
v-bind="{
name: 'refresh.svg',
type: 'primary',
size: 23,
}"
></SvgIcon>
</span>
</button>
</div>
@@ -140,22 +255,30 @@
</div>
<div class="is-flex is-justify-content-flex-end mt-4">
<button class="button has-text-white is-success mr-2" :disabled="!isConfirmed" @click="handleConfirm">
<button
class="button has-text-white is-success mr-2"
:disabled="!isConfirmed"
@click="handleConfirm"
>
<span>Tạo giao dịch</span>
</button>
<button class="button" @click="$emit('close')">Hủy</button>
<button
class="button"
@click="$emit('close')"
>
Hủy
</button>
</div>
</div>
</template>
<script>
import { useStore } from "@/stores/index";
import dayjs from 'dayjs';
import Datepicker from '~/components/datepicker/Datepicker.vue';
import InputNumber from '~/components/common/InputNumber.vue';
import dayjs from "dayjs";
import Datepicker from "~/components/datepicker/Datepicker.vue";
import InputNumber from "~/components/common/InputNumber.vue";
import SearchBox from "~/components/SearchBox";
export default {
components: { Datepicker, InputNumber, SearchBox },
props: {
@@ -171,31 +294,36 @@ export default {
const store = useStore();
return { store };
},
emits: ['close', 'modalevent'],
emits: ["close", "modalevent"],
data() {
const initialDueDate = dayjs(this.initialContractDate).add(0, 'day').format('YYYY-MM-DD');
const initialDueDate = dayjs(this.initialContractDate).add(0, "day").format("YYYY-MM-DD");
return {
editableDueDate: initialDueDate,
dateRecord: { dueDate: initialDueDate },
captchaCode: '',
userInputCaptcha: '',
captchaCode: "",
userInputCaptcha: "",
paymentRecord: { paymentAmount: null },
coOwner: null,
relatedPeople: [],
peopleAddon: { component: "people/People", width: "65%", height: "600px", title: "Người liên quan" }
peopleAddon: {
component: "people/People",
width: "65%",
height: "600px",
title: "Người liên quan",
},
};
},
watch: {
depositAmount: {
immediate: true,
handler(newVal) {
if (this.phaseInfo.id === 'p0' && newVal === 0) {
if (this.phaseInfo.id === "p0" && newVal === 0) {
this.paymentRecord.paymentAmount = null;
} else {
this.paymentRecord.paymentAmount = newVal;
}
}
},
},
selectedCustomer: {
immediate: true,
@@ -206,8 +334,8 @@ export default {
this.relatedPeople = [];
this.coOwner = null;
}
}
}
},
},
},
computed: {
isIndividual() {
@@ -217,17 +345,21 @@ export default {
return this.paymentRecord.paymentAmount;
},
isPaymentInputDisabled() {
return this.phaseInfo.id !== 'p0';
return this.phaseInfo.id !== "p0";
},
isPaymentAmountValid() {
return this.paymentAmount > 0;
},
paymentAmountError() {
if (this.isPaymentAmountValid) return '';
return 'Số tiền thanh toán phải lớn hơn 0.';
if (this.isPaymentAmountValid) return "";
return "Số tiền thanh toán phải lớn hơn 0.";
},
isConfirmed() {
return this.userInputCaptcha.toLowerCase() === this.captchaCode.toLowerCase() && this.userInputCaptcha !== '' && this.isPaymentAmountValid;
return (
this.userInputCaptcha.toLowerCase() === this.captchaCode.toLowerCase() &&
this.userInputCaptcha !== "" &&
this.isPaymentAmountValid
);
},
depositAmount() {
return this.selectedPolicy?.deposit || 0;
@@ -240,14 +372,14 @@ export default {
async fetchRelatedPeople() {
try {
const { $getdata } = useNuxtApp();
const apiName = this.isIndividual ? 'customerpeople' : 'legalrep';
const filterKey = this.isIndividual ? 'customer' : 'organization';
const apiName = this.isIndividual ? "customerpeople" : "legalrep";
const filterKey = this.isIndividual ? "customer" : "organization";
let customerId = this.selectedCustomer.id;
// Nếu organization, cần lấy organization ID
if (!this.isIndividual) {
const org = await $getdata('organization', { customer: customerId }, undefined, true);
const org = await $getdata("organization", { customer: customerId }, undefined, true);
if (org) {
customerId = org.id;
} else {
@@ -258,7 +390,7 @@ export default {
}
const people = await $getdata(apiName, { [filterKey]: customerId });
this.relatedPeople = Array.isArray(people) ? people : (people ? [people] : []);
this.relatedPeople = Array.isArray(people) ? people : people ? [people] : [];
// Auto-select người đầu tiên nếu
if (this.relatedPeople.length > 0) {
@@ -267,7 +399,7 @@ export default {
this.coOwner = null;
}
} catch (error) {
console.error('Error fetching related people:', error);
console.error("Error fetching related people:", error);
this.relatedPeople = [];
this.coOwner = null;
}
@@ -280,7 +412,7 @@ export default {
this.coOwner = selectedPerson || null;
},
formatDate(date) {
return date ? dayjs(date).format('DD/MM/YYYY') : '-';
return date ? dayjs(date).format("DD/MM/YYYY") : "-";
},
updateDueDate(newDate) {
this.editableDueDate = newDate;
@@ -291,22 +423,22 @@ export default {
},
generateCaptcha() {
this.captchaCode = Math.random().toString(36).substring(2, 7).toUpperCase();
this.userInputCaptcha = '';
this.userInputCaptcha = "";
},
handleConfirm() {
if (this.isConfirmed) {
this.$emit('modalevent', {
name: 'confirm',
this.$emit("modalevent", {
name: "confirm",
data: {
currentDate: this.initialContractDate,
dueDate: this.editableDueDate,
paymentAmount: this.depositAmount,
depositReceived: this.paymentAmount,
people: this.isIndividual && this.coOwner ? this.coOwner.people : null,
}
},
});
}
},
}
},
};
</script>
</script>

View File

@@ -1,5 +1,5 @@
<script setup>
import { isNotNil } from 'es-toolkit';
import { isNotNil } from "es-toolkit";
// receive either txndetail or txndetailCode
// pass index to show as timeline item
@@ -21,38 +21,45 @@ const isUploadingFile = ref(false);
onMounted(async () => {
try {
if (!props.txndetail) {
const txndetailData = await $getdata('reservation', undefined, {
filter: { code: props.txndetailCode },
values: 'id,code,date,amount,amount_remaining,amount_received,due_date,transaction,customer_old__fullname,customer_new__fullname,transaction__code,phase,phase__name,creator,creator__fullname,status,status__name,approver,approver__fullname,approve_time,create_time,update_time'
}, true);
const txndetailData = await $getdata(
"reservation",
undefined,
{
filter: { code: props.txndetailCode },
values:
"id,code,date,amount,amount_remaining,amount_received,due_date,transaction,customer_old__fullname,customer_new__fullname,transaction__code,phase,phase__name,creator,creator__fullname,status,status__name,approver,approver__fullname,approve_time,create_time,update_time",
},
true,
);
txndetail.value = txndetailData;
}
const schedules = await $getdata('payment_schedule', undefined, {
const schedules = await $getdata("payment_schedule", undefined, {
filter: { txn_detail: txndetail.value.id },
values: 'id,code,txn_detail,txn_detail__transaction__policy__code,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
values:
"id,code,txn_detail,txn_detail__transaction__policy__code,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry",
});
paymentSchedules.value = schedules;
// Fetch transaction info
if (txndetail.value.transaction) {
transaction.value = await $getdata('transaction', { id: txndetail.value.transaction }, undefined, true);
transaction.value = await $getdata("transaction", { id: txndetail.value.transaction }, undefined, true);
}
} catch (error) {
console.error('Error fetching data:', error);
console.error("Error fetching data:", error);
paymentSchedules.value = [];
}
});
function openModal() {
showModal.value = {
title: 'Hợp đồng',
height: '40vh',
width: '50%',
component: 'transaction/TransactionFiles',
vbind: { txndetail: txndetail.value.id }
}
title: "Hợp đồng",
height: "40vh",
width: "50%",
component: "transaction/TransactionFiles",
vbind: { txndetail: txndetail.value.id },
};
}
async function handleFileUpload(uploadedFiles) {
@@ -73,96 +80,148 @@ async function handleFileUpload(uploadedFiles) {
}
}
$snackbar('File đã được upload thành công!', { type: 'is-success' });
$snackbar("File đã được upload thành công!", { type: "is-success" });
} catch (error) {
console.error('Error uploading file:', error);
$snackbar(error.message || 'Có lỗi khi upload file.', { type: 'is-danger' });
console.error("Error uploading file:", error);
$snackbar(error.message || "Có lỗi khi upload file.", {
type: "is-danger",
});
} finally {
isUploadingFile.value = false;
}
}
</script>
<template>
<div v-if="txndetail" class="is-flex is-gap-2">
<div v-if="props.index !== undefined" class="is-flex is-flex-direction-column is-align-items-center is-gap-1">
<p class="is-size-5 has-text-weight-semibold is-flex is-justify-content-center is-align-items-center" style="
border: 4px solid rgb(32, 72, 83);
border-radius: 9999px;
width: 2.5rem;
height: 2.5rem;
">
<div
v-if="txndetail"
class="is-flex is-gap-2"
>
<div
v-if="props.index !== undefined"
class="is-flex is-flex-direction-column is-align-items-center is-gap-1"
>
<p
class="is-size-5 has-text-weight-semibold is-flex is-justify-content-center is-align-items-center"
style="border: 4px solid rgb(32, 72, 83); border-radius: 9999px; width: 2.5rem; height: 2.5rem"
>
{{ props.index + 1 }}
</p>
<div style="
border: 3px solid #dddddd;
border-radius: 9999px;
flex-grow: 1;
width: min-content;
">
</div>
<div style="border: 3px solid #dddddd; border-radius: 9999px; flex-grow: 1; width: min-content"></div>
</div>
<div class="is-flex is-flex-direction-column is-align-items-baseline is-gap-2 is-flex-grow-1">
<div class="columns is-flex-wrap-wrap is-1.5 fs-15" style="width: 100%">
<div
class="columns is-flex-wrap-wrap is-1.5 fs-15"
style="width: 100%"
>
<div class="column is-12 my-0 columns">
<p class="column is-3">
<span>{{ txndetail.phase__name }} - </span>
<span class="has-text-weight-semibold">{{ txndetail.code }}</span>
</p>
<div v-if="phase != 7" class="column is-3 p-0">
<div
v-if="phase != 7"
class="column is-3 p-0"
>
<p>
<span>Số tiền: </span>
<span class="has-text-weight-semibold">{{ $numtoString(txndetail.amount) }}</span>
</p>
<p class="fs-14 has-text-grey" v-if="isNotNil(txndetail.amount_received) && isNotNil(txndetail.amount_remaining)">
<p
class="fs-14 has-text-grey"
v-if="isNotNil(txndetail.amount_received) && isNotNil(txndetail.amount_remaining)"
>
<span>(</span>
<span title="Đã thu" class="has-text-weight-semibold" style="color: hsl(120, 70%, 40%)">
<span
title="Đã thu"
class="has-text-weight-semibold"
style="color: hsl(120, 70%, 40%)"
>
{{ $numtoString(txndetail.amount_received) }}
</span>
<span> </span>
<span title="Còn lại" class="has-text-weight-semibold" style="color: hsl(0, 50%, 50%)">
<span
title="Còn lại"
class="has-text-weight-semibold"
style="color: hsl(0, 50%, 50%)"
>
{{ $numtoString(txndetail.amount_remaining) }}
</span>
<span>)</span>
</p>
</div>
<p v-if="phase != 7" class="column is-3">
<p
v-if="phase != 7"
class="column is-3"
>
<span>Trạng thái: </span>
<span class="has-text-weight-semibold">{{ txndetail.status__name }}</span>
</p>
<p v-if="phase === 7" class="column is-3">
<p
v-if="phase === 7"
class="column is-3"
>
<span>Khách hàng mới: </span>
<span class="has-text-weight-semibold">{{ txndetail.customer_new__fullname }}</span>
</p>
<p v-if="phase === 7" class="column is-3">
<p
v-if="phase === 7"
class="column is-3"
>
<span>Người chuyển nhượng: </span>
<span class="has-text-weight-semibold">{{ txndetail.creator__fullname }}</span>
</p>
<div class="column is-3 p-0 is-flex is-align-items-center is-gap-3">
<a class="fsb-15 has-text-primary" @click.prevent="openModal">Hợp đồng</a>
<a
class="fsb-15 has-text-primary"
@click.prevent="openModal"
>Hợp đồng</a
>
<!-- <span v-if="!store.dealer" class="mx-1 has-text-grey-light"></span> -->
<FileUpload
v-if="txndetail.phase === 7 && !store.dealer && $getEditRights('edit', { code: 'transaction', category: 'topmenu' })"
<FileUpload
v-if="
txndetail.phase === 7 &&
!store.dealer &&
$getEditRights('edit', {
code: 'transaction',
category: 'topmenu',
})
"
:type="['image', 'pdf']"
@files="handleFileUpload"
position="right"
@files="handleFileUpload"
position="right"
class="file-upload-inline"
style="display: inline-block;"
style="display: inline-block"
/>
</div>
</div>
<hr class="column is-12 my-0 has-background-grey" />
<p v-if="phase === 7" class="column is-12 mt-1 ml-1 columns">
<span>Khách hàng <span class="has-text-weight-semibold">{{txndetail.customer_old__fullname}} </span> chuyển nhượng hợp đồng này cho khách hàng <span class="has-text-weight-semibold">{{txndetail.customer_new__fullname}} </span></span>
</p>
<div v-if="phase != 7" class="column is-12 mt-0 columns">
<p
v-if="phase === 7"
class="column is-12 mt-1 ml-1 columns"
>
<span
>Khách hàng
<span class="has-text-weight-semibold">{{ txndetail.customer_old__fullname }} </span>
chuyển nhượng hợp đồng này cho khách hàng
<span class="has-text-weight-semibold">{{ txndetail.customer_new__fullname }} </span></span
>
</p>
<div
v-if="phase != 7"
class="column is-12 mt-0 columns"
>
<p class="column is-3">
<span>Từ ngày: </span>
<span class="has-text-weight-semibold">{{ txndetail.date ? $dayjs(txndetail.date).format('DD/MM/YYYY') : '' }}</span>
<span class="has-text-weight-semibold">{{
txndetail.date ? $dayjs(txndetail.date).format("DD/MM/YYYY") : ""
}}</span>
</p>
<p class="column is-3">
<span>Đến ngày: </span>
<span class="has-text-weight-semibold">{{ txndetail.due_date ? $dayjs(txndetail.due_date).format('DD/MM/YYYY') : '' }}</span>
<span class="has-text-weight-semibold">{{
txndetail.due_date ? $dayjs(txndetail.due_date).format("DD/MM/YYYY") : ""
}}</span>
</p>
<p class="column is-3">
<span>Người tạo: </span>
@@ -175,21 +234,29 @@ async function handleFileUpload(uploadedFiles) {
</div>
</div>
<div style="width: 100%">
<DataView v-if="paymentSchedules && paymentSchedules.length > 0" v-bind="{
pagename: `txndetail-${txndetail.id}`,
api: 'payment_schedule',
params: {
filter: { txn_detail: txndetail.id },
sort: 'cycle',
values: 'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,remain_amount,paid_amount,remain_amount,code,txn_detail,txn_detail__transaction__policy__code,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
},
setting: 'payment_schedule_list_timeline',
}" />
<DataView
v-if="paymentSchedules && paymentSchedules.length > 0"
v-bind="{
pagename: `txndetail-${txndetail.id}`,
api: 'payment_schedule',
params: {
filter: { txn_detail: txndetail.id },
sort: 'cycle',
values:
'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,remain_amount,paid_amount,remain_amount,code,txn_detail,txn_detail__transaction__policy__code,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
},
setting: 'payment_schedule_list_timeline',
}"
/>
</div>
</div>
</div>
<div v-else>Đã có lỗi khi lấy thông tin chi tiết giao dịch.</div>
<Modal v-if="showModal" v-bind="showModal" @close="showModal = undefined" />
<Modal
v-if="showModal"
v-bind="showModal"
@close="showModal = undefined"
/>
</template>
<style scoped>
@@ -211,4 +278,4 @@ async function handleFileUpload(uploadedFiles) {
background: none;
color: #1a5490;
}
</style>
</style>

View File

@@ -4,11 +4,14 @@
<thead>
<tr>
<th>File</th>
<th style="text-align: center;">Tải xuống</th>
<th style="text-align: center">Tải xuống</th>
</tr>
</thead>
<tbody>
<tr v-for="txnfile in txnfiles" :key="txnfile.id">
<tr
v-for="txnfile in txnfiles"
:key="txnfile.id"
>
<td>
<div class="is-flex is-align-items-center">
<span class="icon is-medium has-text-primary mr-2">
@@ -20,7 +23,10 @@
}"
></SvgIcon>
</span>
<div class="is-clickable" @click="open(txnfile)">
<div
class="is-clickable"
@click="open(txnfile)"
>
<p class="has-text-weight-semibold has-text-primary">
{{ txnfile.file__name }}
</p>
@@ -51,25 +57,30 @@
</td>
</tr>
<tr v-if="txnfiles.length === 0">
<td colspan="4" class="has-text-centered py-4">
<p class="has-text-grey">
Chưa có tài liệu nào được đính kèm.
</p>
<td
colspan="4"
class="has-text-centered py-4"
>
<p class="has-text-grey">Chưa có tài liệu nào được đính kèm.</p>
</td>
</tr>
</tbody>
</table>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ref } from "vue";
const { $formatFileSize, $getdata, $getpath } = useNuxtApp();
const props = defineProps({
txndetail: [String, Number]
txndetail: [String, Number],
});
const showmodal = ref(undefined);
@@ -77,22 +88,28 @@ const txnfiles = ref([]);
async function fetchTxnFiles() {
if (props.txndetail) {
const data = await $getdata('transactionfile', { txn_detail: props.txndetail });
txnfiles.value = Array.isArray(data) ? data : (data ? [data] : []);
const data = await $getdata("transactionfile", {
txn_detail: props.txndetail,
});
txnfiles.value = Array.isArray(data) ? data : data ? [data] : [];
}
}
function open(v) {
const fileName = v.file__name || '';
const fileName = v.file__name || "";
const filePath = v.file__file || v.file;
if (!filePath) return;
if(fileName.indexOf('.png')>=0 || fileName.indexOf('.jpg')>=0 || fileName.indexOf('.jpeg')>=0) {
if (fileName.indexOf(".png") >= 0 || fileName.indexOf(".jpg") >= 0 || fileName.indexOf(".jpeg") >= 0) {
showmodal.value = {
title: fileName,
component: 'media/ChipImage',
vbind: {extend: false, file: v, image: `${$getpath()}static/files/${filePath}`}
title: fileName,
component: "media/ChipImage",
vbind: {
extend: false,
file: v,
image: `${$getpath()}static/files/${filePath}`,
},
};
return;
}
@@ -102,14 +119,17 @@ function open(v) {
function download(v) {
const filePath = v.file__file || v.file;
if (!filePath) return;
window.open(`${$getpath()}static/files/${filePath}`, v.file__name || '_blank');
window.open(`${$getpath()}static/files/${filePath}`, v.file__name || "_blank");
}
fetchTxnFiles();
watch(() => props.txndetail, () => {
fetchTxnFiles();
}, { immediate: true });
watch(
() => props.txndetail,
() => {
fetchTxnFiles();
},
{ immediate: true },
);
</script>

View File

@@ -1,43 +1,44 @@
<script setup>
import TransactionDetail from '@/components/transaction/TransactionDetail.vue';
import TransactionDetail from "@/components/transaction/TransactionDetail.vue";
const props = defineProps({
transaction__code: String,
});
const { $exportpdf, $getdata, $snackbar, $store } = useNuxtApp();
const { dealer} = $store;
const { dealer } = $store;
const txn = ref(null);
const txndetails = ref(null);
const showModal = ref(null);
function openChangeCustomerModal() {
showModal.value = {
title: 'Đổi khách hàng',
height: '40vh',
width: '50%',
component: 'transaction/ChangeCustomerModal',
vbind: { transactionId: txn.value.id }
}
title: "Đổi khách hàng",
height: "40vh",
width: "50%",
component: "transaction/ChangeCustomerModal",
vbind: { transactionId: txn.value.id },
};
}
function print() {
const fileName = `Giao-dich-${props.transaction__code}`;
const docId = 'print-area';
$exportpdf(docId, fileName, 'a4', 'landscape');
$snackbar('Đang xuất PDF...', { type: 'is-info' });
const docId = "print-area";
$exportpdf(docId, fileName, "a4", "landscape");
$snackbar("Đang xuất PDF...", { type: "is-info" });
}
onMounted(async () => {
const fetchedTxn = await $getdata('transaction', { code: props.transaction__code }, undefined, true);
const fetchedTxn = await $getdata("transaction", { code: props.transaction__code }, undefined, true);
txn.value = fetchedTxn;
const fetchedTxndetails = await $getdata('reservation', undefined, {
const fetchedTxndetails = await $getdata("reservation", undefined, {
filter: {
transaction__code: props.transaction__code
transaction__code: props.transaction__code,
},
sort: 'create_time',
values: 'id,code,date,amount,amount_remaining,amount_received,due_date,transaction,customer_old__fullname,customer_new__fullname,transaction__code,phase,phase__name,creator,creator__fullname,status,status__name,approver,approver__fullname,approve_time,create_time,update_time'
sort: "create_time",
values:
"id,code,date,amount,amount_remaining,amount_received,due_date,transaction,customer_old__fullname,customer_new__fullname,transaction__code,phase,phase__name,creator,creator__fullname,status,status__name,approver,approver__fullname,approve_time,create_time,update_time",
});
txndetails.value = fetchedTxndetails;
});
@@ -77,15 +78,21 @@ onMounted(async () => {
</span>
</p>
</div>
<div id="ignore" class="column is-narrow is-flex is-align-items-center is-gap-2">
<button
<div
id="ignore"
class="column is-narrow is-flex is-align-items-center is-gap-2"
>
<button
v-if="txn?.phase === 4 && !dealer && $getEditRights('edit', { code: 'transaction', category: 'topmenu' })"
@click="openChangeCustomerModal"
@click="openChangeCustomerModal"
class="button is-link"
>
Đổi khách hàng
</button>
<button @click="print" class="button is-light">
<button
@click="print"
class="button is-light"
>
In thông tin
</button>
</div>
@@ -99,9 +106,9 @@ onMounted(async () => {
/>
</div>
</div>
<Modal
v-if="showModal"
v-bind="showModal"
@close="showModal = undefined"
<Modal
v-if="showModal"
v-bind="showModal"
@close="showModal = undefined"
/>
</template>
</template>

View File

@@ -1,41 +1,59 @@
<template>
<div>
<div class="mb-5">
<label
v-for="item in filteredPhases"
<label
v-for="item in filteredPhases"
:key="item.id"
class="py-3 is-clickable is-block"
:class="{ 'has-background-success-light': selectedPhaseId === item.id }"
style="border-bottom: 1px solid #dbdbdb;"
style="border-bottom: 1px solid #dbdbdb"
>
<div class="columns px-3 is-mobile is-vcentered is-gapless mb-0">
<div class="column">
<span class="is-size-6">{{ item.name }}</span>
</div>
<div class="column is-narrow">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" :stroke="selectedPhaseId === item.id ? '#48c78e' : '#dbdbdb'" stroke-width="2"/>
<circle v-if="selectedPhaseId === item.id" cx="12" cy="12" r="6" fill="#48c78e"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
:stroke="selectedPhaseId === item.id ? '#48c78e' : '#dbdbdb'"
stroke-width="2"
/>
<circle
v-if="selectedPhaseId === item.id"
cx="12"
cy="12"
r="6"
fill="#48c78e"
/>
</svg>
<input
type="radio"
:value="item.id"
<input
type="radio"
:value="item.id"
v-model="selectedPhaseId"
style="display: none;"
style="display: none"
/>
</div>
</div>
</label>
</div>
<div class="field">
<div class="control">
<button
class="button is-success has-text-white is-fullwidth"
@click="submit"
<button
class="button is-success has-text-white is-fullwidth"
@click="submit"
:disabled="!selectedPhaseId"
>
{{ isVietnamese ? 'Xác nhận' : 'Confirm' }}
{{ isVietnamese ? "Xác nhận" : "Confirm" }}
</button>
</div>
</div>
@@ -49,43 +67,71 @@ export default {
const store = useStore();
return { store };
},
emits: ['modalevent', 'close'],
emits: ["modalevent", "close"],
props: {
filterPhases: {
type: Array,
default: () => []
}
default: () => [],
},
},
data() {
return {
selectedPhaseId: null,
phases: [
{ id: 'p1', name: 'Giữ chỗ', code: 'reserved', phase: 1, status: 3,transstatus :1 },
{ id: 'p2a', name: 'Thỏa thuận thực hiện nguyện vọng', code: 'fulfillwish', phase: 4, status: 5,transstatus :1 },
{ id: 'p2b', name: 'Đặt cọc', code: 'deposit', phase: 2, status: 4,transstatus :1 },
{ id: 'p3', name: 'Mua bán', code: 'pertrade', phase: 3, status: 6,transstatus :1 }
]
{
id: "p1",
name: "Giữ chỗ",
code: "reserved",
phase: 1,
status: 3,
transstatus: 1,
},
{
id: "p2a",
name: "Thỏa thuận thực hiện nguyện vọng",
code: "fulfillwish",
phase: 4,
status: 5,
transstatus: 1,
},
{
id: "p2b",
name: "Đặt cọc",
code: "deposit",
phase: 2,
status: 4,
transstatus: 1,
},
{
id: "p3",
name: "Mua bán",
code: "pertrade",
phase: 3,
status: 6,
transstatus: 1,
},
],
};
},
computed: {
isVietnamese() {
return this.store.lang === 'vi';
return this.store.lang === "vi";
},
filteredPhases() {
if (this.filterPhases.length === 0) {
return this.phases; // If no filter is provided, show all phases
}
return this.phases.filter(phase => this.filterPhases.includes(phase.code));
}
return this.phases.filter((phase) => this.filterPhases.includes(phase.code));
},
},
methods: {
submit() {
if (this.selectedPhaseId) {
const selected = this.phases.find(p => p.id === this.selectedPhaseId);
this.$emit('modalevent', { name: 'phaseSelected', data: selected });
this.$emit('close');
const selected = this.phases.find((p) => p.id === this.selectedPhaseId);
this.$emit("modalevent", { name: "phaseSelected", data: selected });
this.$emit("close");
}
}
}
},
},
};
</script>
</script>

View File

@@ -1,12 +1,19 @@
<template>
<div :id="docid" v-if="record">
<div
:id="docid"
v-if="record"
>
<div :id="docid1">
<div class="columns is-multiline mx-0">
<div class="column is-3 pb-1 px-0">
<div class="field">
<label class="label">{{ this.data && findFieldName("id")[this.lang] }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(record.id)">{{ record.id }}</span>
<span
class="hyperlink"
@click="$copyToClipboard(record.id)"
>{{ record.id }}</span
>
</div>
</div>
</div>
@@ -14,13 +21,17 @@
<div class="field">
<label class="label">{{ this.data && findFieldName("code")[this.lang] }}</label>
<div class="control">
<span class="hyperlink" @click="$copyToClipboard(record.code)">{{ record.code }}</span>
<span
class="hyperlink"
@click="$copyToClipboard(record.code)"
>{{ record.code }}</span
>
</div>
</div>
</div>
<div class="column is-3 pb-1 px-0">
<div class="field">
<label class="label">{{ this.lang === 'vi' ? 'Tên' : 'Name' }}</label>
<label class="label">{{ this.lang === "vi" ? "Tên" : "Name" }}</label>
<div class="control">
{{ record.name }}
</div>
@@ -28,7 +39,7 @@
</div>
<div class="column is-3 pb-1 px-0">
<div class="field">
<label class="label">{{ this.lang === 'vi' ? 'Chi tiết' : 'Detail' }}</label>
<label class="label">{{ this.lang === "vi" ? "Chi tiết" : "Detail" }}</label>
<div class="control">
<span>{{ record.detail }}</span>
</div>

File diff suppressed because it is too large Load Diff