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:
Net 2024-10-31 10:00:35 +07:00 committed by GitHub
parent e273ad1015
commit 0986200910
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 201 additions and 92 deletions

View file

@ -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') }}

View file

@ -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%"

View file

@ -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 }"

View file

@ -47,7 +47,7 @@ export const columnPaySplit = [
name: 'status',
align: 'center',
label: 'general.status',
field: 'invoice',
field: 'invoiceId',
},
] satisfies QTableProps['columns'];

View file

@ -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) =>

View file

@ -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[];
};

View file

@ -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,

View file

@ -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;

View file

@ -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;