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

@@ -6,14 +6,27 @@
<!-- Cart selection list -->
<div v-if="showCartList">
<div class="tags">
<div v-for="(cart, index) in carts" :key="cart.id" class="tag is-medium has-addons"
<div
v-for="(cart, index) in carts"
:key="cart.id"
class="tag is-medium has-addons"
:class="{ 'is-primary': selectedCarts.includes(cart.id) }"
@click="handleCartClick(cart.id, index, $event)" style="cursor: pointer; user-select: none;">
@click="handleCartClick(cart.id, index, $event)"
style="cursor: pointer; user-select: none"
>
<span>{{ cart.code }} - {{ cart.name }}</span>
<span v-if="selectedCarts.includes(cart.id) && $getEditRights()" class="ml-2 tag is-delete"
@click.stop="removeCart(cart.id)"></span>
<span v-if="selectedCarts.includes(cart.id) && $getEditRights()" class="tag ml-2" @click.stop="editCart(cart.id)"
title="Chỉnh sửa giỏ hàng" style="cursor: pointer;">
<span
v-if="selectedCarts.includes(cart.id) && $getEditRights()"
class="ml-2 tag is-delete"
@click.stop="removeCart(cart.id)"
></span>
<span
v-if="selectedCarts.includes(cart.id) && $getEditRights()"
class="tag ml-2"
@click.stop="editCart(cart.id)"
title="Chỉnh sửa giỏ hàng"
style="cursor: pointer"
>
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'dark', size: 16 }" />
</span>
</div>
@@ -21,10 +34,16 @@
</div>
</div>
<div v-if="$getEditRights()" class="column is-2">
<div
v-if="$getEditRights()"
class="column is-2"
>
<div class="field">
<div class="control">
<a class="mr-3" @click="upload()">
<a
class="mr-3"
@click="upload()"
>
<span class="icon-text">
<SvgIcon v-bind="{ name: 'upload.svg', type: 'primary', size: 25 }" />
<span class="ml-1 fsb-17">Phân bổ</span>
@@ -42,12 +61,18 @@
</div>
<!-- Data View với product được filter -->
<div class="m-0" v-if="selectedCarts.length > 0">
<div
class="m-0"
v-if="selectedCarts.length > 0"
>
<div class="level mb-3">
<div class="level-left">
<div class="level-item">
<h5 class="title is-5">Sản phẩm trong giỏ</h5>
<button class="button is-small is-text" @click="clearSelection">
<button
class="button is-small is-text"
@click="clearSelection"
>
Bỏ chọn tất cả
</button>
</div>
@@ -55,180 +80,188 @@
</div>
<!-- DataView với key động để force reload khi filter thay đổi -->
<DataView v-bind="dataViewConfig" :key="selectedCartsKey" />
<DataView
v-bind="dataViewConfig"
:key="selectedCartsKey"
/>
</div>
<!-- Modal Components -->
<Modal v-if="showmodal" @close="showmodal = undefined" @dataevent="dataevent" v-bind="showmodal" />
<Modal
v-if="showmodal"
@close="showmodal = undefined"
@dataevent="dataevent"
v-bind="showmodal"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useStore } from '~/stores/index'
import DataView from '~/components/datatable/DataView.vue'
import Modal from '~/components/Modal.vue'
import SvgIcon from '~/components/SvgIcon.vue'
import { ref, computed, onMounted } from "vue";
import { useStore } from "~/stores/index";
import DataView from "~/components/datatable/DataView.vue";
import Modal from "~/components/Modal.vue";
import SvgIcon from "~/components/SvgIcon.vue";
const { $getapi, $findapi, $copy, $find } = useNuxtApp()
const store = useStore()
const { $getapi, $findapi, $copy, $find } = useNuxtApp();
const store = useStore();
// State
const showmodal = ref()
const showCartList = ref(true)
const selectedCarts = ref([])
const carts = ref([])
const lastSelectedIndex = ref(null)
const showmodal = ref();
const showCartList = ref(true);
const selectedCarts = ref([]);
const carts = ref([]);
const lastSelectedIndex = ref(null);
// Key động để force reload DataView khi selectedCarts thay đổi
const selectedCartsKey = computed(() => {
const sorted = [...selectedCarts.value].sort((a, b) => a - b)
return 'dataview-cart-' + sorted.join('-')
})
const sorted = [...selectedCarts.value].sort((a, b) => a - b);
return "dataview-cart-" + sorted.join("-");
});
// DataView config
const dataViewConfig = computed(() => {
const config = {
api: 'product',
setting: 'product-list-cart',
pagename: 'pagedata1',
modal: { component: 'parameter/ProductForm', title: 'Sản phẩm' },
api: "product",
setting: "product-list-cart",
pagename: "pagedata1",
modal: { component: "parameter/ProductForm", title: "Sản phẩm" },
timeopt: { time: 36000, disable: "add" },
realtime: { time: 2, update: "false" }
}
realtime: { time: 2, update: "false" },
};
if (selectedCarts.value.length > 0) {
config.filter = { cart__in: selectedCarts.value }
config.filter = { cart__in: selectedCarts.value };
}
return config
})
return config;
});
// Modal configs
const newAddon = {
component: 'parameter/NewCart',
title: 'Giỏ hàng',
width: '900px',
height: '400px'
}
component: "parameter/NewCart",
title: "Giỏ hàng",
width: "900px",
height: "400px",
};
// Methods
function handleCartClick(cartId, index, event) {
if (event.ctrlKey) {
// Ctrl + Click: Chọn/bỏ chọn từng cái
toggleCart(cartId)
lastSelectedIndex.value = index
toggleCart(cartId);
lastSelectedIndex.value = index;
} else if (event.shiftKey) {
// Shift + Click: Chọn range từ last đến hiện tại
if (lastSelectedIndex.value !== null) {
selectRange(lastSelectedIndex.value, index)
selectRange(lastSelectedIndex.value, index);
} else {
toggleCart(cartId)
lastSelectedIndex.value = index
toggleCart(cartId);
lastSelectedIndex.value = index;
}
} else {
// Click bình thường: Chọn chỉ cái này
selectedCarts.value = [cartId]
lastSelectedIndex.value = index
selectedCarts.value = [cartId];
lastSelectedIndex.value = index;
}
}
function selectRange(startIndex, endIndex) {
const start = Math.min(startIndex, endIndex)
const end = Math.max(startIndex, endIndex)
const start = Math.min(startIndex, endIndex);
const end = Math.max(startIndex, endIndex);
for (let i = start; i <= end; i++) {
const cartId = carts.value[i].id
const cartId = carts.value[i].id;
if (!selectedCarts.value.includes(cartId)) {
selectedCarts.value.push(cartId)
selectedCarts.value.push(cartId);
}
}
}
function toggleCart(cartId) {
const index = selectedCarts.value.indexOf(cartId)
const index = selectedCarts.value.indexOf(cartId);
if (index > -1) {
selectedCarts.value.splice(index, 1)
selectedCarts.value.splice(index, 1);
} else {
selectedCarts.value.push(cartId)
selectedCarts.value.push(cartId);
}
}
function removeCart(cartId) {
const index = selectedCarts.value.indexOf(cartId)
const index = selectedCarts.value.indexOf(cartId);
if (index > -1) {
selectedCarts.value.splice(index, 1)
selectedCarts.value.splice(index, 1);
}
}
function getCartName(cartId) {
const cart = carts.value.find(c => c.id === cartId)
return cart ? `${cart.code} - ${cart.name}` : ''
const cart = carts.value.find((c) => c.id === cartId);
return cart ? `${cart.code} - ${cart.name}` : "";
}
function applyFilter() {
if (selectedCarts.value.length === 0) {
alert('Vui lòng chọn ít nhất một giỏ hàng')
return
alert("Vui lòng chọn ít nhất một giỏ hàng");
return;
}
showCartList.value = false
showCartList.value = false;
}
function clearSelection() {
selectedCarts.value = []
showCartList.value = true
selectedCarts.value = [];
showCartList.value = true;
}
function upload() {
showmodal.value = {
component: 'parameter/ImportData',
title: 'Phân bổ giỏ hàng',
width: '80%',
height: '400px',
vbind: { code: 'product-cart' }
}
component: "parameter/ImportData",
title: "Phân bổ giỏ hàng",
width: "80%",
height: "400px",
vbind: { code: "product-cart" },
};
}
function addNew() {
showmodal.value = {
component: 'parameter/NewCart',
title: 'Thêm giỏ hàng mới',
width: '900px',
height: '500px'
}
component: "parameter/NewCart",
title: "Thêm giỏ hàng mới",
width: "900px",
height: "500px",
};
}
function editCart(cartId) {
showmodal.value = {
component: 'parameter/NewCart',
title: 'Chỉnh sửa giỏ hàng',
width: '900px',
height: '500px',
component: "parameter/NewCart",
title: "Chỉnh sửa giỏ hàng",
width: "900px",
height: "500px",
vbind: {
id: cartId
}
}
id: cartId,
},
};
}
function dataevent() {
loadCarts()
loadCarts();
}
async function loadCarts() {
try {
const conn = $findapi('cart')
const rs = await $getapi([conn])
const obj = $find(rs, { name: 'cart' })
const conn = $findapi("cart");
const rs = await $getapi([conn]);
const obj = $find(rs, { name: "cart" });
if (obj) {
carts.value = $copy(obj.data.rows || [])
carts.value = $copy(obj.data.rows || []);
}
} catch (error) {
console.error('Lỗi khi fetch giỏ hàng:', error)
console.error("Lỗi khi fetch giỏ hàng:", error);
}
}
// Init
onMounted(() => {
loadCarts()
})
</script>
loadCarts();
});
</script>

View File

@@ -18,15 +18,18 @@
@option="selected('product', $event)"
/>
</div>
<p class="help is-danger" v-if="false">error</p>
<p
class="help is-danger"
v-if="false"
>
error
</p>
</div>
</div>
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">
Giao dịch<b class="ml-1 has-text-danger">*</b>
</label>
<label class="label"> Giao dịch<b class="ml-1 has-text-danger">*</b> </label>
<div class="control">
<SearchBox
v-bind="{
@@ -40,7 +43,10 @@
@option="selected('phase', $event)"
/>
</div>
<p class="help is-danger" v-if="errors.phase">
<p
class="help is-danger"
v-if="errors.phase"
>
{{ errors.phase }}
</p>
</div>
@@ -48,9 +54,7 @@
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">
Khách hàng<b class="ml-1 has-text-danger">*</b>
</label>
<label class="label"> Khách hàng<b class="ml-1 has-text-danger">*</b> </label>
<div class="control">
<SearchBox
v-bind="{
@@ -65,7 +69,10 @@
@option="selected('customer', $event)"
/>
</div>
<p class="help is-danger" v-if="errors.customer">
<p
class="help is-danger"
v-if="errors.customer"
>
{{ errors.customer }}
</p>
</div>
@@ -73,9 +80,7 @@
<div :class="`column px-0 is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">
Số tiền đặt cọc<b class="ml-1 has-text-danger">*</b>
</label>
<label class="label"> Số tiền đặt cọc<b class="ml-1 has-text-danger">*</b> </label>
<div class="control">
<InputNumber
v-bind="{
@@ -86,7 +91,10 @@
@number="selected('amount', $event)"
></InputNumber>
</div>
<p class="help is-danger" v-if="errors.amount">
<p
class="help is-danger"
v-if="errors.amount"
>
{{ errors.amount }}
</p>
</div>
@@ -94,16 +102,17 @@
<div :class="`column is-6 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label"
>Hạn thanh toán<b class="ml-1 has-text-danger">*</b></label
>
<label class="label">Hạn thanh toán<b class="ml-1 has-text-danger">*</b></label>
<div class="control">
<Datepicker
v-bind="{ record: formData, attr: 'due_date' }"
@date="selected('due_date', $event)"
/>
</div>
<p class="help is-danger" v-if="errors.due_date">
<p
class="help is-danger"
v-if="errors.due_date"
>
{{ errors.due_date }}
</p>
</div>
@@ -111,9 +120,7 @@
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<div class="field">
<label class="label">{{
dataLang && findFieldName("note")[lang]
}}</label>
<label class="label">{{ dataLang && findFieldName("note")[lang] }}</label>
<div class="control">
<textarea
v-model="formData.note"
@@ -123,15 +130,18 @@
rows="3"
></textarea>
</div>
<p class="help is-danger" v-if="errors.note">{{ errors.note }}</p>
<p
class="help is-danger"
v-if="errors.note"
>
{{ errors.note }}
</p>
</div>
</div>
<div :class="`column is-full px-0 ${viewport === 1 ? 'px-0 pb-1' : ''}`">
<button
:class="`button is-primary has-text-white ${
isSubmitting ? 'is-loading' : ''
}`"
:class="`button is-primary has-text-white ${isSubmitting ? 'is-loading' : ''}`"
@click="handleSubmitData"
:disabled="isSubmitting"
>
@@ -170,28 +180,10 @@ const props = defineProps({
api: String,
});
const {
$updatepage,
$getdata,
$insertapi,
$updateapi,
$empty,
$snackbar,
$generateDocument,
} = useNuxtApp();
const transaction = await $getdata(
"transaction",
{ product: props.row.id },
undefined,
true
);
const { $updatepage, $getdata, $insertapi, $updateapi, $empty, $snackbar, $generateDocument } = useNuxtApp();
const transaction = await $getdata("transaction", { product: props.row.id }, undefined, true);
const reservation = transaction
? await $getdata(
"reservation",
{ transaction: transaction.id },
undefined,
true
)
? await $getdata("reservation", { transaction: transaction.id }, undefined, true)
: undefined;
const viewport = 5;
@@ -205,12 +197,7 @@ const showContractModal = ref(false);
// Load contract nếu đã có
if (transaction) {
contractData.value = await $getdata(
"contract",
{ transaction: transaction.id },
undefined,
true
);
contractData.value = await $getdata("contract", { transaction: transaction.id }, undefined, true);
}
const initFormData = {
@@ -279,9 +266,7 @@ const findFieldName = (code) => {
const selected = (fieldName, value) => {
const finalValue =
value !== null && typeof value === "object"
? value.id || value.index || value.code || value.label
: value;
value !== null && typeof value === "object" ? value.id || value.index || value.code || value.label : value;
formData.value[fieldName] = finalValue;
};
@@ -296,21 +281,15 @@ const checkErrors = (fields) => {
}
if ($empty(customer)) {
errors.value.customer = isVietnamese.value
? "Khách hàng không được để trống"
: "Customer is required";
errors.value.customer = isVietnamese.value ? "Khách hàng không được để trống" : "Customer is required";
}
if ($empty(amount)) {
errors.value.amount = isVietnamese.value
? "Số tiền đặt cọc không được để trống"
: "Deposit amount is required";
errors.value.amount = isVietnamese.value ? "Số tiền đặt cọc không được để trống" : "Deposit amount is required";
}
if ($empty(due_date)) {
errors.value.due_date = isVietnamese.value
? "Hạn thanh toán không được để trống"
: "Due date is required";
errors.value.due_date = isVietnamese.value ? "Hạn thanh toán không được để trống" : "Due date is required";
}
return Object.keys(errors.value).length > 0;
@@ -321,19 +300,13 @@ async function handleSubmitData() {
if (isSubmitting.value) return;
if (isEqual(formData.value, initFormData)) {
$snackbar(
isVietnamese.value ? "Form không thay đổi" : "Form is unchanged"
);
$snackbar(isVietnamese.value ? "Form không thay đổi" : "Form is unchanged");
return;
}
const hasValidationErrors = checkErrors(formData.value);
if (hasValidationErrors) {
$snackbar(
isVietnamese.value
? "Vui lòng kiểm tra lại dữ liệu."
: "Please check the data again."
);
$snackbar(isVietnamese.value ? "Vui lòng kiểm tra lại dữ liệu." : "Please check the data again.");
return;
}
@@ -345,12 +318,7 @@ async function handleSubmitData() {
date: dayjs().format("YYYY-MM-DD"),
};
const transactionRs = await $insertapi(
"transaction",
transactionPayload,
undefined,
false
);
const transactionRs = await $insertapi("transaction", transactionPayload, undefined, false);
const { date, id: transactionId, code: transactionCode } = transactionRs;
// 2. Tạo reservation
@@ -358,16 +326,11 @@ async function handleSubmitData() {
amount: Number(formData.value.amount),
due_date: formData.value.due_date,
creator: 1,
date:dayjs().format("YYYY-MM-DD"),
date: dayjs().format("YYYY-MM-DD"),
transaction: transactionId,
};
const reservationnRs = await $insertapi(
"reservation",
reservationPayload,
undefined,
false
);
const reservationnRs = await $insertapi("reservation", reservationPayload, undefined, false);
const reservationId = reservationnRs.id;
// 3. Generate document
@@ -409,19 +372,12 @@ async function handleSubmitData() {
user: null,
};
const contractResult = await $insertapi(
"contract",
contractPayload,
undefined,
false
);
const contractResult = await $insertapi("contract", contractPayload, undefined, false);
contractData.value = contractResult;
}
$snackbar(
isVietnamese.value
? "Tạo giao dịch và hợp đồng thành công!"
: "Transaction and contract created successfully!"
isVietnamese.value ? "Tạo giao dịch và hợp đồng thành công!" : "Transaction and contract created successfully!",
);
// Tự động mở modal xem hợp đồng
@@ -430,11 +386,7 @@ async function handleSubmitData() {
}
} catch (error) {
console.error("Create transaction failed:", error);
$snackbar(
isVietnamese.value
? "Tạo giao dịch thất bại"
: "Create transaction failed"
);
$snackbar(isVietnamese.value ? "Tạo giao dịch thất bại" : "Create transaction failed");
} finally {
isSubmitting.value = false;
}
@@ -447,11 +399,7 @@ function openContractModal() {
function handleContractUpdated(eventData) {
if (eventData?.data) {
contractData.value = { ...contractData.value, ...eventData.data };
$snackbar(
isVietnamese.value
? "Hợp đồng đã được cập nhật"
: "Contract has been updated"
);
$snackbar(isVietnamese.value ? "Hợp đồng đã được cập nhật" : "Contract has been updated");
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,8 @@ const props = defineProps({
row: Object,
});
const {
$buildFileUrl,
$id,
$formatFileSize,
$getdata,
$getpath,
$getapi,
$findapi,
$snackbar,
$exportpdf,
} = useNuxtApp();
const { $buildFileUrl, $id, $formatFileSize, $getdata, $getpath, $getapi, $findapi, $snackbar, $exportpdf } =
useNuxtApp();
const store = useStore();
const product = props.row || {};
const tab = ref("info");
@@ -37,19 +28,7 @@ const documents = ref([]);
const projectDocuments = ref([]);
const viewingImage = ref(null);
const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
const documentExtensions = [
"pdf",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"csv",
"zip",
"rar",
];
const documentExtensions = ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv", "zip", "rar"];
const policies = ref([]);
const selectedPolicy = ref(null);
@@ -79,9 +58,7 @@ async function loadAllPolicyData() {
const plansData = await $getdata("paymentplan", {}, undefined, false);
policies.value = policiesData || [];
allPaymentPlans.value = plansData
? plansData.sort((a, b) => a.cycle - b.cycle)
: [];
allPaymentPlans.value = plansData ? plansData.sort((a, b) => a.cycle - b.cycle) : [];
if (policies.value.length > 0) {
loadPlansForPolicy(policies.value[0].id);
@@ -102,27 +79,18 @@ function loadPlansForPolicy(id) {
if (!policy) return;
selectedPolicy.value = policy;
paymentPlans.value = allPaymentPlans.value.filter(
(plan) => plan.policy === id
);
paymentPlans.value = allPaymentPlans.value.filter((plan) => plan.policy === id);
activeTab.value = id;
}
function printContent() {
if (!record.value || !selectedPolicy.value) {
$snackbar(
isVietnamese.value
? "Vui lòng chọn chính sách"
: "Please select a policy",
{ type: "is-warning" }
);
$snackbar(isVietnamese.value ? "Vui lòng chọn chính sách" : "Please select a policy", { type: "is-warning" });
return;
}
const docId = 'print-area';
const fileName = `${selectedPolicy.value?.name || "Payment Schedule"} - ${
record.value?.code
}`;
const docId = "print-area";
const fileName = `${selectedPolicy.value?.name || "Payment Schedule"} - ${record.value?.code}`;
if (typeof $exportpdf === "function") {
$exportpdf(docId, fileName);
@@ -184,12 +152,7 @@ async function loadProductFiles() {
const files = await Promise.all(
links.map(async (link) => {
const fileData = await $getdata(
"file",
{ id: link.file },
undefined,
true
).catch((error) => {
const fileData = await $getdata("file", { id: link.file }, undefined, true).catch((error) => {
console.error(`Error loading file ${link.file}:`, error);
return null;
});
@@ -203,7 +166,7 @@ async function loadProductFiles() {
caption: fileData?.caption || "",
create_time: link.create_time,
};
})
}),
);
const imageList = [];
@@ -256,12 +219,7 @@ async function loadProjectFiles() {
const files = await Promise.all(
links.map(async (link) => {
const fileData = await $getdata(
"file",
{ id: link.file },
undefined,
true
).catch((error) => {
const fileData = await $getdata("file", { id: link.file }, undefined, true).catch((error) => {
console.error(`Error loading project file ${link.file}:`, error);
return null;
});
@@ -269,7 +227,7 @@ async function loadProjectFiles() {
link,
fileData,
};
})
}),
);
const validFiles = files.filter((entry) => entry.fileData);
@@ -286,9 +244,7 @@ async function loadProjectFiles() {
caption: entry.fileData.caption || "",
url: $buildFileUrl(entry.fileData.file),
file: entry.fileData.file,
size: entry.fileData.size
? $formatFileSize(Number(entry.fileData.size))
: "",
size: entry.fileData.size ? $formatFileSize(Number(entry.fileData.size)) : "",
uploaded_by: entry.fileData.creator__fullname || "Admin",
uploaded_at: entry.fileData.create_time || entry.link.create_time || "",
};
@@ -333,48 +289,44 @@ async function loadLookupNames() {
if (record.value.status && !record.value.status__name) {
promises.push(
$getdata("productstatus", { id: record.value.status }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.status__name = data.name;
}
})
$getdata("productstatus", { id: record.value.status }, undefined, true).then((data) => {
if (data && data.name) {
record.value.status__name = data.name;
}
}),
);
}
if (record.value.type && !record.value.type__name) {
promises.push(
$getdata("producttype", { id: record.value.type }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.type__name = data.name;
record.value.type__en = data.name_en || data.en || data.name;
}
})
$getdata("producttype", { id: record.value.type }, undefined, true).then((data) => {
if (data && data.name) {
record.value.type__name = data.name;
record.value.type__en = data.name_en || data.en || data.name;
}
}),
);
}
if (record.value.direction && !record.value.direction__name) {
promises.push(
$getdata("direction", { id: record.value.direction }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.direction__name = data.name;
record.value.direction__en = data.name_en || data.en || data.name;
}
})
$getdata("direction", { id: record.value.direction }, undefined, true).then((data) => {
if (data && data.name) {
record.value.direction__name = data.name;
record.value.direction__en = data.name_en || data.en || data.name;
}
}),
);
}
if (record.value.zone_type && !record.value.zone_type__name) {
promises.push(
$getdata("zonetype", { id: record.value.zone_type }, undefined, true)
.then((data) => {
if (data && data.name) {
record.value.zone_type__name = data.name;
record.value.zone_type__en = data.name_en || data.en || data.name;
}
})
$getdata("zonetype", { id: record.value.zone_type }, undefined, true).then((data) => {
if (data && data.name) {
record.value.zone_type__name = data.name;
record.value.zone_type__en = data.name_en || data.en || data.name;
}
}),
);
}
@@ -391,21 +343,16 @@ async function loadLookupNames() {
const rs = await $getapi([projectApi]);
const data = rs[0]?.data;
if (data) {
const projectName =
data.name || data.fullname || data.code || data.title;
const projectName = data.name || data.fullname || data.code || data.title;
if (projectName) {
record.value.project__name = projectName;
record.value.project__fullname =
data.fullname || data.name || projectName;
record.value.project__fullname = data.fullname || data.name || projectName;
}
}
} catch (err) {
console.error(
`Failed to load project for id ${record.value.project}:`,
err
);
console.error(`Failed to load project for id ${record.value.project}:`, err);
}
})()
})(),
);
}
@@ -418,20 +365,13 @@ async function loadLookupNames() {
async function loadFullData() {
if (!props.row?.id) return;
try {
const fullData = await $getdata(
"product",
{ id: props.row.id },
undefined,
true
);
const fullData = await $getdata("product", { id: props.row.id }, undefined, true);
record.value = fullData;
await loadLookupNames();
await loadProductFiles();
await loadProjectFiles();
await loadAllPolicyData();
} catch (error) {
console.error("Error loading full product data:", error);
record.value = props.row ? { ...props.row } : null;
@@ -450,7 +390,7 @@ watch(
await loadFullData();
}
},
{ immediate: false }
{ immediate: false },
);
watch(
@@ -463,7 +403,7 @@ watch(
projectDocuments.value = [];
}
},
{ immediate: false }
{ immediate: false },
);
onMounted(async () => {
@@ -472,7 +412,10 @@ onMounted(async () => {
</script>
<template>
<div v-if="record" class="px-5">
<div
v-if="record"
class="px-5"
>
<div class="tabs is-toggle is-fullwidth mb-4">
<ul>
<li :class="{ 'is-active': tab === 'info' }">
@@ -502,14 +445,17 @@ onMounted(async () => {
</div>
<div class="">
<div v-if="tab === 'info' && record" :id="docid">
<div
v-if="tab === 'info' && record"
:id="docid"
>
<TransactionView
pagename="ProductView"
:transactionId="product.txnprd"
:productId="product.id"
/>
</div>
<!-- Tab Hình ảnh -->
<div v-if="tab === 'image' && record">
<div class="columns is-gapless">
@@ -565,9 +511,7 @@ onMounted(async () => {
</div>
<div class="card-content p-2">
<div class="image-meta">
<p
class="has-text-weight-semibold is-size-7 is-clipped"
>
<p class="has-text-weight-semibold is-size-7 is-clipped">
{{ image.name }}
</p>
<p class="is-size-7 is-clipped">
@@ -578,7 +522,10 @@ onMounted(async () => {
</div>
</div>
</div>
<div v-else class="has-text-centered py-4">
<div
v-else
class="has-text-centered py-4"
>
<p class="">Chưa hình ảnh nào được tải lên.</p>
</div>
</div>
@@ -609,9 +556,7 @@ onMounted(async () => {
</div>
<div class="card-content p-2">
<div class="image-meta">
<p
class="has-text-weight-semibold is-size-7 is-clipped"
>
<p class="has-text-weight-semibold is-size-7 is-clipped">
{{ image.name }}
</p>
<p class="is-size-7 is-clipped">
@@ -622,7 +567,10 @@ onMounted(async () => {
</div>
</div>
</div>
<div v-else class="has-text-centered py-4">
<div
v-else
class="has-text-centered py-4"
>
<p class="">Chưa hình ảnh dự án nào được tải lên.</p>
</div>
</div>
@@ -636,16 +584,13 @@ onMounted(async () => {
@click.self="closeViewImage"
>
<div class="modal-background"></div>
<div class="modal-content" style="width: 90vw; height: 90vh">
<div
class="modal-content"
style="width: 90vw; height: 90vh"
>
<figure
class="image"
style="
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
"
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center"
>
<img
:src="viewingImage.url"
@@ -704,7 +649,10 @@ onMounted(async () => {
</tr>
</thead>
<tbody>
<tr v-for="doc in documents" :key="doc.id">
<tr
v-for="doc in documents"
:key="doc.id"
>
<td>
<div class="is-flex is-align-items-center">
<span class="icon is-medium has-text-info mr-2">
@@ -735,13 +683,7 @@ onMounted(async () => {
{{ doc.uploaded_by }}
</p>
<p class="is-size-7 has-text-grey">
{{
doc.uploaded_at
? new Date(doc.uploaded_at).toLocaleString(
"vi-VN"
)
: "-"
}}
{{ doc.uploaded_at ? new Date(doc.uploaded_at).toLocaleString("vi-VN") : "-" }}
</p>
</div>
</td>
@@ -767,10 +709,11 @@ onMounted(async () => {
</td>
</tr>
<tr v-if="documents.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>
@@ -791,7 +734,10 @@ onMounted(async () => {
</tr>
</thead>
<tbody>
<tr v-for="doc in projectDocuments" :key="doc.id">
<tr
v-for="doc in projectDocuments"
:key="doc.id"
>
<td>
<div class="is-flex is-align-items-center">
<span class="icon is-medium has-text-info mr-2">
@@ -822,13 +768,7 @@ onMounted(async () => {
{{ doc.uploaded_by }}
</p>
<p class="is-size-7 has-text-grey">
{{
doc.uploaded_at
? new Date(doc.uploaded_at).toLocaleString(
"vi-VN"
)
: "-"
}}
{{ doc.uploaded_at ? new Date(doc.uploaded_at).toLocaleString("vi-VN") : "-" }}
</p>
</div>
</td>
@@ -854,10 +794,11 @@ onMounted(async () => {
</td>
</tr>
<tr v-if="projectDocuments.length === 0">
<td colspan="4" class="has-text-centered py-4">
<p class="has-text-grey">
Chưa có tài liệu dự án 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 dự án nào được đính kèm.</p>
</td>
</tr>
</tbody>
@@ -869,11 +810,18 @@ onMounted(async () => {
</div>
<!-- Tab Chính sách tài chính -->
<div v-if="tab === 'financial-policy' && record" class="py-3">
<div v-if="isLoadingPolicies" class="has-text-centered">
<div
v-if="tab === 'financial-policy' && record"
class="py-3"
>
<div
v-if="isLoadingPolicies"
class="has-text-centered"
>
<p class="has-text-info is-italic">Đang tải chính sách...</p>
</div>
<PaymentSchedule v-else
<PaymentSchedule
v-else
:productData="record"
:policies="policies"
:activeTab="activeTab"
@@ -1052,4 +1000,4 @@ li a:hover .icon :deep(img) {
border-bottom: 1px solid #e0e0e0;
padding-bottom: 1rem;
}
</style>
</style>

View File

@@ -1,25 +1,24 @@
<script setup>
import FileUpload from '@/components/media/FileUpload.vue';
import FileUpload from "@/components/media/FileUpload.vue";
const { dealer } = useStore();
const { $buildFileUrl, $formatFileSize, $getdata, $insertapi, $patchapi, $snackbar } = useNuxtApp();
const project = await $getdata('project', undefined, undefined, true);
const project = await $getdata("project", undefined, undefined, true);
const projectDocuments = ref([]);
async function loadProjectDocuments() {
projectDocuments.value = await $getdata("projectfile", undefined, {
filter: { project: project.id, file__type: 1 } ,
values: "id,project,file,file__id,file__code,file__type,file__name,file__file,file__size,file__caption,file__user,file__user__fullname,create_time",
})
filter: { project: project.id, file__type: 1 },
values:
"id,project,file,file__id,file__code,file__type,file__name,file__file,file__size,file__caption,file__user,file__user__fullname,create_time",
});
}
loadProjectDocuments();
async function attachFilesToProject(files) {
const payload = files
.filter((file) => file && file.id)
.map((file) => ({ project: project.id, file: file.id }));
const payload = files.filter((file) => file && file.id).map((file) => ({ project: project.id, file: file.id }));
if (payload.length === 0) return 0;
const result = await $insertapi("projectfile", payload, undefined, false);
if (result === "error") {
@@ -36,18 +35,12 @@ async function onUploadedProjectDocs(payload) {
if (linked > 0) {
await loadProjectDocuments();
const message =
linked === 1
? "Đã thêm tài liệu dự án thành công"
: `Đã thêm ${linked} tài liệu dự án thành công`;
linked === 1 ? "Đã thêm tài liệu dự án thành công" : `Đã thêm ${linked} tài liệu dự án thành công`;
$snackbar(message, "Thành công", "Success");
}
} catch (error) {
console.error("Error attaching project documents:", error);
$snackbar(
"Không thể thêm tài liệu dự án, vui lòng thử lại",
"Lỗi",
"Error"
);
$snackbar("Không thể thêm tài liệu dự án, vui lòng thử lại", "Lỗi", "Error");
}
return;
}
@@ -67,9 +60,7 @@ async function onUploadedProjectDocs(payload) {
});
});
const message =
totalFiles === 1
? "Đã thêm tài liệu dự án thành công"
: `Đã thêm ${totalFiles} tài liệu dự án thành công`;
totalFiles === 1 ? "Đã thêm tài liệu dự án thành công" : `Đã thêm ${totalFiles} tài liệu dự án thành công`;
$snackbar(message, "Thành công", "Success");
event.target.value = "";
}
@@ -87,9 +78,7 @@ function editDocument(doc) {
async function saveEditDocument() {
if (!editingDocument.value) return;
const index = projectDocuments.value.findIndex(
(doc) => doc.id === editingDocument.value.id
);
const index = projectDocuments.value.findIndex((doc) => doc.id === editingDocument.value.id);
if (index !== -1) {
projectDocuments.value[index] = {
...projectDocuments.value[index],
@@ -107,7 +96,7 @@ async function saveEditDocument() {
caption: editingDocumentCaption.value?.trim() || null,
},
{},
false
false,
);
$snackbar("Đã cập nhật tài liệu thành công", "Thành công", "Success");
} catch (error) {
@@ -145,7 +134,10 @@ function cancelEditDocument() {
</tr>
</thead>
<tbody>
<template v-for="doc in projectDocuments" :key="doc.id">
<template
v-for="doc in projectDocuments"
:key="doc.id"
>
<tr v-if="editingDocument?.id === doc.id">
<td colspan="4">
<div class="columns is-multiline">
@@ -201,7 +193,7 @@ function cancelEditDocument() {
<td>
<div class="is-flex is-align-items-center">
<div>
<a
<a
:href="$buildFileUrl(doc.file__file)"
target="_blank"
class="has-text-weight-semibold"
@@ -215,9 +207,7 @@ function cancelEditDocument() {
</div>
</td>
<td>
<span class="is-size-7">{{
doc.file__caption || "-"
}}</span>
<span class="is-size-7">{{ doc.file__caption || "-" }}</span>
</td>
<td>
<div>
@@ -225,13 +215,7 @@ function cancelEditDocument() {
{{ doc.file__user__fullname }}
</p>
<p class="is-size-7 has-text-grey">
{{
doc.create_time
? new Date(doc.create_time).toLocaleString(
"vi-VN"
)
: "-"
}}
{{ doc.create_time ? new Date(doc.create_time).toLocaleString("vi-VN") : "-" }}
</p>
</div>
</td>
@@ -252,7 +236,7 @@ function cancelEditDocument() {
></SvgIcon
></span>
</a>
<a
<a
:href="$buildFileUrl(doc.file__file)"
target="_blank"
title="Tải xuống"
@@ -267,9 +251,9 @@ function cancelEditDocument() {
></SvgIcon
></span>
</a>
<a
<a
v-if="!dealer && $getEditRights()"
@click="deleteDocument(doc)"
@click="deleteDocument(doc)"
title="Xóa"
>
<span class="icon"
@@ -287,13 +271,14 @@ function cancelEditDocument() {
</tr>
</template>
<tr v-if="projectDocuments.length === 0">
<td colspan="4" class="has-text-centered py-4">
<p class="has-text-grey">
Chưa 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 tài liệu nào được đính kèm.</p>
</td>
</tr>
</tbody>
</table>
</div>
</template>
</template>