feat: adjust payment page (#31)
* feat: add installment no label * feat: update types * refactor: add i18n * refactor: view receipt * refactor: add type * refactor: add dateFormatTh * fixup! refactor: add i18n * fixup! refactor: add dateFormatTh * refactor: use dateFormatJS in monthDisplay * refactor: handle year th-TH * ลบ log * refactor: handle color view mod
This commit is contained in:
parent
e273ad1015
commit
0986200910
9 changed files with 201 additions and 92 deletions
|
|
@ -3,6 +3,8 @@ import { baseUrl } from 'stores/utils';
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { useConfigStore } from 'stores/config';
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { MainButton } from 'components/button';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useQuotationPayment } from 'src/stores/quotations';
|
||||
import {
|
||||
|
|
@ -11,7 +13,7 @@ import {
|
|||
QuotationFull,
|
||||
QuotationPaymentData,
|
||||
} from 'src/stores/quotations/types';
|
||||
import { dateFormat } from 'src/utils/datetime';
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
import { QFile, QMenu } from 'quasar';
|
||||
import UploadFileCard from 'src/components/upload-file/UploadFileCard.vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
|
@ -60,10 +62,12 @@ const state = reactive({
|
|||
payExpansion: [] as boolean[],
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
(e: 'view', id: string): void;
|
||||
}>();
|
||||
|
||||
function monthDisplay(date: Date | string) {
|
||||
const arr = dateFormat(date, true).split(' ');
|
||||
arr.shift();
|
||||
return arr.join(' ');
|
||||
return dateFormatJS({ date, monthStyle: 'long', locale: 'th-Th' });
|
||||
}
|
||||
|
||||
function pickFile(index: number) {
|
||||
|
|
@ -463,83 +467,97 @@ onMounted(async () => {
|
|||
class="row full-width items-center surface-2 bordered-b q-px-md q-py-sm"
|
||||
>
|
||||
<span class="text-weight-medium column">
|
||||
{{ $t('quotation.receiptDialog.allInstallments') }}
|
||||
{{ monthDisplay(p.date) }}
|
||||
<span
|
||||
class="text-caption app-text-muted-2"
|
||||
v-if="data.payCondition !== 'Full'"
|
||||
>
|
||||
{{ $t('quotation.receiptDialog.paymentDueDate') }}
|
||||
{{
|
||||
data.payCondition !== 'BillFull'
|
||||
? dateFormat(p.date, true)
|
||||
: dateFormat(data.payBillDate)
|
||||
}}
|
||||
</span>
|
||||
{{ $t('quotation.periodNo') }} {{ i + 1 }}
|
||||
{{ monthDisplay(p.createdAt) }}
|
||||
<!-- {{ monthDisplay(p.date) }} -->
|
||||
<!-- <span -->
|
||||
<!-- class="text-caption app-text-muted-2" -->
|
||||
<!-- v-if="data.payCondition !== 'Full'" -->
|
||||
<!-- > -->
|
||||
<!-- {{ $t('quotation.receiptDialog.paymentDueDate') }} -->
|
||||
<!-- {{ -->
|
||||
<!-- data.payCondition !== 'BillFull' -->
|
||||
<!-- ? dateFormat(p.date, true) -->
|
||||
<!-- : dateFormat(data.payBillDate) -->
|
||||
<!-- }} -->
|
||||
<!-- </span> -->
|
||||
</span>
|
||||
|
||||
<q-btn
|
||||
@click.stop
|
||||
unelevated
|
||||
padding="4px 8px"
|
||||
class="q-ml-auto rounded text-capitalize text-weight-regular payment-wait row items-center"
|
||||
:class="{
|
||||
'payment-pending': p.paymentStatus === 'PaymentWait',
|
||||
'payment-process':
|
||||
p.paymentStatus === 'PaymentInProcess',
|
||||
'payment-retry': p.paymentStatus === 'PaymentRetry',
|
||||
'payment-success': p.paymentStatus === 'PaymentSuccess',
|
||||
}"
|
||||
>
|
||||
<q-icon
|
||||
size="xs"
|
||||
:name="
|
||||
paymentStatusOpts.find(
|
||||
(s) => s.status === p.paymentStatus,
|
||||
)?.icon || 'mdi-hand-coin-outline'
|
||||
"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
<span>
|
||||
{{ $t(`quotation.receiptDialog.${p.paymentStatus}`) }}
|
||||
</span>
|
||||
<q-icon name="mdi-chevron-down" class="q-pl-xs" />
|
||||
<q-menu
|
||||
ref="refQMenu"
|
||||
fit
|
||||
:offset="[0, 8]"
|
||||
class="rounded"
|
||||
<div class="q-ml-auto row" style="gap: 10px">
|
||||
<MainButton
|
||||
v-if="p.paymentStatus === 'PaymentSuccess'"
|
||||
outlined
|
||||
icon="mdi-play-box-outline"
|
||||
color="207 96% 32%"
|
||||
@click.stop="$emit('view', p.invoiceId)"
|
||||
>
|
||||
<q-list dense>
|
||||
<template
|
||||
v-for="opts in paymentStatusOpts"
|
||||
:key="opts.status"
|
||||
>
|
||||
<q-item
|
||||
v-if="
|
||||
(p.paymentStatus === 'PaymentWait' &&
|
||||
opts.status !== 'PaymentRetry') ||
|
||||
(p.paymentStatus === 'PaymentInProcess' &&
|
||||
opts.status !== 'PaymentInProcess') ||
|
||||
(p.paymentStatus === 'PaymentRetry' &&
|
||||
opts.status !== 'PaymentRetry')
|
||||
"
|
||||
clickable
|
||||
class="row items-center"
|
||||
@click="selectStatus(p, opts.status, i)"
|
||||
{{ $t('customerEmployee.fileType.receipt') }}
|
||||
</MainButton>
|
||||
|
||||
<q-btn
|
||||
@click.stop
|
||||
unelevated
|
||||
padding="4px 8px"
|
||||
class="rounded text-capitalize text-weight-regular payment-wait row items-center"
|
||||
:class="{
|
||||
'payment-pending': p.paymentStatus === 'PaymentWait',
|
||||
'payment-process':
|
||||
p.paymentStatus === 'PaymentInProcess',
|
||||
'payment-retry': p.paymentStatus === 'PaymentRetry',
|
||||
'payment-success':
|
||||
p.paymentStatus === 'PaymentSuccess',
|
||||
}"
|
||||
>
|
||||
<q-icon
|
||||
size="xs"
|
||||
:name="
|
||||
paymentStatusOpts.find(
|
||||
(s) => s.status === p.paymentStatus,
|
||||
)?.icon || 'mdi-hand-coin-outline'
|
||||
"
|
||||
class="q-pr-sm"
|
||||
/>
|
||||
<span>
|
||||
{{ $t(`quotation.receiptDialog.${p.paymentStatus}`) }}
|
||||
</span>
|
||||
<q-icon name="mdi-chevron-down" class="q-pl-xs" />
|
||||
<q-menu
|
||||
ref="refQMenu"
|
||||
fit
|
||||
:offset="[0, 8]"
|
||||
class="rounded"
|
||||
>
|
||||
<q-list dense>
|
||||
<template
|
||||
v-for="opts in paymentStatusOpts"
|
||||
:key="opts.status"
|
||||
>
|
||||
<q-icon
|
||||
:name="opts.icon"
|
||||
:color="opts.color"
|
||||
class="q-pr-sm"
|
||||
size="xs"
|
||||
/>
|
||||
{{ $t(opts.name) }}
|
||||
</q-item>
|
||||
</template>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<q-item
|
||||
v-if="
|
||||
(p.paymentStatus === 'PaymentWait' &&
|
||||
opts.status !== 'PaymentRetry') ||
|
||||
(p.paymentStatus === 'PaymentInProcess' &&
|
||||
opts.status !== 'PaymentInProcess') ||
|
||||
(p.paymentStatus === 'PaymentRetry' &&
|
||||
opts.status !== 'PaymentRetry')
|
||||
"
|
||||
clickable
|
||||
class="row items-center"
|
||||
@click="selectStatus(p, opts.status, i)"
|
||||
>
|
||||
<q-icon
|
||||
:name="opts.icon"
|
||||
:color="opts.color"
|
||||
class="q-pr-sm"
|
||||
size="xs"
|
||||
/>
|
||||
{{ $t(opts.name) }}
|
||||
</q-item>
|
||||
</template>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</section>
|
||||
<section class="row items-center q-px-md q-py-sm">
|
||||
{{ $t('quotation.receiptDialog.amountToBePaid') }}
|
||||
|
|
|
|||
|
|
@ -845,6 +845,7 @@ function storeDataLocal() {
|
|||
3: 'invoice',
|
||||
4: 'payment',
|
||||
6: 'receipt',
|
||||
7: 'receipt',
|
||||
};
|
||||
const documentType = documentTypes[view.value] || '';
|
||||
|
||||
|
|
@ -1221,6 +1222,7 @@ const view = ref<View>(View.Quotation);
|
|||
<template v-if="true">
|
||||
<QuotationFormInfo
|
||||
:view="view"
|
||||
:installment-no="selectedInstallmentNo"
|
||||
v-model:pay-type="quotationFormData.payCondition"
|
||||
v-model:pay-bank="payBank"
|
||||
v-model:pay-split-count="quotationFormData.paySplitCount"
|
||||
|
|
@ -1294,6 +1296,15 @@ const view = ref<View>(View.Quotation);
|
|||
<PaymentForm
|
||||
v-if="view !== View.InvoicePre"
|
||||
:data="quotationFormState.source"
|
||||
@view="
|
||||
(invoiceId) => {
|
||||
selectedInstallmentNo =
|
||||
quotationFormState.source?.paySplit
|
||||
.filter((v) => v.invoiceId === invoiceId)
|
||||
.map((v) => v.no) || [];
|
||||
view = View.Receipt;
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<q-expansion-item
|
||||
|
|
@ -1319,7 +1330,13 @@ const view = ref<View>(View.Quotation);
|
|||
v-model:selected="selectedInstallment"
|
||||
@update:selected="
|
||||
(v) => {
|
||||
selectedInstallmentNo = v.map((value) => value.no);
|
||||
selectedInstallment = v.filter(
|
||||
(value) => !value.invoiceId,
|
||||
);
|
||||
|
||||
selectedInstallmentNo = selectedInstallment.map(
|
||||
(value: any) => value.no,
|
||||
);
|
||||
}
|
||||
"
|
||||
row-key="no"
|
||||
|
|
@ -1342,12 +1359,28 @@ const view = ref<View>(View.Quotation);
|
|||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-tr
|
||||
:class="{ 'cursor-pointer': props.row.invoiceId }"
|
||||
:props="props"
|
||||
@click="
|
||||
() => {
|
||||
if (props.row.invoiceId) {
|
||||
selectedInstallmentNo =
|
||||
quotationFormState.source?.paySplit
|
||||
.filter(
|
||||
(v) =>
|
||||
v.invoiceId === props.row.invoiceId,
|
||||
)
|
||||
.map((v) => v.no) || [];
|
||||
view = View.Invoice;
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-td auto-width>
|
||||
<q-checkbox
|
||||
:disable="props.row.invoice"
|
||||
v-model="props.selected"
|
||||
v-if="!props.row.invoice"
|
||||
v-if="!props.row.invoiceId"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td
|
||||
|
|
@ -1396,7 +1429,7 @@ const view = ref<View>(View.Quotation);
|
|||
}"
|
||||
>
|
||||
<MainButton
|
||||
v-if="view !== View.InvoicePre"
|
||||
v-if="view !== View.InvoicePre && view !== View.PaymentPre"
|
||||
outlined
|
||||
icon="mdi-play-box-outline"
|
||||
color="207 96% 32%"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ enum View {
|
|||
defineProps<{
|
||||
readonly?: boolean;
|
||||
quotationNo?: string;
|
||||
installmentNo?: number[];
|
||||
view?: View;
|
||||
data?: {
|
||||
total: number;
|
||||
|
|
@ -255,7 +256,12 @@ watch(
|
|||
class="col-12 text-caption"
|
||||
style="padding-left: 20px"
|
||||
>
|
||||
<template v-for="(period, i) in paySplit" :key="period.no">
|
||||
<template
|
||||
v-for="(period, i) in !!installmentNo
|
||||
? paySplit.filter((value) => installmentNo?.includes(value.no))
|
||||
: paySplit"
|
||||
:key="period.no"
|
||||
>
|
||||
<div
|
||||
class="row app-text-muted items-center"
|
||||
:class="{ 'q-mb-sm': i !== paySplit.length }"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const columnPaySplit = [
|
|||
name: 'status',
|
||||
align: 'center',
|
||||
label: 'general.status',
|
||||
field: 'invoice',
|
||||
field: 'invoiceId',
|
||||
},
|
||||
] satisfies QTableProps['columns'];
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ const DEFAULT_DATA: QuotationPayload = {
|
|||
const DEFAULT_DATA_INVOICE: InvoicePayload = {
|
||||
quotationId: '',
|
||||
amount: 0,
|
||||
productServiceListId: [],
|
||||
installmentNo: [],
|
||||
};
|
||||
|
||||
|
|
@ -136,7 +135,7 @@ export const useQuotationForm = defineStore('form-quotation', () => {
|
|||
payBillDate: data.payBillDate ? new Date(data.payBillDate) : undefined,
|
||||
paySplit: data.paySplit.map((p, index) => ({
|
||||
no: index + 1,
|
||||
invoice: p.invoice,
|
||||
invoiceId: p.invoiceId,
|
||||
amount: p.amount,
|
||||
})),
|
||||
worker: data.worker.map((v) =>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export type Invoice = {
|
|||
|
||||
amount: number;
|
||||
|
||||
productServiceList: QuotationFull['productServiceList'];
|
||||
installements: QuotationFull['paySplit'];
|
||||
|
||||
quotation: Quotation;
|
||||
|
||||
|
|
@ -18,8 +18,6 @@ export type Invoice = {
|
|||
export type InvoicePayload = {
|
||||
quotationId: string;
|
||||
amount: number;
|
||||
// NOTE: For individual list that will be include in the quotation
|
||||
productServiceListId?: string[];
|
||||
// NOTE: Will be pulled from quotation
|
||||
installmentNo?: number[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
|||
|
||||
const res = await api.post('/quotation', {
|
||||
...payload,
|
||||
paySplit: data.paySplit.map((v) => ({
|
||||
amount: v.amount,
|
||||
})),
|
||||
productServiceList: data.productServiceList.map((v) => ({
|
||||
vat: v.vat,
|
||||
amount: v.amount,
|
||||
|
|
@ -104,6 +107,9 @@ export const useQuotationStore = defineStore('quotation-store', () => {
|
|||
const { _count, ...payload } = data;
|
||||
const res = await api.put(`/quotation/${data.id}`, {
|
||||
...payload,
|
||||
paySplit: data.paySplit.map((v) => ({
|
||||
amount: v.amount,
|
||||
})),
|
||||
productServiceList: payload.productServiceList.map((v) => ({
|
||||
vat: v.vat,
|
||||
amount: v.amount,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { CustomerType } from '../customer/types';
|
||||
import { District, Province, SubDistrict } from '../address';
|
||||
import { CreatedBy, Status, UpdatedBy } from '../types';
|
||||
import { Invoice } from '../payment/types';
|
||||
|
||||
export type QuotationStatus =
|
||||
| 'Issued'
|
||||
|
|
@ -204,7 +205,7 @@ export type Quotation = {
|
|||
paySplitCount: number;
|
||||
paySplit: {
|
||||
no: number;
|
||||
invoice: boolean;
|
||||
invoiceId: string;
|
||||
amount: number;
|
||||
}[];
|
||||
payCondition:
|
||||
|
|
@ -286,7 +287,12 @@ export type QuotationFull = {
|
|||
urgent: boolean;
|
||||
payBillDate: string | Date | null;
|
||||
paySplitCount: number | null;
|
||||
paySplit: { no: number; amount: number; invoice: boolean }[];
|
||||
paySplit: {
|
||||
no: number;
|
||||
amount: number;
|
||||
invoice: Invoice;
|
||||
invoiceId: string;
|
||||
}[];
|
||||
payCondition:
|
||||
| 'Full'
|
||||
| 'Split'
|
||||
|
|
@ -397,6 +403,8 @@ export type ProductGroup = {
|
|||
};
|
||||
|
||||
export type QuotationPaymentData = {
|
||||
invoiceId: string;
|
||||
createdAt: Date;
|
||||
paymentStatus: string;
|
||||
amount: number;
|
||||
remark: string;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,52 @@ export function setLocale(locale: string) {
|
|||
moment.locale(locale);
|
||||
}
|
||||
|
||||
export function dateFormatJS(opts: {
|
||||
date: string | Date | null;
|
||||
locale?: string;
|
||||
dayStyle?: 'numeric' | '2-digit';
|
||||
monthStyle?: 'numeric' | '2-digit' | 'long' | 'short';
|
||||
timeStyle?: 'full' | 'long' | 'medium' | 'short';
|
||||
}) {
|
||||
const dateObject = opts.date ? new Date(opts.date) : new Date();
|
||||
|
||||
const dateFormat = new Date(
|
||||
Date.UTC(
|
||||
dateObject.getUTCFullYear(),
|
||||
dateObject.getUTCMonth(),
|
||||
dateObject.getUTCDate(),
|
||||
dateObject.getUTCHours(),
|
||||
dateObject.getUTCMinutes(),
|
||||
dateObject.getUTCSeconds(),
|
||||
),
|
||||
);
|
||||
|
||||
let formattedDate = new Intl.DateTimeFormat(opts.locale || 'en-US', {
|
||||
day: opts.dayStyle,
|
||||
month: opts.monthStyle,
|
||||
timeStyle: opts.timeStyle,
|
||||
year: 'numeric',
|
||||
}).format(dateFormat);
|
||||
|
||||
if (opts.locale === 'th-Th') {
|
||||
formattedDate = formattedDate.replace(/(\d{4})/, (year) =>
|
||||
(parseInt(year) - 543).toString(),
|
||||
);
|
||||
}
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use dateFormatJS.
|
||||
*/
|
||||
|
||||
export function dateFormat(
|
||||
date?: string | Date | null,
|
||||
fullmonth = false,
|
||||
time = false,
|
||||
number = false,
|
||||
days = false,
|
||||
) {
|
||||
const m = moment(date);
|
||||
|
||||
|
|
@ -27,7 +68,7 @@ export function dateFormat(
|
|||
|
||||
const monthFormat = fullmonth ? 'MMMM' : 'MMM';
|
||||
const formattedDate = m.format(
|
||||
`DD ${monthFormat} YYYY ${time ? ' HH:mm' : ''}`,
|
||||
` ${days ? '' : 'DD'} ${monthFormat} YYYY ${time ? ' HH:mm' : ''}`,
|
||||
);
|
||||
|
||||
return formattedDate;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue