changes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<div :id="docid">
|
||||
<div :id="docid1">
|
||||
<Caption
|
||||
v-bind="{
|
||||
title: isVietnamese ? 'Thanh toán' : 'Payment',
|
||||
type: 'has-text-warning',
|
||||
}"
|
||||
></Caption>
|
||||
<div class="columns is-multiline mx-0">
|
||||
<div class="column is-4 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("loan_code")[lang] }}</label>
|
||||
<div class="control">
|
||||
<span
|
||||
class="hyperlink"
|
||||
@click="$copyToClipboard(record.code)"
|
||||
>{{ record?.code || "/" }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("name")[lang] }}</label>
|
||||
<div class="control">
|
||||
<span>{{ record?.fullname || "/" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("phone_number")[lang] }}</label>
|
||||
<div class="control">
|
||||
<span>{{ record?.phone || "/" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("modalcollaboratorcode")[lang] }}</label>
|
||||
<div class="control">
|
||||
<span>{{ record?.collaborator__code || "/" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ isVietnamese ? "Họ tên CTV" : "CTV name" }}</label>
|
||||
<div class="control">
|
||||
<span>{{ record?.collaborator__fullname || "/" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ dataLang && findFieldName("commissionamount")[lang] }}</label>
|
||||
<div class="control">
|
||||
<span>{{ record?.commission ? $numtoString(record.commission) : "/" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-5 pb-1 px-0">
|
||||
<div class="field">
|
||||
<label class="label">{{ isVietnamese ? " Trạng thái" : "Status" }}</label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
true
|
||||
v-bind="{
|
||||
api: 'paymentstatus',
|
||||
field: isVietnamese ? 'name' : 'en',
|
||||
column: ['code'],
|
||||
first: true,
|
||||
optionid: record.payment_status ? record.payment_status : 1,
|
||||
}"
|
||||
@option="selected('payment_status', $event)"
|
||||
position="is-top-left"
|
||||
></SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-2 border-bottom"></div> -->
|
||||
<div
|
||||
class="buttons mt-5"
|
||||
id="ignore"
|
||||
>
|
||||
<button
|
||||
class="button is-primary has-text-white mt-2"
|
||||
@click="handleUpdate()"
|
||||
>
|
||||
{{ dataLang && findFieldName("update")[lang] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useStore } from "@/stores/index";
|
||||
import { useNuxtApp } from "#app";
|
||||
const nuxtApp = useNuxtApp();
|
||||
|
||||
const {
|
||||
$updatepage,
|
||||
$getdata,
|
||||
$updateapi,
|
||||
$insertapi,
|
||||
$copyToClipboard,
|
||||
$empty,
|
||||
$snackbar,
|
||||
$numtoString,
|
||||
$formatNumber,
|
||||
} = nuxtApp;
|
||||
const store = useStore();
|
||||
const lang = computed(() => store.lang);
|
||||
const isVietnamese = computed(() => lang.value === "vi");
|
||||
const dataLang = ref(store.common);
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
api: String,
|
||||
pagename: String,
|
||||
});
|
||||
const record = ref(props.row);
|
||||
const findFieldName = (code) => {
|
||||
let field = dataLang.value.find((v) => v.code === code);
|
||||
return field;
|
||||
};
|
||||
|
||||
const selected = (fieldName, value) => {
|
||||
if (value) {
|
||||
record.value.payment_status = value.id;
|
||||
record.value.payment_status__code = value.code;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
await $updateapi(props.api, record.value);
|
||||
let ele = await $getdata(props.api, { id: record.value.id }, undefined, true);
|
||||
$updatepage(props.pagename, ele);
|
||||
$snackbar(isVietnamese.value ? "Cập nhật thành công" : "Update successful");
|
||||
emit("close");
|
||||
} catch (error) {
|
||||
console.error("Error updating data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
record.value = await $getdata(props.api, { id: record.value.id }, undefined, true);
|
||||
});
|
||||
</script>
|
||||
@@ -1,286 +0,0 @@
|
||||
<template>
|
||||
<div :id="docid">
|
||||
<!-- Loading state -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="has-text-centered mt-5 mb-5"
|
||||
style="min-height: 500px"
|
||||
>
|
||||
<button class="button is-primary is-loading is-large"></button>
|
||||
<p class="mt-4 has-text-primary has-text-weight-semibold">
|
||||
{{ isVietnamese ? "Đang tải hợp đồng..." : "Loading contracts..." }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- No contract state -->
|
||||
<div
|
||||
v-else-if="!hasContracts"
|
||||
class="has-text-centered mt-5 mb-5"
|
||||
style="min-height: 500px"
|
||||
>
|
||||
<article class="message is-primary">
|
||||
<div
|
||||
class="message-body"
|
||||
style="font-size: 17px; text-align: left; color: black"
|
||||
>
|
||||
{{
|
||||
isVietnamese
|
||||
? "Chưa có hợp đồng. Vui lòng tạo giao dịch và hợp đồng trước."
|
||||
: "No contract available. Please create transaction and contract first."
|
||||
}}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Contracts list -->
|
||||
<template v-else>
|
||||
<!-- Tabs khi có nhiều hợp đồng -->
|
||||
<div
|
||||
class="tabs border-bottom"
|
||||
id="ignore"
|
||||
v-if="contractsList.length > 1"
|
||||
>
|
||||
<ul class="tabs-list">
|
||||
<li
|
||||
class="tabs-item"
|
||||
style="border: none"
|
||||
v-for="(contract, index) in contractsList"
|
||||
:key="index"
|
||||
:class="{
|
||||
'bg-primary has-text-white': activeContractIndex === index,
|
||||
}"
|
||||
@click="switchContract(index)"
|
||||
>
|
||||
<a class="tabs-link">
|
||||
<span>{{ contract.document[0]?.name || contract.document[0]?.en || `Contract ${index + 1}` }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contract content -->
|
||||
<div v-if="currentContract && pdfFileUrl && hasValidDocument">
|
||||
<div class="contract-content mt-2">
|
||||
<iframe
|
||||
:src="`https://mozilla.github.io/pdf.js/web/viewer.html?file=${pdfFileUrl}`"
|
||||
width="100%"
|
||||
height="90vh"
|
||||
scrolling="no"
|
||||
style="border: none; height: 75vh; top: 0; left: 0; right: 0; bottom: 0"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download buttons -->
|
||||
<div
|
||||
class="mt-4"
|
||||
id="ignore"
|
||||
>
|
||||
<button
|
||||
v-if="hasValidDocument"
|
||||
class="button is-primary has-text-white mr-4"
|
||||
@click="downloadDocx"
|
||||
>
|
||||
{{ isVietnamese ? "Tải file docx" : "Download contract as docx" }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="hasValidDocument"
|
||||
class="button is-primary has-text-white mr-4"
|
||||
@click="downloadPdf"
|
||||
>
|
||||
{{ isVietnamese ? "Tải file pdf" : "Download contract as pdf" }}
|
||||
</button>
|
||||
|
||||
<p
|
||||
v-if="contractError"
|
||||
class="has-text-danger mt-2"
|
||||
>
|
||||
{{ contractError }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useStore } from "@/stores/index";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const store = useStore();
|
||||
return { store };
|
||||
},
|
||||
props: {
|
||||
contractId: {
|
||||
type: [Number, String],
|
||||
default: null,
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
directDocument: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["contractCreated", "update", "close", "dataevent"],
|
||||
data() {
|
||||
return {
|
||||
docid: this.$id(),
|
||||
contractsList: [],
|
||||
activeContractIndex: 0,
|
||||
isLoading: false,
|
||||
contractError: null,
|
||||
lang: this.store.lang,
|
||||
isVietnamese: this.store.lang === "vi",
|
||||
link: this.$getpath().indexOf("dev") >= 0 ? "dev.utopia.y99.vn" : "utopia.y99.vn",
|
||||
pdfFileUrl: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasContracts() {
|
||||
return this.contractsList && this.contractsList.length > 0;
|
||||
},
|
||||
currentContract() {
|
||||
return this.hasContracts ? this.contractsList[this.activeContractIndex] : null;
|
||||
},
|
||||
hasValidDocument() {
|
||||
if (!this.currentContract) return false;
|
||||
return (
|
||||
this.currentContract.document &&
|
||||
this.currentContract.document.length > 0 &&
|
||||
this.currentContract.document[0]?.pdf
|
||||
);
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.contractError = null;
|
||||
|
||||
if (this.directDocument) {
|
||||
this.contractsList = [{ document: [this.directDocument] }];
|
||||
this.updatePdfUrl(0);
|
||||
return;
|
||||
}
|
||||
|
||||
let contracts = [];
|
||||
let fetchParams = null;
|
||||
|
||||
if (this.contractId) {
|
||||
fetchParams = { id: this.contractId };
|
||||
} else if (this.row?.id) {
|
||||
fetchParams = { transaction: this.row.id };
|
||||
}
|
||||
|
||||
if (!fetchParams) {
|
||||
throw new Error(
|
||||
this.isVietnamese
|
||||
? "Không có ID hợp đồng hoặc transaction để tải."
|
||||
: "No contract ID or transaction provided to load.",
|
||||
);
|
||||
}
|
||||
|
||||
contracts = await this.$getdata("contract", fetchParams, undefined);
|
||||
|
||||
if (!contracts || contracts.length === 0) {
|
||||
throw new Error(this.isVietnamese ? "Không tìm thấy hợp đồng." : "Contract not found.");
|
||||
}
|
||||
|
||||
this.contractsList = contracts;
|
||||
console.log(this.contractsList);
|
||||
|
||||
if (this.hasContracts) {
|
||||
this.updatePdfUrl(this.activeContractIndex);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading contracts:", error);
|
||||
this.contractError =
|
||||
error.message || (this.isVietnamese ? "Lỗi khi tải danh sách hợp đồng." : "Error loading contracts list.");
|
||||
this.contractsList = [];
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePdfUrl(index) {
|
||||
const contract = this.contractsList[index];
|
||||
if (contract?.document && contract.document[0]?.pdf) {
|
||||
this.pdfFileUrl = `${this.$getpath()}download-contract/${contract.document[0].pdf}`;
|
||||
} else {
|
||||
this.pdfFileUrl = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
switchContract(index) {
|
||||
this.activeContractIndex = index;
|
||||
this.updatePdfUrl(index);
|
||||
},
|
||||
|
||||
downloadDocx() {
|
||||
if (!this.hasValidDocument) {
|
||||
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = this.currentContract.document[0].file;
|
||||
const url = `${this.$getpath()}download/?name=${filename}&type=contract`;
|
||||
this.$download(url, filename);
|
||||
},
|
||||
|
||||
downloadPdf() {
|
||||
if (!this.hasValidDocument) {
|
||||
this.$snackbar(this.isVietnamese ? "Không có file để tải" : "No file to download", { type: "is-warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = this.currentContract.document[0].pdf;
|
||||
const url = `${this.$getpath()}download/?name=${filename}&type=contract`;
|
||||
this.$download(url, filename);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contract-content {
|
||||
max-width: 95%;
|
||||
margin: 0 auto;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
font-family: "Times New Roman", serif;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tabs-list {
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tabs-item {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.tabs-item.bg-primary:hover a {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.tabs-link {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.contract-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,556 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Loading Overlay -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-overlay"
|
||||
>
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<!-- View Mode Toggle -->
|
||||
<div
|
||||
class="mb-5 pb-3"
|
||||
style="border-bottom: 2px solid #e8e8e8"
|
||||
>
|
||||
<div class="buttons has-addons">
|
||||
<button
|
||||
@click="viewMode = 'list'"
|
||||
:class="['button', viewMode === 'list' ? 'is-primary' : 'is-light']"
|
||||
>
|
||||
Danh sách
|
||||
</button>
|
||||
<button
|
||||
@click="viewMode = 'gallery'"
|
||||
:class="['button', viewMode === 'gallery' ? 'is-primary' : 'is-light']"
|
||||
>
|
||||
Thư viện
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase Document Types List -->
|
||||
<div v-if="phasedoctypes && phasedoctypes.length > 0">
|
||||
<div
|
||||
v-for="doctype in phasedoctypes"
|
||||
:key="doctype.id"
|
||||
class="mb-6"
|
||||
>
|
||||
<!-- Document Type Header with Upload Button -->
|
||||
<div class="level is-mobile mb-4">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<p class="is-size-6 has-text-weight-semibold has-text-primary">
|
||||
{{ doctype.doctype__name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<FileUpload
|
||||
v-if="$getEditRights()"
|
||||
:type="['file', 'image', 'pdf']"
|
||||
@files="(files) => handleUpload(files, doctype.doctype)"
|
||||
position="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div v-if="viewMode === 'list'">
|
||||
<div v-if="getFilesByDocType(doctype.doctype).length > 0">
|
||||
<div
|
||||
v-for="file in getFilesByDocType(doctype.doctype)"
|
||||
:key="file.id"
|
||||
class="is-flex is-justify-content-space-between is-align-items-center py-3 px-4 has-background-warning has-text-white"
|
||||
style="border-bottom: #e8e8e8 solid 1px; transition: all 0.2s ease; opacity: 0.95; cursor: pointer"
|
||||
@mouseenter="$event.currentTarget.style.opacity = '1'"
|
||||
@mouseleave="$event.currentTarget.style.opacity = '0.95'"
|
||||
>
|
||||
<div style="flex: 1; min-width: 0">
|
||||
<p
|
||||
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
||||
style="word-break: break-word"
|
||||
>
|
||||
{{ file.name || file.file__name }}
|
||||
</p>
|
||||
<p class="is-size-7 has-text-white-bis">
|
||||
{{ $formatFileSize(file.file__size) }} •
|
||||
{{ $dayjs(file.create_time).format("L HH:mm") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="buttons are-small ml-3">
|
||||
<button
|
||||
@click="viewFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'view.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click="downloadFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'download.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteFile(file.id)"
|
||||
class="button has-background-white has-text-danger"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'bin.svg',
|
||||
type: 'danger',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
||||
>
|
||||
Chưa có file nào
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery View -->
|
||||
<div v-if="viewMode === 'gallery'">
|
||||
<div
|
||||
v-if="getFilesByDocType(doctype.doctype).length > 0"
|
||||
class="columns is-multiline is-variable is-2"
|
||||
>
|
||||
<div
|
||||
v-for="file in getFilesByDocType(doctype.doctype)"
|
||||
:key="file.id"
|
||||
class="column is-half-tablet is-one-third-desktop"
|
||||
>
|
||||
<div
|
||||
class="has-background-warning has-text-white"
|
||||
style="
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(50, 115, 220, 0.2);
|
||||
"
|
||||
@mouseenter="
|
||||
$event.currentTarget.style.transform = 'translateY(-4px)';
|
||||
$event.currentTarget.style.boxShadow = '0 6px 16px rgba(50, 115, 220, 0.3)';
|
||||
"
|
||||
@mouseleave="
|
||||
$event.currentTarget.style.transform = 'translateY(0)';
|
||||
$event.currentTarget.style.boxShadow = '0 2px 8px rgba(50, 115, 220, 0.2)';
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
min-height: 140px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="isImage(file.file__name)"
|
||||
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center"
|
||||
>
|
||||
<img
|
||||
:src="`${$getpath()}static/files/${file.file__file}`"
|
||||
:alt="file.file__name"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="has-text-white-ter"
|
||||
style="font-size: 48px; line-height: 1"
|
||||
>
|
||||
FILE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px 16px">
|
||||
<p
|
||||
class="is-size-7 has-text-weight-semibold has-text-white mb-1"
|
||||
:title="file.file__name"
|
||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
|
||||
>
|
||||
{{ file.file__name }}
|
||||
</p>
|
||||
<p class="is-size-7 has-text-white-bis mb-3">
|
||||
{{ $formatFileSize(file.file__size) }}
|
||||
</p>
|
||||
|
||||
<div class="buttons are-small is-centered">
|
||||
<button
|
||||
@click="viewFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'view.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click="downloadFile(file)"
|
||||
class="button has-background-white has-text-primary"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'download.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteFile(file.id)"
|
||||
class="button has-background-white has-text-danger"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'bin.svg',
|
||||
type: 'danger',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="has-text-grey-light is-size-7 has-text-centered py-5"
|
||||
>
|
||||
Chưa có file nào
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- If no phase doctypes -->
|
||||
<div
|
||||
v-else-if="!isLoading"
|
||||
class="has-text-centered py-6"
|
||||
>
|
||||
<p class="has-text-grey-light is-size-7">Chưa có loại tài liệu được định nghĩa cho giai đoạn này.</p>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
@close="showmodal = undefined"
|
||||
@modalevent="handleModalEvent"
|
||||
v-bind="showmodal"
|
||||
v-if="showmodal"
|
||||
></Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ContractPaymentUpload",
|
||||
setup() {
|
||||
const { $formatFileSize, $dayjs, $getpath } = useNuxtApp();
|
||||
return { $formatFileSize, $dayjs, $getpath };
|
||||
},
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
transaction: null,
|
||||
files: [],
|
||||
isLoading: false,
|
||||
showmodal: undefined,
|
||||
phasedoctypes: [],
|
||||
viewMode: "list",
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
const { $getdata } = useNuxtApp();
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const transactionId = this.row.id;
|
||||
this.transaction = await $getdata("transaction", { id: transactionId }, undefined, true);
|
||||
|
||||
if (this.transaction?.phase) {
|
||||
await this.fetchPhaseDoctypes();
|
||||
}
|
||||
|
||||
await this.fetchFiles();
|
||||
} catch (error) {
|
||||
console.error("Error during component creation:", error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchPhaseDoctypes() {
|
||||
const { $getdata } = useNuxtApp();
|
||||
if (!this.transaction?.phase) return;
|
||||
|
||||
try {
|
||||
const phasedoctypesData = await $getdata(
|
||||
"phasedoctype",
|
||||
{
|
||||
phase: this.transaction.phase,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
if (phasedoctypesData) {
|
||||
this.phasedoctypes = Array.isArray(phasedoctypesData) ? phasedoctypesData : [phasedoctypesData];
|
||||
} else {
|
||||
this.phasedoctypes = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Lỗi khi tải phase doctypes:", error);
|
||||
this.phasedoctypes = [];
|
||||
}
|
||||
},
|
||||
async fetchFiles() {
|
||||
const { $getdata } = useNuxtApp();
|
||||
if (!this.row.id) return;
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const detail = await $getdata(
|
||||
"reservation",
|
||||
{
|
||||
id: this.transaction.txncurrent__detail,
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
const filesArray = await $getdata(
|
||||
"transactionfile",
|
||||
{
|
||||
txn_detail: detail.id,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
if (filesArray) {
|
||||
this.files = (Array.isArray(filesArray) ? filesArray : [filesArray]).sort(
|
||||
(a, b) => new Date(b.create_time) - new Date(a.create_time),
|
||||
);
|
||||
} else {
|
||||
this.files = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Lỗi khi tải danh sách file:", error);
|
||||
this.files = [];
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
getFilesByDocType(docTypeId) {
|
||||
return this.files.filter(
|
||||
(file) => file.file__doc_type === docTypeId || (file.file__doc_type == null && docTypeId == null),
|
||||
);
|
||||
},
|
||||
getFileExtension(fileName) {
|
||||
return fileName ? fileName.split(".").pop().toLowerCase() : "file";
|
||||
},
|
||||
isImage(fileName) {
|
||||
const ext = this.getFileExtension(fileName);
|
||||
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
|
||||
},
|
||||
isViewableDocument(fileName) {
|
||||
const ext = this.getFileExtension(fileName);
|
||||
return ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(ext);
|
||||
},
|
||||
async handleUpload(uploadedFiles, docTypeId) {
|
||||
if (!uploadedFiles || uploadedFiles.length === 0) return;
|
||||
|
||||
this.isLoading = true;
|
||||
const { $patchapi, $getdata, $insertapi } = useNuxtApp();
|
||||
|
||||
try {
|
||||
for (const fileRecord of uploadedFiles) {
|
||||
if (docTypeId) {
|
||||
await $patchapi("file", {
|
||||
id: fileRecord.id,
|
||||
doc_type: docTypeId,
|
||||
});
|
||||
}
|
||||
|
||||
const detail = await $getdata(
|
||||
"reservation",
|
||||
{
|
||||
id: this.transaction.txncurrent__detail,
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const payload = {
|
||||
txn_detail: detail.id,
|
||||
file: fileRecord.id,
|
||||
phase: this.transaction?.phase,
|
||||
};
|
||||
|
||||
const result = await $insertapi("transactionfile", payload);
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error || "Lưu file không thành công.");
|
||||
}
|
||||
}
|
||||
|
||||
await this.fetchFiles();
|
||||
this.$emit("upload-completed");
|
||||
} catch (error) {
|
||||
console.error("Lỗi khi lưu file:", error);
|
||||
alert("Đã xảy ra lỗi khi tải file lên. Vui lòng thử lại.");
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
downloadFile(file) {
|
||||
const { $getpath } = useNuxtApp();
|
||||
const filePath = file.file__file || file.file;
|
||||
if (!filePath) return;
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = `${$getpath()}static/files/${filePath}`;
|
||||
link.download = file.file__name || "download";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
deleteFile(fileId) {
|
||||
this.showmodal = {
|
||||
component: "dialog/Confirm",
|
||||
title: "Xác nhận xóa",
|
||||
height: "10vh",
|
||||
width: "40%",
|
||||
vbind: {
|
||||
content: "Bạn có chắc chắn muốn xóa file này không?",
|
||||
},
|
||||
onConfirm: async () => {
|
||||
this.isLoading = true;
|
||||
const { $deleteapi } = useNuxtApp();
|
||||
try {
|
||||
const result = await $deleteapi("transactionfile", fileId);
|
||||
if (result && !result.error) {
|
||||
await this.fetchFiles();
|
||||
} else {
|
||||
throw new Error(result.error || "Xóa file không thành công.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Lỗi khi xóa file:", error);
|
||||
alert("Đã xảy ra lỗi khi xóa file. Vui lòng thử lại.");
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
handleModalEvent(event) {
|
||||
if (event.name === "confirm" && typeof this.showmodal?.onConfirm === "function") {
|
||||
this.showmodal.onConfirm();
|
||||
}
|
||||
},
|
||||
viewFile(file) {
|
||||
const { $getpath } = useNuxtApp();
|
||||
const fileName = file.file__name || "";
|
||||
const filePath = file.file__file || file.file;
|
||||
if (!filePath) return;
|
||||
|
||||
const isImageFile = this.isImage(fileName);
|
||||
const isViewable = this.isViewableDocument(fileName);
|
||||
const fileUrl = `${$getpath()}static/files/${filePath}`;
|
||||
|
||||
if (isImageFile) {
|
||||
this.showmodal = {
|
||||
title: fileName,
|
||||
component: "media/ChipImage",
|
||||
vbind: {
|
||||
extend: false,
|
||||
file: file,
|
||||
image: fileUrl,
|
||||
show: ["download", "delete"],
|
||||
},
|
||||
};
|
||||
} else if (isViewable) {
|
||||
// Mở Google Viewer trực tiếp trong tab mới
|
||||
const viewerUrl = `https://docs.google.com/gview?url=${fileUrl}&embedded=false`;
|
||||
window.open(viewerUrl, "_blank");
|
||||
} else {
|
||||
this.downloadFile(file);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loader {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3273dc;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,798 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="productData"
|
||||
class="grid px-3"
|
||||
>
|
||||
<div class="cell is-col-span-12">
|
||||
<div id="schedule-content">
|
||||
<div
|
||||
v-if="selectedPolicy"
|
||||
id="print-area"
|
||||
:class="{ 'is-loading': isLoading }"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
||||
<h3 class="title is-4 has-text-primary mb-1">
|
||||
{{ selectedPolicy.name }}
|
||||
</h3>
|
||||
<div>
|
||||
<span class="button is-white">
|
||||
<span class="has-text-weight-semibold">Đơn vị: VNĐ</span>
|
||||
</span>
|
||||
<button
|
||||
class="button is-light"
|
||||
@click="$emit('print')"
|
||||
id="ignore-print"
|
||||
>
|
||||
<span class="is-size-6">In</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr
|
||||
class="my-4"
|
||||
style="background-color: var(--bulma-background)"
|
||||
/>
|
||||
|
||||
<!-- Summary Information -->
|
||||
<div class="fixed-grid has-4-cols-mobile has-7-cols-desktop">
|
||||
<div class="grid">
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Sản phẩm</p>
|
||||
<p class="has-text-primary has-text-weight-medium">
|
||||
{{ productData.trade_code || productData.code }}
|
||||
<a
|
||||
class="ml-4"
|
||||
id="ignore"
|
||||
@click="$copyToClipboard(productData.trade_code)"
|
||||
>
|
||||
<SvgIcon
|
||||
name="copy.svg"
|
||||
type="primary"
|
||||
:size="18"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá niêm yết</p>
|
||||
<p class="has-text-primary">
|
||||
{{ $numtoString(calculatorData.originPrice) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Tổng chiết khấu</p>
|
||||
<p class="has-text-danger has-text-weight-bold">
|
||||
{{ $numtoString(calculatorData.totalDiscount) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá sau chiết khấu</p>
|
||||
<p class="has-text-black has-text-weight-bold">
|
||||
{{ $numtoString(calculatorData.salePrice) }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedPolicy.contract_allocation_percentage < 100"
|
||||
class="cell is-col-span-6-mobile is-col-span-1-desktop"
|
||||
>
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Giá trị bảo đảm</p>
|
||||
<p class="has-text-primary">
|
||||
{{ $numtoString(calculatorData.allocatedPrice) }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="totalPaid === 0"
|
||||
class="cell is-col-span-6-mobile is-col-span-1-desktop"
|
||||
>
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Đặt cọc</p>
|
||||
<p class="has-text-primary">
|
||||
{{ $numtoString(selectedPolicy.deposit) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
|
||||
<p class="is-size-6 has-text-weight-bold mb-1">Khách hàng</p>
|
||||
<p
|
||||
v-if="selectedCustomer"
|
||||
class="has-text-primary has-text-weight-medium"
|
||||
>
|
||||
{{ selectedCustomer.code }} - {{ selectedCustomer.fullname }}
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="has-text-grey is-italic is-size-6"
|
||||
>
|
||||
Chưa chọn
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr
|
||||
class="my-4"
|
||||
style="background-color: var(--bulma-background)"
|
||||
/>
|
||||
|
||||
<!-- Detailed Discounts -->
|
||||
<div
|
||||
v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0"
|
||||
class="mt-4 mb-4"
|
||||
>
|
||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">CHI TIẾT CHIẾT KHẤU:</p>
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
colspan="2"
|
||||
>
|
||||
Diễn giải chiết khấu
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
width="15%"
|
||||
>
|
||||
Giá trị
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
width="20%"
|
||||
>
|
||||
Thành tiền
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
width="20%"
|
||||
>
|
||||
Còn lại
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
class="has-text-grey-light"
|
||||
>
|
||||
<td
|
||||
colspan="4"
|
||||
class="has-text-right pt-1 pb-1"
|
||||
>
|
||||
Giá gốc
|
||||
</td>
|
||||
<td class="has-text-right has-text-weight-bold pt-1 pb-1">
|
||||
{{ $numtoString(calculatorData.originPrice) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(item, idx) in calculatorData.detailedDiscounts"
|
||||
:key="`discount-${idx}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
>
|
||||
<td
|
||||
width="5%"
|
||||
class="has-text-centered"
|
||||
>
|
||||
{{ idx + 1 }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="has-text-weight-semibold">{{ item.name }}</span>
|
||||
<span class="tag is-primary has-text-white is-rounded border ml-1">{{ item.code }}</span>
|
||||
</td>
|
||||
<td class="has-text-right">
|
||||
{{ item.customType === 1 ? item.customValue + "%" : $numtoString(item.customValue) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-danger">-{{ $numtoString(item.amount) }}</td>
|
||||
<td class="has-text-right has-text-primary">
|
||||
{{ $numtoString(item.remaining) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Early Payment Details -->
|
||||
<div
|
||||
v-if="isEarlyPaymentActive"
|
||||
class="mt-4 mb-4"
|
||||
>
|
||||
<!-- Original Schedule -->
|
||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
|
||||
LỊCH THANH TOÁN GỐC (THEO CHÍNH SÁCH)
|
||||
</p>
|
||||
<div class="table-container schedule-container mb-4">
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Đợt
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Tỷ lệ
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số tiền (VND)
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày bắt đầu
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày đến hạn
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số ngày
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(plan, index) in calculatorData.originalPaymentSchedule"
|
||||
:key="`orig-plan-${index}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
>
|
||||
<td class="has-text-weight-semibold">Đợt {{ plan.cycle }}</td>
|
||||
<td class="has-text-right">
|
||||
{{ plan.type === 1 ? `${plan.value}%` : "-" }}
|
||||
</td>
|
||||
<td class="has-text-right">
|
||||
{{ $numtoString(plan.amount) }}
|
||||
</td>
|
||||
<td>{{ formatDate(plan.from_date) }}</td>
|
||||
<td>{{ formatDate(plan.to_date) }}</td>
|
||||
<td class="has-text-right">{{ plan.days }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Early Discount Calculation Details -->
|
||||
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
|
||||
DIỄN GIẢI CHIẾT KHẤU THANH TOÁN SỚM
|
||||
</p>
|
||||
<div class="table-container schedule-container">
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Đợt
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Hạn TT Gốc
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày TT Thực Tế
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số tiền gốc
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số ngày TT sớm
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Tỷ lệ CK (%/ngày)
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Tiền chiết khấu
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, idx) in calculatorData.earlyDiscountDetails"
|
||||
:key="`early-discount-${idx}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
>
|
||||
<td>Đợt {{ item.cycle }}</td>
|
||||
<td>{{ formatDate(item.original_payment_date) }}</td>
|
||||
<td>{{ formatDate(item.actual_payment_date) }}</td>
|
||||
<td class="has-text-right">
|
||||
{{ $numtoString(item.original_amount) }}
|
||||
</td>
|
||||
<td class="has-text-right">{{ item.early_days }}</td>
|
||||
<td class="has-text-right">{{ item.discount_rate }}</td>
|
||||
<td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="has-background-light">
|
||||
<th
|
||||
colspan="6"
|
||||
class="has-text-right has-text-weight-bold"
|
||||
>
|
||||
Tổng chiết khấu thanh toán sớm
|
||||
</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-danger">
|
||||
-{{ $numtoString(totalEarlyDiscount) }}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Schedule Table -->
|
||||
<div
|
||||
v-if="displaySchedule.length > 0"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="level m-0 mb-2 is-mobile">
|
||||
<div class="level-left">
|
||||
<p class="has-text-weight-bold is-size-5 has-text-primary is-underlined">
|
||||
<span v-if="isEarlyPaymentActive">LỊCH THANH TOÁN CUỐI CÙNG</span>
|
||||
<span v-else>LỊCH THANH TOÁN</span>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="level-right"
|
||||
id="ignore-print"
|
||||
>
|
||||
<div class="buttons are-small has-addons">
|
||||
<button
|
||||
class="button"
|
||||
@click="viewMode = 'table'"
|
||||
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'"
|
||||
>
|
||||
<span class="is-size-6">Bảng</span>
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
@click="viewMode = 'list'"
|
||||
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'"
|
||||
>
|
||||
<span class="is-size-6">Thẻ</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table View -->
|
||||
<div
|
||||
v-if="viewMode === 'table'"
|
||||
class="table-container schedule-container"
|
||||
>
|
||||
<table class="table is-fullwidth is-hoverable is-size-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Đợt thanh toán
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Số tiền (VND)
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Đã thanh toán
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal has-text-right"
|
||||
style="border: none"
|
||||
>
|
||||
Còn phải TT
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày bắt đầu
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Ngày đến hạn
|
||||
</th>
|
||||
<th
|
||||
class="has-background-primary has-text-white has-font-weight-normal"
|
||||
style="border: none"
|
||||
>
|
||||
Trạng thái
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(plan, index) in displaySchedule"
|
||||
:key="`plan-${index}`"
|
||||
style="border-bottom: 1px solid #f5f5f5"
|
||||
:class="plan.is_merged ? 'has-background-warning-light' : ''"
|
||||
>
|
||||
<td
|
||||
class="has-text-weight-semibold"
|
||||
:class="plan.is_merged ? 'has-text-warning' : ''"
|
||||
>
|
||||
Đợt {{ plan.cycle }}
|
||||
<span
|
||||
v-if="plan.is_merged"
|
||||
class="tag is-warning is-light ml-1 is-size-7"
|
||||
>GỘP SỚM</span
|
||||
>
|
||||
</td>
|
||||
<td class="has-text-right">
|
||||
<div
|
||||
v-if="plan.is_merged"
|
||||
class="has-text-right"
|
||||
>
|
||||
<p
|
||||
class="has-text-grey"
|
||||
title="Tổng các đợt gốc"
|
||||
>
|
||||
{{ $numtoString(totalOriginalEarlyAmount) }}
|
||||
</p>
|
||||
<p
|
||||
class="has-text-danger"
|
||||
title="Chiết khấu thanh toán sớm"
|
||||
>
|
||||
- {{ $numtoString(totalEarlyDiscount) }}
|
||||
</p>
|
||||
<hr
|
||||
class="my-1"
|
||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%"
|
||||
/>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $numtoString(plan.amount) }}
|
||||
</p>
|
||||
</div>
|
||||
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
||||
</td>
|
||||
<td class="has-text-right has-text-success">
|
||||
{{ $numtoString(plan.paid_amount) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-danger">
|
||||
{{ $numtoString(plan.remain_amount) }}
|
||||
</td>
|
||||
<td>{{ formatDate(plan.from_date) }}</td>
|
||||
<td>{{ formatDate(plan.to_date) }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="plan.status === 2"
|
||||
class="tag is-success"
|
||||
>Đã thanh toán</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="tag is-warning"
|
||||
>Chờ thanh toán</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="has-background-light">
|
||||
<th class="has-text-right has-text-weight-bold">Tổng cộng</th>
|
||||
<th class="has-text-right has-text-weight-bold">
|
||||
{{ $numtoString(totalAmount) }}
|
||||
</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-success">
|
||||
{{ $numtoString(totalPaid) }}
|
||||
</th>
|
||||
<th class="has-text-right has-text-weight-bold has-text-danger">
|
||||
{{ $numtoString(calculatorData.totalRemaining) }}
|
||||
</th>
|
||||
<th colspan="3"></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- List View (Card) -->
|
||||
<div
|
||||
v-else-if="viewMode === 'list'"
|
||||
class="schedule-container"
|
||||
>
|
||||
<div
|
||||
v-for="(plan, index) in displaySchedule"
|
||||
:key="`card-${index}`"
|
||||
class="card mb-4"
|
||||
:class="plan.is_merged ? 'has-background-warning-light' : ''"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="level is-mobile mb-5">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<span
|
||||
class="tag is-primary"
|
||||
:class="plan.is_merged ? 'is-warning' : ''"
|
||||
>Đợt {{ plan.cycle }}</span
|
||||
>
|
||||
<span
|
||||
v-if="plan.is_merged"
|
||||
class="tag is-warning is-light ml-1 is-size-7"
|
||||
>GỘP SỚM</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item has-text-weight-bold">
|
||||
<div
|
||||
v-if="plan.is_merged"
|
||||
class="has-text-right"
|
||||
>
|
||||
<p
|
||||
class="has-text-grey"
|
||||
title="Tổng các đợt gốc"
|
||||
>
|
||||
{{ $numtoString(totalOriginalEarlyAmount) }}
|
||||
</p>
|
||||
<p
|
||||
class="has-text-danger"
|
||||
title="Chiết khấu thanh toán sớm"
|
||||
>
|
||||
- {{ $numtoString(totalEarlyDiscount) }}
|
||||
</p>
|
||||
<hr
|
||||
class="my-1"
|
||||
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto"
|
||||
/>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $numtoString(plan.amount) }}
|
||||
</p>
|
||||
</div>
|
||||
<span v-else>{{ $numtoString(plan.amount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Đã thanh toán:</div>
|
||||
<div class="level-right has-text-success">
|
||||
{{ $numtoString(plan.paid_amount) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Còn phải TT:</div>
|
||||
<div class="level-right has-text-danger">
|
||||
{{ $numtoString(plan.remain_amount) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Từ ngày:</div>
|
||||
<div class="level-right">
|
||||
{{ formatDate(plan.from_date) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mb-1">
|
||||
<div class="level-left">Đến hạn:</div>
|
||||
<div class="level-right">
|
||||
{{ formatDate(plan.to_date) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">Trạng thái:</div>
|
||||
<div class="level-right">
|
||||
<span
|
||||
v-if="plan.status === 2"
|
||||
class="tag is-success"
|
||||
>Đã thanh toán</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="tag is-warning"
|
||||
>Chờ thanh toán</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Footer -->
|
||||
<div
|
||||
class=""
|
||||
style="border-top: 1px solid #eee"
|
||||
>
|
||||
<div class="level is-mobile is-size-6 my-4">
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<span class="is-uppercase is-size-4 has-text-weight-semibold">Tổng cộng: </span>
|
||||
<span class="has-text-success has-text-weight-bold is-size-4">
|
||||
{{ $numtoString(calculatorData.allocatedPrice) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN
|
||||
const props = defineProps({
|
||||
productData: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
selectedPolicy: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
selectedCustomer: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
calculatorData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
// Cấu trúc:
|
||||
// {
|
||||
// originPrice: number,
|
||||
// totalDiscount: number,
|
||||
// salePrice: number,
|
||||
// allocatedPrice: number,
|
||||
// originalPaymentSchedule: array,
|
||||
// finalPaymentSchedule: array,
|
||||
// earlyDiscountDetails: array,
|
||||
// totalRemaining: number,
|
||||
// detailedDiscounts: array,
|
||||
// baseDate: Date
|
||||
// }
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["print"]);
|
||||
|
||||
// Local state
|
||||
const viewMode = ref("table");
|
||||
|
||||
// Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN
|
||||
const displaySchedule = computed(() => {
|
||||
return props.calculatorData?.finalPaymentSchedule || [];
|
||||
});
|
||||
|
||||
const isEarlyPaymentActive = computed(() => {
|
||||
return props.calculatorData.earlyDiscountDetails && props.calculatorData.earlyDiscountDetails.length > 0;
|
||||
});
|
||||
|
||||
const totalEarlyDiscount = computed(() => {
|
||||
return props.calculatorData.earlyDiscountDetails?.reduce((sum, item) => sum + item.discount_amount, 0) || 0;
|
||||
});
|
||||
|
||||
const totalOriginalEarlyAmount = computed(() => {
|
||||
return props.calculatorData.earlyDiscountDetails?.reduce((sum, item) => sum + item.original_amount, 0) || 0;
|
||||
});
|
||||
|
||||
const totalAmount = computed(() => {
|
||||
return displaySchedule.value.reduce((sum, plan) => sum + plan.amount, 0);
|
||||
});
|
||||
|
||||
const totalPaid = computed(() => {
|
||||
return displaySchedule.value.reduce((sum, plan) => sum + plan.paid_amount, 0);
|
||||
});
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return "-";
|
||||
return dayjs(date).format("L");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-container.schedule-container thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: white;
|
||||
z-index: 2;
|
||||
border-bottom: 1px solid #dbdbdb !important;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
li.is-active a,
|
||||
li a:hover {
|
||||
color: white !important;
|
||||
background-color: #204853 !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
tr,
|
||||
td,
|
||||
th,
|
||||
.card {
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.schedule-container {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.table-container.schedule-container thead th {
|
||||
position: static;
|
||||
}
|
||||
|
||||
#ignore-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user