feat: quotation payment method
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s

This commit is contained in:
puriphatt 2025-09-11 12:53:13 +07:00
parent c749e91ffa
commit a7392e44b4
5 changed files with 594 additions and 467 deletions

View file

@ -54,10 +54,11 @@ onMounted(() => {
@click="$emit('click')"
>
<q-icon :name="icon" size="lg" :style="`color: ${color}`" />
<article class="col column q-pl-md">
<span class="ellipsis full-width">
<div class="col column q-pl-md">
<div class="ellipsis full-width" style="max-width: 65vw !important">
{{ name }}
</span>
</div>
<span class="text-caption app-text-muted-2">
{{
uploading.loaded
@ -79,7 +80,7 @@ onMounted(() => {
/>
{{ idle ? `Pending` : progress !== 1 ? `Uploading...` : 'Completed' }}
</span>
</article>
</div>
<q-btn
v-if="closeable"
icon="mdi-close"

View file

@ -777,6 +777,8 @@ export default {
seller: 'Seller',
paymentChannels: 'Payment Channels',
channelsThat: 'Channels That',
refNo: 'Reference Number',
bankAccount: 'Bank Account',
bankAccountNumber: 'Bank Account Number',
bankAccountName: 'Bank Account Name',
inTheNameOf: 'In The Name Of',

View file

@ -775,6 +775,8 @@ export default {
seller: 'ผู้ขาย',
paymentChannels: 'ช่องทางชำระเงิน',
channelsThat: 'ช่องทางที่',
refNo: 'เลขที่อ้างอิง',
bankAccount: 'บัญชีธนาคาร',
bankAccountNumber: 'เลขบัญชีธนาคาร',
bankAccountName: 'ชื่อบัญชี',
inTheNameOf: 'ในนาม',

View file

@ -3,7 +3,8 @@ import { baseUrl, dialog } from 'stores/utils';
import { storeToRefs } from 'pinia';
import { useConfigStore } from 'stores/config';
import { formatNumberDecimal } from 'stores/utils';
import { MainButton } from 'components/button';
import { SaveButton, EditButton, UndoButton } from 'components/button';
import SelectInput from 'src/components/shared/SelectInput.vue';
import { useI18n } from 'vue-i18n';
@ -38,8 +39,15 @@ const firstCodePayment = defineModel<string>('firstCodePayment');
const refQFile = ref<InstanceType<typeof QFile>[]>([]);
const refQMenu = ref<InstanceType<typeof QMenu>[]>([]);
const paymentData = ref<QuotationPaymentData[]>([]);
const submitPaymentData = ref<QuotationPaymentData[]>([]);
const payAll = ref<boolean>(false);
const formPaymentMethod = ref<
{
id: string;
channel: string | null;
reference: string | null;
account: string | null;
isEdit?: boolean;
}[]
>([]);
const paymentStatusOpts = [
{
status: 'PaymentInProcess',
@ -188,18 +196,29 @@ async function selectStatus(
refQMenu.value[index].hide();
}
async function triggerSubmit() {
submitPaymentData.value.forEach(async (p) => {
async function triggerSubmit(id: string) {
const index = paymentData.value.findIndex((p) => p.id === id);
if (index === -1) return;
const p = paymentData.value[index];
const payload: PaymentPayload = {
paymentStatus: p.paymentStatus,
date: new Date(p.date),
amount: p.amount,
account: formPaymentMethod.value[index].account,
channel: formPaymentMethod.value[index].channel,
reference: formPaymentMethod.value[index].reference,
};
await quotationPayment.updateQuotationPayment(p.id, payload);
});
formPaymentMethod.value[index].isEdit = false;
setTimeout(async () => {
await fetchData();
}, 300);
}
onMounted(async () => {
async function fetchData() {
if (!prop.data) return;
const ret = await quotationPayment.getQuotationPayment({
quotationId: prop.isDebitNote === true ? undefined : prop.data.id,
@ -209,6 +228,13 @@ onMounted(async () => {
});
if (ret) {
paymentData.value = ret.result;
formPaymentMethod.value = ret.result.map((p, i) => ({
id: p.id,
channel: p.channel,
reference: p.reference,
account: p.account,
isEdit: formPaymentMethod.value[i]?.isEdit || false,
}));
slipFile.value = paymentData.value.map((v, i) => {
if (i === 0) {
firstCodePayment.value = v.code;
@ -219,17 +245,14 @@ onMounted(async () => {
};
});
}
}
onMounted(async () => {
await fetchData();
});
</script>
<template>
<q-form
greedy
@submit.prevent
@validation-success="triggerSubmit"
class="column full-height"
v-if="data"
>
<div class="col column no-wrap">
<div class="column no-wrap">
<!-- PRICE DETAIL -->
<section class="bordered rounded surface-1" style="overflow: hidden">
<q-expansion-item
@ -293,10 +316,8 @@ onMounted(async () => {
<span class="q-ml-auto" style="color: var(--foreground)">
฿
{{
formatNumberDecimal(
data.totalPrice - data.totalDiscount,
2,
) || '0.00'
formatNumberDecimal(data.totalPrice - data.totalDiscount, 2) ||
'0.00'
}}
</span>
</span>
@ -487,7 +508,7 @@ onMounted(async () => {
<!-- payment item -->
<section class="row">
<div
v-for="(p, i) in paymentData"
v-for="(payment, i) in paymentData"
:key="i"
class="bordered rounded surface-1 q-mb-md col-12"
style="overflow: hidden"
@ -496,7 +517,7 @@ onMounted(async () => {
hide-expand-icon
v-model="state.payExpansion[i]"
header-style="padding:0px"
@before-show="getSlipList(p, i)"
@before-show="getSlipList(payment, i)"
>
<template v-slot:header>
<div class="column full-width">
@ -505,7 +526,7 @@ onMounted(async () => {
>
<span class="text-weight-medium column">
{{ $t('quotation.periodNo') }} {{ i + 1 }}
{{ monthDisplay(p.createdAt) }}
{{ monthDisplay(payment.createdAt) }}
<!-- {{ monthDisplay(p.date) }} -->
<!-- <span -->
<!-- class="text-caption app-text-muted-2" -->
@ -529,25 +550,29 @@ onMounted(async () => {
padding="4px 8px"
class="rounded text-capitalize text-weight-regular payment-wait row items-center"
:class="{
'payment-pending': p.paymentStatus === 'PaymentWait',
'payment-pending':
payment.paymentStatus === 'PaymentWait',
'payment-process':
p.paymentStatus === 'PaymentInProcess',
'payment-retry': p.paymentStatus === 'PaymentRetry',
payment.paymentStatus === 'PaymentInProcess',
'payment-retry':
payment.paymentStatus === 'PaymentRetry',
'payment-success':
p.paymentStatus === 'PaymentSuccess',
payment.paymentStatus === 'PaymentSuccess',
}"
>
<q-icon
size="xs"
:name="
paymentStatusOpts.find(
(s) => s.status === p.paymentStatus,
(s) => s.status === payment.paymentStatus,
)?.icon || 'mdi-hand-coin-outline'
"
class="q-pr-sm"
/>
<span>
{{ $t(`quotation.receiptDialog.${p.paymentStatus}`) }}
{{
$t(`quotation.receiptDialog.${payment.paymentStatus}`)
}}
</span>
<q-icon
v-if="!readonly"
@ -568,16 +593,16 @@ onMounted(async () => {
<q-item
:id="`btn-payment-${opts.status}`"
v-if="
(p.paymentStatus === 'PaymentWait' &&
(payment.paymentStatus === 'PaymentWait' &&
opts.status !== 'PaymentRetry') ||
(p.paymentStatus === 'PaymentInProcess' &&
(payment.paymentStatus === 'PaymentInProcess' &&
opts.status !== 'PaymentInProcess') ||
(p.paymentStatus === 'PaymentRetry' &&
(payment.paymentStatus === 'PaymentRetry' &&
opts.status !== 'PaymentRetry')
"
clickable
class="row items-center"
@click="selectStatus(p, opts.status, i)"
@click="selectStatus(payment, opts.status, i)"
>
<q-icon
:name="opts.icon"
@ -599,7 +624,7 @@ onMounted(async () => {
class="q-px-sm q-ml-auto"
style="color: var(--foreground)"
>
{{ formatNumberDecimal(p.amount, 2) }}
{{ formatNumberDecimal(payment.amount, 2) }}
</span>
<q-btn
dense
@ -608,15 +633,105 @@ onMounted(async () => {
padding="0"
id="btn-show-file"
:icon="`mdi-chevron-${state.payExpansion[i] ? 'down' : 'up'}`"
@click.stop="
state.payExpansion[i] = !state.payExpansion[i]
"
@click.stop="state.payExpansion[i] = !state.payExpansion[i]"
/>
</section>
</div>
</template>
<!-- วิธีการรับชำระ -->
<section
class="q-px-md q-py-xs text-weight-medium row items-center"
style="background-color: hsla(var(--info-bg) / 0.1)"
>
{{ $t('quotation.receiptDialog.paymentMethod') }}
</section>
<div
class="surface-2 q-px-md q-py-sm row q-col-gutter-sm items-center"
>
<SelectInput
id="input-payment-channel"
for="input-payment-channel"
:readonly="
readonly ||
(!formPaymentMethod[i].isEdit && !!payment.channel)
"
v-model="formPaymentMethod[i].channel"
class="col-md-2 col-6"
:option="[
{
label: $t('creditNote.label.Cash'),
value: 'Cash',
},
{
label: $t('creditNote.label.BankTransfer'),
value: 'BankTransfer',
},
]"
:label="$t('quotation.receiptDialog.paymentMethod')"
@update:model-value="
() => {
if (formPaymentMethod[i].channel === 'Cash') {
formPaymentMethod[i].reference = null;
formPaymentMethod[i].account = null;
}
}
"
/>
<q-input
v-if="formPaymentMethod[i].channel === 'BankTransfer'"
dense
outlined
class="col-md-3 col-6"
v-model="formPaymentMethod[i].reference"
:readonly="
readonly ||
(!formPaymentMethod[i].isEdit && !!payment.channel)
"
:label="$t('quotation.refNo')"
/>
<q-input
v-if="formPaymentMethod[i].channel === 'BankTransfer'"
dense
outlined
class="col"
v-model="formPaymentMethod[i].account"
:readonly="
readonly ||
(!formPaymentMethod[i].isEdit && !!payment.channel)
"
:label="$t('quotation.bankAccount')"
/>
<div class="q-ml-auto">
<UndoButton
v-if="formPaymentMethod[i].isEdit"
icon-only
@click="
() => {
formPaymentMethod[i].isEdit = false;
formPaymentMethod[i].channel = payment.channel;
formPaymentMethod[i].reference = payment.reference;
formPaymentMethod[i].account = payment.account;
}
"
/>
<SaveButton
v-if="!payment.channel || formPaymentMethod[i].isEdit"
icon-only
@click="triggerSubmit(payment.id)"
/>
<EditButton
v-if="
payment.channel && formPaymentMethod[i].isEdit === false
"
icon-only
@click="formPaymentMethod[i].isEdit = true"
/>
</div>
</div>
<!-- อัปโหลดใบเสร็จ -->
<section
class="q-px-md q-py-xs text-weight-medium row items-center"
style="background-color: hsla(var(--info-bg) / 0.1)"
>
@ -625,7 +740,7 @@ onMounted(async () => {
msg: $t('quotation.receiptDialog.slip'),
})
}}
</div>
</section>
<div class="surface-2 q-pa-md">
<div
class="upload-section column rounded q-py-md full-height items-center justify-center no-wrap"
@ -652,7 +767,9 @@ onMounted(async () => {
ref="refQFile"
v-show="false"
v-model="slipFile[i].file"
@update:model-value="triggerUpload(p, i, slipFile[i].file)"
@update:model-value="
triggerUpload(payment, i, slipFile[i].file)
"
/>
</div>
</div>
@ -672,12 +789,12 @@ onMounted(async () => {
:name="d.name"
:progress="d.progress"
:uploading="{ loaded: d.loaded, total: d.total }"
:url="`/quotation/${data.id}/payment/${p.id}/attachment/${d.name}`"
:url="`/payment/${payment.id}/attachment/${d.name}`"
icon="mdi-file-image-outline"
color="hsl(var(--text-mute))"
clickable
@click="triggerViewSlip(p, d.name)"
@close="triggerDelete(p, d.name, i)"
@click="triggerViewSlip(payment, d.name)"
@close="triggerDelete(payment, d.name, i)"
/>
</div>
</section>
@ -686,7 +803,6 @@ onMounted(async () => {
</section>
</section>
</div>
</q-form>
</template>
<style scoped>
.bg-color-orange {

View file

@ -417,6 +417,9 @@ export type QuotationPaymentData = {
date: string;
id: string;
code: string;
account?: string;
channel?: string;
reference?: string;
};
export type Details = {
@ -435,6 +438,9 @@ export type PaymentPayload = {
paymentStatus: string;
date: Date;
amount: number;
account?: string;
channel?: string;
reference?: string;
};
export type ProductServiceList = {