Initial commit

This commit is contained in:
Viet An
2026-03-02 09:45:33 +07:00
commit d17a9e2588
415 changed files with 92113 additions and 0 deletions

View File

@@ -0,0 +1,290 @@
<template>
<div class="content-transaction-invoice">
<div class="container is-fluid px-4">
<div class="columns">
<div class="column">
<label class="label">Link<b class="ml-1 has-text-danger">*</b></label>
</div>
<div class="column is-2">
<label class="label"> tra cứu<b class="ml-1 has-text-danger">*</b></label>
</div>
<div class="column is-2">
<label class="label">Số tiền<b class="ml-1 has-text-danger">*</b></label>
</div>
<div class="column is-2">
<label class="label">Loại tiền<b class="ml-1 has-text-danger">*</b></label>
</div>
<div class="column is-1"></div>
</div>
<div class="columns" v-for="(invoice, index) in invoices">
<div class="column">
<input
class="input has-text-centered has-text-weight-bold has-text-left"
type="text"
placeholder="Nhập link tra cứu"
v-model="invoice.link"
@blur="
validateField({
value: invoice.link,
type: 'link',
index,
field: 'errorLink',
})
"
/>
<p v-if="invoice.errorLink" class="help is-danger">Link phải bắt đầu bằng https</p>
</div>
<div class="column is-2">
<input
class="input has-text-centered has-text-weight-bold has-text-left"
type="text"
placeholder="Nhập mã tra cứu"
v-model="invoice.ref_code"
@blur="
validateField({
value: invoice.ref_code,
type: 'text',
index,
field: 'errorCode',
})
"
/>
<p v-if="invoice.errorCode" class="help is-danger"> tra cứu không được bỏ trống</p>
</div>
<div class="column is-2">
<input
class="input has-text-centered has-text-weight-bold has-text-right"
type="number"
placeholder="Số tiền"
v-model="invoice.amount"
@blur="
validateField({
value: invoice.amount,
type: 'text',
index,
field: 'errorAmount',
})
"
/>
<p v-if="invoice.errorAmount" class="help is-danger">Số tiền không được bỏ trống</p>
</div>
<div class="column is-2">
<select
v-model="invoice.note"
style="width: 100%; height: var(--bulma-control-height)"
@blur="
validateField({
value: invoice.note,
type: 'text',
index,
field: 'errorType',
})
"
>
<option value="principal">Tiền gốc</option>
<option value="interest">Tiền lãi</option>
</select>
<p v-if="invoice.errorType" class="help is-danger">Loại tiền không được bỏ trống</p>
</div>
<div class="column is-narrow is-1">
<label class="label" v-if="i === 0">&nbsp;</label>
<div class="buttons is-gap-0.5 is-flex-wrap-nowrap are-small" style="height: 40px">
<button class="button is-dark" @click="handlerRemove(index)">
<span class="icon">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
</button>
<button class="button is-dark" @click="add()">
<span class="icon">
<SvgIcon v-bind="{ name: 'add4.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
</button>
<a class="button is-dark" :href="invoice.link" target="_blank">
<span class="icon">
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 20 }"></SvgIcon>
</span>
</a>
</div>
</div>
</div>
<div class="mt-5 buttons is-right">
<button class="button" @click="emit('close')">{{ isVietnamese ? "Hủy" : "Cancel" }}</button>
<button class="button is-primary" @click="handlerUpdate">{{ isVietnamese ? "Lưu lại" : "Save" }}</button>
</div>
</div>
</div>
</template>
<script setup>
const { $snackbar, $getdata, $insertapi, $store, $updateapi, $deleteapi, $formatNumber } = useNuxtApp();
const isVietnamese = computed(() => $store.lang.toLowerCase() === "vi");
const invoices = ref([{}]);
const delInvoices = ref([]);
const emit = defineEmits(["close"]);
const props = defineProps({
row: Object,
});
const resInvoice = await $getdata("invoice", { payment: props.row.id }, undefined, false);
const validateField = ({ value, type = "text", index, field }) => {
if (index < 0 || index >= invoices.value.length) return false;
const val = value?.toString().trim();
let isInvalid = false;
// 1. Không được bỏ trống (áp dụng cho tất cả)
if (!val) {
isInvalid = true;
}
// 2. Validate theo type
if (!isInvalid && type === "link") {
isInvalid = !/^https:\/\//.test(val);
}
// set lỗi
invoices.value[index][field] = isInvalid;
return !isInvalid;
};
if (resInvoice.length) {
const error = {
errorLink: false,
errorCode: false,
errorAmount: false,
errorType: false,
};
const formatData = resInvoice.map((invoice) => ({ ...invoice, amount: $formatNumber(invoice.amount), ...error }));
invoices.value = formatData;
}
const add = () => invoices.value.push({});
const validateAll = () => {
const errors = [];
invoices.value.forEach((inv, index) => {
const checks = [
{
value: inv.link,
type: "link",
field: "errorLink",
label: "Link",
},
{
value: inv.ref_code,
type: "number",
field: "errorCode",
label: "Mã tham chiếu",
},
{
value: inv.amount,
type: "number",
field: "errorAmount",
label: "Số tiền",
},
{
value: inv.note,
type: "number",
field: "errorType",
label: "Số tiền",
},
];
checks.forEach(({ value, type, field, label }) => {
const isValid = validateField({
value,
type,
index,
field,
});
if (!isValid) {
errors.push({
index,
field,
label,
});
}
});
});
return {
valid: errors.length === 0,
errors,
};
};
const handlerUpdate = async () => {
try {
// 1. Insert / Update
if (!validateAll()?.valid) {
$snackbar("Dữ liệu chưa hợp lệ");
return;
}
for (const invoice of invoices.value) {
let res;
if (invoice.id) {
res = await $updateapi("invoice", invoice, undefined, false);
} else {
const dataSend = {
...invoice,
payment: props.row.id,
};
res = await $insertapi("invoice", dataSend, undefined, false);
}
if (!res || res === "error") {
throw new Error("Save invoice failed");
}
}
// 2. Delete
for (const id of delInvoices.value) {
const res = await $deleteapi("invoice", id);
if (!res || res === "error") {
throw new Error("Delete invoice failed");
}
}
$snackbar("Lưu hóa đơn thành công");
emit("close");
} catch (err) {
console.error(err);
$snackbar("Có lỗi khi lưu hóa đơn");
}
};
const handlerRemove = (index) => {
if (index < 0 || index >= invoices.value.length) return;
const [removed] = invoices.value.splice(index, 1);
if (removed?.id && !delInvoices.value.includes(removed.id)) {
delInvoices.value.push(removed.id);
}
};
</script>
<style>
.content-transaction-invoice input,
select {
border-radius: 5px;
border-color: gray;
}
.content-transaction-invoice input[type="number"]::-webkit-inner-spin-button,
.content-transaction-invoice input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.content-transaction-invoice input[type="number"] {
-moz-appearance: textfield;
}
</style>