Initial commit
This commit is contained in:
234
app/components/product/Cart.vue
Normal file
234
app/components/product/Cart.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header với nút thêm và upload -->
|
||||
<div class="columns mb-0">
|
||||
<div class="column is-10">
|
||||
<!-- 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"
|
||||
:class="{ 'is-primary': selectedCarts.includes(cart.id) }"
|
||||
@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;">
|
||||
<SvgIcon v-bind="{ name: 'pen1.svg', type: 'dark', size: 16 }" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$getEditRights()" class="column is-2">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<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>
|
||||
</span>
|
||||
</a>
|
||||
<a @click="addNew()">
|
||||
<span class="icon-text">
|
||||
<SvgIcon v-bind="{ name: 'add1.png', type: 'primary', size: 23 }" />
|
||||
<span class="ml-1 fsb-17">Thêm mới</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data View với product được filter -->
|
||||
<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">
|
||||
Bỏ chọn tất cả
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DataView với key động để force reload khi filter thay đổi -->
|
||||
<DataView v-bind="dataViewConfig" :key="selectedCartsKey" />
|
||||
</div>
|
||||
|
||||
<!-- Modal Components -->
|
||||
<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'
|
||||
|
||||
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)
|
||||
|
||||
// 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('-')
|
||||
})
|
||||
|
||||
// DataView config
|
||||
const dataViewConfig = computed(() => {
|
||||
const config = {
|
||||
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" }
|
||||
}
|
||||
|
||||
if (selectedCarts.value.length > 0) {
|
||||
config.filter = { cart__in: selectedCarts.value }
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
// Modal configs
|
||||
const newAddon = {
|
||||
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
|
||||
} else if (event.shiftKey) {
|
||||
// Shift + Click: Chọn range từ last đến hiện tại
|
||||
if (lastSelectedIndex.value !== null) {
|
||||
selectRange(lastSelectedIndex.value, index)
|
||||
} else {
|
||||
toggleCart(cartId)
|
||||
lastSelectedIndex.value = index
|
||||
}
|
||||
} else {
|
||||
// Click bình thường: Chọn chỉ cái này
|
||||
selectedCarts.value = [cartId]
|
||||
lastSelectedIndex.value = index
|
||||
}
|
||||
}
|
||||
|
||||
function selectRange(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
|
||||
if (!selectedCarts.value.includes(cartId)) {
|
||||
selectedCarts.value.push(cartId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCart(cartId) {
|
||||
const index = selectedCarts.value.indexOf(cartId)
|
||||
if (index > -1) {
|
||||
selectedCarts.value.splice(index, 1)
|
||||
} else {
|
||||
selectedCarts.value.push(cartId)
|
||||
}
|
||||
}
|
||||
|
||||
function removeCart(cartId) {
|
||||
const index = selectedCarts.value.indexOf(cartId)
|
||||
if (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}` : ''
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
if (selectedCarts.value.length === 0) {
|
||||
alert('Vui lòng chọn ít nhất một giỏ hàng')
|
||||
return
|
||||
}
|
||||
showCartList.value = false
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
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' }
|
||||
}
|
||||
}
|
||||
|
||||
function addNew() {
|
||||
showmodal.value = {
|
||||
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',
|
||||
vbind: {
|
||||
id: cartId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dataevent() {
|
||||
loadCarts()
|
||||
}
|
||||
|
||||
async function loadCarts() {
|
||||
try {
|
||||
const conn = $findapi('cart')
|
||||
const rs = await $getapi([conn])
|
||||
const obj = $find(rs, { name: 'cart' })
|
||||
if (obj) {
|
||||
carts.value = $copy(obj.data.rows || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Lỗi khi fetch giỏ hàng:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
onMounted(() => {
|
||||
loadCarts()
|
||||
})
|
||||
</script>
|
||||
457
app/components/product/Product.vue
Normal file
457
app/components/product/Product.vue
Normal file
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline mx-1">
|
||||
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">Sản phẩm</label>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'product',
|
||||
field: 'trade_code',
|
||||
column: ['trade_code'],
|
||||
optionid: props.row.trade_code,
|
||||
first: true,
|
||||
disabled: true,
|
||||
viewaddon: productViewAddon,
|
||||
}"
|
||||
@option="selected('product', $event)"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'transactionphase',
|
||||
field: 'name',
|
||||
column: ['name'],
|
||||
optionid: transaction?.phase,
|
||||
first: true,
|
||||
viewaddon: transactionTypeViewAddon,
|
||||
}"
|
||||
@option="selected('phase', $event)"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.phase">
|
||||
{{ errors.phase }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="control">
|
||||
<SearchBox
|
||||
v-bind="{
|
||||
api: 'customer',
|
||||
field: 'label',
|
||||
column: ['fullname', 'phone'],
|
||||
first: true,
|
||||
optionid: transaction?.customer,
|
||||
addon: customerAddon,
|
||||
viewaddon: customerViewAddon,
|
||||
}"
|
||||
@option="selected('customer', $event)"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.customer">
|
||||
{{ errors.customer }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="control">
|
||||
<InputNumber
|
||||
v-bind="{
|
||||
record: formData,
|
||||
attr: 'amount',
|
||||
defaultValue: true,
|
||||
}"
|
||||
@number="selected('amount', $event)"
|
||||
></InputNumber>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errors.amount">
|
||||
{{ errors.amount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
>
|
||||
<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">
|
||||
{{ errors.due_date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="`column px-0 is-full ${viewport === 1 ? 'px-0 pb-1' : ''}`">
|
||||
<div class="field">
|
||||
<label class="label">{{
|
||||
dataLang && findFieldName("note")[lang]
|
||||
}}</label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
v-model="formData.note"
|
||||
class="textarea"
|
||||
name="note"
|
||||
placeholder=""
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<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' : ''
|
||||
}`"
|
||||
@click="handleSubmitData"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
{{ isVietnamese ? "Tạo giao dịch" : "Create Transaction" }}
|
||||
</button>
|
||||
|
||||
<!-- Nút xem hợp đồng - chỉ hiện khi có contract -->
|
||||
<button
|
||||
v-if="contractData"
|
||||
class="button is-info ml-3"
|
||||
@click="openContractModal"
|
||||
>
|
||||
<span>{{ isVietnamese ? "Xem hợp đồng" : "View Contract" }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal hiển thị hợp đồng -->
|
||||
<Modal
|
||||
v-if="showContractModal"
|
||||
@close="showContractModal = false"
|
||||
@dataevent="handleContractUpdated"
|
||||
v-bind="contractModalConfig"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { isEqual, pick } from "es-toolkit";
|
||||
import dayjs from "dayjs";
|
||||
import { useStore } from "~/stores/index";
|
||||
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
api: String,
|
||||
});
|
||||
|
||||
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
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const viewport = 5;
|
||||
const store = useStore();
|
||||
const lang = computed(() => store.lang);
|
||||
const isVietnamese = computed(() => lang.value === "vi");
|
||||
const dataLang = ref(store.common);
|
||||
const isSubmitting = ref(false);
|
||||
const contractData = ref(null);
|
||||
const showContractModal = ref(false);
|
||||
|
||||
// Load contract nếu đã có
|
||||
if (transaction) {
|
||||
contractData.value = await $getdata(
|
||||
"contract",
|
||||
{ transaction: transaction.id },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const initFormData = {
|
||||
product: props.row.id,
|
||||
phase: transaction?.phase,
|
||||
customer: transaction?.customer,
|
||||
amount: reservation?.amount,
|
||||
due_date: reservation?.due_date,
|
||||
note: props.row.note,
|
||||
};
|
||||
|
||||
const formData = ref({ ...initFormData });
|
||||
watch(formData, (val) => console.log(toRaw(val)), { deep: true });
|
||||
|
||||
const errors = ref({});
|
||||
|
||||
const productViewAddon = {
|
||||
component: "product/ProductView",
|
||||
width: "55%",
|
||||
height: "600px",
|
||||
title: lang === "en" ? "Product" : "Sản phẩm",
|
||||
};
|
||||
|
||||
const transactionTypeViewAddon = {
|
||||
component: "transaction/TransactionTypeView",
|
||||
width: "40%",
|
||||
height: "100px",
|
||||
title: lang === "en" ? "Transaction Type" : "Loại giao dịch",
|
||||
};
|
||||
|
||||
const customerViewAddon = {
|
||||
component: "customer/CustomerView",
|
||||
width: "75%",
|
||||
height: "600px",
|
||||
title: lang === "en" ? "Customer" : "Khách hàng",
|
||||
};
|
||||
|
||||
const customerAddon = {
|
||||
component: "customer/Customer",
|
||||
width: "75%",
|
||||
height: "400px",
|
||||
title: isVietnamese.value ? "Tạo khách hàng" : "Add customer",
|
||||
};
|
||||
|
||||
// Config cho modal hợp đồng
|
||||
const contractModalConfig = computed(() => ({
|
||||
component: "application/Contract",
|
||||
title: isVietnamese.value ? "Hợp đồng" : "Contract",
|
||||
width: "90%",
|
||||
height: "90vh",
|
||||
vbind: {
|
||||
row: {
|
||||
id: contractData.value?.transaction,
|
||||
...contractData.value,
|
||||
},
|
||||
api: "transaction",
|
||||
},
|
||||
event: "contractUpdated",
|
||||
eventname: "dataevent",
|
||||
}));
|
||||
|
||||
const findFieldName = (code) => {
|
||||
let field = dataLang.value.find((v) => v.code === code);
|
||||
return field;
|
||||
};
|
||||
|
||||
const selected = (fieldName, value) => {
|
||||
const finalValue =
|
||||
value !== null && typeof value === "object"
|
||||
? value.id || value.index || value.code || value.label
|
||||
: value;
|
||||
formData.value[fieldName] = finalValue;
|
||||
};
|
||||
|
||||
const checkErrors = (fields) => {
|
||||
const { phase, customer, amount, due_date } = fields;
|
||||
errors.value = {};
|
||||
|
||||
if ($empty(phase)) {
|
||||
errors.value.phase = isVietnamese.value
|
||||
? "Giai đoạn giao dịch không được để trống"
|
||||
: "Transaction phase is required";
|
||||
}
|
||||
|
||||
if ($empty(customer)) {
|
||||
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";
|
||||
}
|
||||
|
||||
if ($empty(due_date)) {
|
||||
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;
|
||||
};
|
||||
|
||||
async function handleSubmitData() {
|
||||
try {
|
||||
if (isSubmitting.value) return;
|
||||
|
||||
if (isEqual(formData.value, initFormData)) {
|
||||
$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."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
// 1. Tạo transaction
|
||||
const transactionPayload = {
|
||||
...pick(formData.value, ["product", "phase", "customer"]),
|
||||
date: dayjs().format("YYYY-MM-DD"),
|
||||
};
|
||||
|
||||
const transactionRs = await $insertapi(
|
||||
"transaction",
|
||||
transactionPayload,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
const { date, id: transactionId, code: transactionCode } = transactionRs;
|
||||
|
||||
// 2. Tạo reservation
|
||||
const reservationPayload = {
|
||||
amount: Number(formData.value.amount),
|
||||
due_date: formData.value.due_date,
|
||||
creator: 1,
|
||||
date:dayjs().format("YYYY-MM-DD"),
|
||||
transaction: transactionId,
|
||||
};
|
||||
|
||||
const reservationnRs = await $insertapi(
|
||||
"reservation",
|
||||
reservationPayload,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
const reservationId = reservationnRs.id;
|
||||
|
||||
// 3. Generate document
|
||||
const documents = [];
|
||||
|
||||
try {
|
||||
const docResult = await $generateDocument({
|
||||
doc_code: "PXLTT1",
|
||||
customer_id: formData.value.customer,
|
||||
investor_id: 1,
|
||||
product_id: formData.value.product,
|
||||
reservation: reservationId,
|
||||
output_filename: `hop_dong_${transactionCode}`,
|
||||
});
|
||||
|
||||
if (docResult.success && docResult.data) {
|
||||
documents.push({
|
||||
code: docResult.data.code,
|
||||
name: docResult.data.name,
|
||||
en: docResult.data.name,
|
||||
file: docResult.data.file,
|
||||
pdf: docResult.data.pdf,
|
||||
});
|
||||
} else {
|
||||
console.error("Generate document failed:", docResult.error);
|
||||
}
|
||||
} catch (docError) {
|
||||
console.error("Lỗi khi tạo tài liệu:", docError);
|
||||
}
|
||||
|
||||
// 4. Tạo contract record
|
||||
if (documents.length > 0) {
|
||||
const contractPayload = {
|
||||
transaction: transactionId,
|
||||
document: documents,
|
||||
link: crypto.randomUUID(),
|
||||
signature: null,
|
||||
status: 1,
|
||||
user: null,
|
||||
};
|
||||
|
||||
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!"
|
||||
);
|
||||
|
||||
// Tự động mở modal xem hợp đồng
|
||||
if (contractData.value) {
|
||||
showContractModal.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Create transaction failed:", error);
|
||||
$snackbar(
|
||||
isVietnamese.value
|
||||
? "Tạo giao dịch thất bại"
|
||||
: "Create transaction failed"
|
||||
);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openContractModal() {
|
||||
showContractModal.value = true;
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
1530
app/components/product/ProductEdit.vue
Normal file
1530
app/components/product/ProductEdit.vue
Normal file
File diff suppressed because it is too large
Load Diff
1055
app/components/product/ProductView.vue
Normal file
1055
app/components/product/ProductView.vue
Normal file
File diff suppressed because it is too large
Load Diff
304
app/components/product/ProjectDocuments.vue
Normal file
304
app/components/product/ProjectDocuments.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<script setup>
|
||||
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 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",
|
||||
})
|
||||
}
|
||||
|
||||
loadProjectDocuments();
|
||||
|
||||
async function attachFilesToProject(files) {
|
||||
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") {
|
||||
throw new Error("Không thể liên kết tệp với dự án");
|
||||
}
|
||||
return payload.length;
|
||||
}
|
||||
|
||||
async function onUploadedProjectDocs(payload) {
|
||||
const uploadedFiles = Array.isArray(payload) ? payload : null;
|
||||
if (uploadedFiles && uploadedFiles.length > 0) {
|
||||
try {
|
||||
const linked = await attachFilesToProject(uploadedFiles);
|
||||
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`;
|
||||
$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"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const event = payload?.target ? payload : null;
|
||||
if (!event) return;
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const totalFiles = files.length;
|
||||
Array.from(files).forEach((file) => {
|
||||
projectDocuments.value.push({
|
||||
id: Date.now() + Math.random(),
|
||||
name: file.name,
|
||||
size: $formatFileSize(file.size),
|
||||
uploaded_by: store.login?.fullname || "Admin",
|
||||
});
|
||||
});
|
||||
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`;
|
||||
$snackbar(message, "Thành công", "Success");
|
||||
event.target.value = "";
|
||||
}
|
||||
|
||||
const editingDocument = ref(null);
|
||||
const editingDocumentName = ref("");
|
||||
const editingDocumentCaption = ref("");
|
||||
|
||||
// Sửa tài liệu
|
||||
function editDocument(doc) {
|
||||
editingDocument.value = doc;
|
||||
editingDocumentName.value = doc.file__name;
|
||||
editingDocumentCaption.value = doc.file__caption || "";
|
||||
}
|
||||
|
||||
async function saveEditDocument() {
|
||||
if (!editingDocument.value) return;
|
||||
const index = projectDocuments.value.findIndex(
|
||||
(doc) => doc.id === editingDocument.value.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
projectDocuments.value[index] = {
|
||||
...projectDocuments.value[index],
|
||||
name: editingDocumentName.value,
|
||||
caption: editingDocumentCaption.value,
|
||||
};
|
||||
const fileId = editingDocument.value.fileId || editingDocument.value.file;
|
||||
if (fileId) {
|
||||
try {
|
||||
await $patchapi(
|
||||
"file",
|
||||
{
|
||||
id: fileId,
|
||||
name: editingDocumentName.value,
|
||||
caption: editingDocumentCaption.value?.trim() || null,
|
||||
},
|
||||
{},
|
||||
false
|
||||
);
|
||||
$snackbar("Đã cập nhật tài liệu thành công", "Thành công", "Success");
|
||||
} catch (error) {
|
||||
console.error("Error updating document metadata:", error);
|
||||
$snackbar("Cập nhật tài liệu thất bại", "Lỗi", "Error");
|
||||
}
|
||||
}
|
||||
}
|
||||
cancelEditDocument();
|
||||
}
|
||||
|
||||
function cancelEditDocument() {
|
||||
editingDocument.value = null;
|
||||
editingDocumentName.value = "";
|
||||
editingDocumentCaption.value = "";
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<FileUpload
|
||||
v-if="!dealer && $getEditRights()"
|
||||
position="right"
|
||||
:type="['pdf', 'file']"
|
||||
@files="onUploadedProjectDocs"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Mô tả</th>
|
||||
<th>Người tải lên</th>
|
||||
<th>Chức năng</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="doc in projectDocuments" :key="doc.id">
|
||||
<tr v-if="editingDocument?.id === doc.id">
|
||||
<td colspan="4">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Tên tài liệu</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="editingDocumentName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label">Mô tả</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="editingDocumentCaption"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field">
|
||||
<label class="label"> </label>
|
||||
<div class="control">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="button has-text-white is-primary"
|
||||
@click="saveEditDocument"
|
||||
>
|
||||
Lưu
|
||||
</button>
|
||||
<button
|
||||
class="button is-danger has-text-white"
|
||||
@click="cancelEditDocument"
|
||||
>
|
||||
Hủy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<div class="is-flex is-align-items-center">
|
||||
<div>
|
||||
<a
|
||||
:href="$buildFileUrl(doc.file__file)"
|
||||
target="_blank"
|
||||
class="has-text-weight-semibold"
|
||||
>
|
||||
{{ doc.file__name }}
|
||||
</a>
|
||||
<p class="is-size-7 has-text-grey">
|
||||
{{ $formatFileSize(doc.file__size) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="is-size-7">{{
|
||||
doc.file__caption || "-"
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<p class="is-size-7 has-text-weight-semibold">
|
||||
{{ doc.file__user__fullname }}
|
||||
</p>
|
||||
<p class="is-size-7 has-text-grey">
|
||||
{{
|
||||
doc.create_time
|
||||
? new Date(doc.create_time).toLocaleString(
|
||||
"vi-VN"
|
||||
)
|
||||
: "-"
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<a
|
||||
v-if="!dealer && $getEditRights()"
|
||||
@click="editDocument(doc)"
|
||||
title="Sửa"
|
||||
>
|
||||
<span class="icon"
|
||||
><SvgIcon
|
||||
v-bind="{
|
||||
name: 'edit.svg',
|
||||
type: 'primary',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon
|
||||
></span>
|
||||
</a>
|
||||
<a
|
||||
:href="$buildFileUrl(doc.file__file)"
|
||||
target="_blank"
|
||||
title="Tải xuống"
|
||||
>
|
||||
<span class="icon"
|
||||
><SvgIcon
|
||||
v-bind="{
|
||||
name: 'download.svg',
|
||||
type: 'success',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon
|
||||
></span>
|
||||
</a>
|
||||
<a
|
||||
v-if="!dealer && $getEditRights()"
|
||||
@click="deleteDocument(doc)"
|
||||
title="Xóa"
|
||||
>
|
||||
<span class="icon"
|
||||
><SvgIcon
|
||||
v-bind="{
|
||||
name: 'bin1.svg',
|
||||
type: 'primary',
|
||||
size: 18,
|
||||
}"
|
||||
></SvgIcon
|
||||
></span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<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 nào được đính kèm.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.table th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user