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 { storeToRefs } from 'pinia';
import { useConfigStore } from 'stores/config'; import { useConfigStore } from 'stores/config';
import { formatNumberDecimal } from 'stores/utils'; import { formatNumberDecimal } from 'stores/utils';
import { MainButton } from 'components/button';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useQuotationPayment } from 'src/stores/quotations'; import { useQuotationPayment } from 'src/stores/quotations';
import { import {
@ -11,7 +13,7 @@ import {
QuotationFull, QuotationFull,
QuotationPaymentData, QuotationPaymentData,
} from 'src/stores/quotations/types'; } from 'src/stores/quotations/types';
import { dateFormat } from 'src/utils/datetime'; import { dateFormatJS } from 'src/utils/datetime';
import { QFile, QMenu } from 'quasar'; import { QFile, QMenu } from 'quasar';
import UploadFileCard from 'src/components/upload-file/UploadFileCard.vue'; import UploadFileCard from 'src/components/upload-file/UploadFileCard.vue';
import { onMounted } from 'vue'; import { onMounted } from 'vue';
@ -60,10 +62,12 @@ const state = reactive({
payExpansion: [] as boolean[], payExpansion: [] as boolean[],
}); });
defineEmits<{
(e: 'view', id: string): void;
}>();
function monthDisplay(date: Date | string) { function monthDisplay(date: Date | string) {
const arr = dateFormat(date, true).split(' '); return dateFormatJS({ date, monthStyle: 'long', locale: 'th-Th' });
arr.shift();
return arr.join(' ');
} }
function pickFile(index: number) { function pickFile(index: number) {
@ -463,32 +467,45 @@ onMounted(async () => {
class="row full-width items-center surface-2 bordered-b q-px-md q-py-sm" class="row full-width items-center surface-2 bordered-b q-px-md q-py-sm"
> >
<span class="text-weight-medium column"> <span class="text-weight-medium column">
{{ $t('quotation.receiptDialog.allInstallments') }} {{ $t('quotation.periodNo') }} {{ i + 1 }}
{{ monthDisplay(p.date) }} {{ monthDisplay(p.createdAt) }}
<span <!-- {{ monthDisplay(p.date) }} -->
class="text-caption app-text-muted-2" <!-- <span -->
v-if="data.payCondition !== 'Full'" <!-- 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>
<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)"
> >
{{ $t('quotation.receiptDialog.paymentDueDate') }} {{ $t('customerEmployee.fileType.receipt') }}
{{ </MainButton>
data.payCondition !== 'BillFull'
? dateFormat(p.date, true)
: dateFormat(data.payBillDate)
}}
</span>
</span>
<q-btn <q-btn
@click.stop @click.stop
unelevated unelevated
padding="4px 8px" padding="4px 8px"
class="q-ml-auto rounded text-capitalize text-weight-regular payment-wait row items-center" class="rounded text-capitalize text-weight-regular payment-wait row items-center"
:class="{ :class="{
'payment-pending': p.paymentStatus === 'PaymentWait', 'payment-pending': p.paymentStatus === 'PaymentWait',
'payment-process': 'payment-process':
p.paymentStatus === 'PaymentInProcess', p.paymentStatus === 'PaymentInProcess',
'payment-retry': p.paymentStatus === 'PaymentRetry', 'payment-retry': p.paymentStatus === 'PaymentRetry',
'payment-success': p.paymentStatus === 'PaymentSuccess', 'payment-success':
p.paymentStatus === 'PaymentSuccess',
}" }"
> >
<q-icon <q-icon
@ -540,6 +557,7 @@ onMounted(async () => {
</q-list> </q-list>
</q-menu> </q-menu>
</q-btn> </q-btn>
</div>
</section> </section>
<section class="row items-center q-px-md q-py-sm"> <section class="row items-center q-px-md q-py-sm">
{{ $t('quotation.receiptDialog.amountToBePaid') }} {{ $t('quotation.receiptDialog.amountToBePaid') }}

View file

@ -845,6 +845,7 @@ function storeDataLocal() {
3: 'invoice', 3: 'invoice',
4: 'payment', 4: 'payment',
6: 'receipt', 6: 'receipt',
7: 'receipt',
}; };
const documentType = documentTypes[view.value] || ''; const documentType = documentTypes[view.value] || '';
@ -1221,6 +1222,7 @@ const view = ref<View>(View.Quotation);
<template v-if="true"> <template v-if="true">
<QuotationFormInfo <QuotationFormInfo
:view="view" :view="view"
:installment-no="selectedInstallmentNo"
v-model:pay-type="quotationFormData.payCondition" v-model:pay-type="quotationFormData.payCondition"
v-model:pay-bank="payBank" v-model:pay-bank="payBank"
v-model:pay-split-count="quotationFormData.paySplitCount" v-model:pay-split-count="quotationFormData.paySplitCount"
@ -1294,6 +1296,15 @@ const view = ref<View>(View.Quotation);
<PaymentForm <PaymentForm
v-if="view !== View.InvoicePre" v-if="view !== View.InvoicePre"
:data="quotationFormState.source" :data="quotationFormState.source"
@view="
(invoiceId) => {
selectedInstallmentNo =
quotationFormState.source?.paySplit
.filter((v) => v.invoiceId === invoiceId)
.map((v) => v.no) || [];
view = View.Receipt;
}
"
/> />
<q-expansion-item <q-expansion-item
@ -1319,7 +1330,13 @@ const view = ref<View>(View.Quotation);
v-model:selected="selectedInstallment" v-model:selected="selectedInstallment"
@update:selected=" @update:selected="
(v) => { (v) => {
selectedInstallmentNo = v.map((value) => value.no); selectedInstallment = v.filter(
(value) => !value.invoiceId,
);
selectedInstallmentNo = selectedInstallment.map(
(value: any) => value.no,
);
} }
" "
row-key="no" row-key="no"
@ -1342,12 +1359,28 @@ const view = ref<View>(View.Quotation);
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <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-td auto-width>
<q-checkbox <q-checkbox
:disable="props.row.invoice"
v-model="props.selected" v-model="props.selected"
v-if="!props.row.invoice" v-if="!props.row.invoiceId"
/> />
</q-td> </q-td>
<q-td <q-td
@ -1396,7 +1429,7 @@ const view = ref<View>(View.Quotation);
}" }"
> >
<MainButton <MainButton
v-if="view !== View.InvoicePre" v-if="view !== View.InvoicePre && view !== View.PaymentPre"
outlined outlined
icon="mdi-play-box-outline" icon="mdi-play-box-outline"
color="207 96% 32%" color="207 96% 32%"

View file

@ -28,6 +28,7 @@ enum View {
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
quotationNo?: string; quotationNo?: string;
installmentNo?: number[];
view?: View; view?: View;
data?: { data?: {
total: number; total: number;
@ -255,7 +256,12 @@ watch(
class="col-12 text-caption" class="col-12 text-caption"
style="padding-left: 20px" 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 <div
class="row app-text-muted items-center" class="row app-text-muted items-center"
:class="{ 'q-mb-sm': i !== paySplit.length }" :class="{ 'q-mb-sm': i !== paySplit.length }"

View file

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

View file

@ -42,7 +42,6 @@ const DEFAULT_DATA: QuotationPayload = {
const DEFAULT_DATA_INVOICE: InvoicePayload = { const DEFAULT_DATA_INVOICE: InvoicePayload = {
quotationId: '', quotationId: '',
amount: 0, amount: 0,
productServiceListId: [],
installmentNo: [], installmentNo: [],
}; };
@ -136,7 +135,7 @@ export const useQuotationForm = defineStore('form-quotation', () => {
payBillDate: data.payBillDate ? new Date(data.payBillDate) : undefined, payBillDate: data.payBillDate ? new Date(data.payBillDate) : undefined,
paySplit: data.paySplit.map((p, index) => ({ paySplit: data.paySplit.map((p, index) => ({
no: index + 1, no: index + 1,
invoice: p.invoice, invoiceId: p.invoiceId,
amount: p.amount, amount: p.amount,
})), })),
worker: data.worker.map((v) => worker: data.worker.map((v) =>

View file

@ -6,7 +6,7 @@ export type Invoice = {
amount: number; amount: number;
productServiceList: QuotationFull['productServiceList']; installements: QuotationFull['paySplit'];
quotation: Quotation; quotation: Quotation;
@ -18,8 +18,6 @@ export type Invoice = {
export type InvoicePayload = { export type InvoicePayload = {
quotationId: string; quotationId: string;
amount: number; amount: number;
// NOTE: For individual list that will be include in the quotation
productServiceListId?: string[];
// NOTE: Will be pulled from quotation // NOTE: Will be pulled from quotation
installmentNo?: number[]; installmentNo?: number[];
}; };

View file

@ -72,6 +72,9 @@ export const useQuotationStore = defineStore('quotation-store', () => {
const res = await api.post('/quotation', { const res = await api.post('/quotation', {
...payload, ...payload,
paySplit: data.paySplit.map((v) => ({
amount: v.amount,
})),
productServiceList: data.productServiceList.map((v) => ({ productServiceList: data.productServiceList.map((v) => ({
vat: v.vat, vat: v.vat,
amount: v.amount, amount: v.amount,
@ -104,6 +107,9 @@ export const useQuotationStore = defineStore('quotation-store', () => {
const { _count, ...payload } = data; const { _count, ...payload } = data;
const res = await api.put(`/quotation/${data.id}`, { const res = await api.put(`/quotation/${data.id}`, {
...payload, ...payload,
paySplit: data.paySplit.map((v) => ({
amount: v.amount,
})),
productServiceList: payload.productServiceList.map((v) => ({ productServiceList: payload.productServiceList.map((v) => ({
vat: v.vat, vat: v.vat,
amount: v.amount, amount: v.amount,

View file

@ -1,6 +1,7 @@
import { CustomerType } from '../customer/types'; import { CustomerType } from '../customer/types';
import { District, Province, SubDistrict } from '../address'; import { District, Province, SubDistrict } from '../address';
import { CreatedBy, Status, UpdatedBy } from '../types'; import { CreatedBy, Status, UpdatedBy } from '../types';
import { Invoice } from '../payment/types';
export type QuotationStatus = export type QuotationStatus =
| 'Issued' | 'Issued'
@ -204,7 +205,7 @@ export type Quotation = {
paySplitCount: number; paySplitCount: number;
paySplit: { paySplit: {
no: number; no: number;
invoice: boolean; invoiceId: string;
amount: number; amount: number;
}[]; }[];
payCondition: payCondition:
@ -286,7 +287,12 @@ export type QuotationFull = {
urgent: boolean; urgent: boolean;
payBillDate: string | Date | null; payBillDate: string | Date | null;
paySplitCount: number | null; paySplitCount: number | null;
paySplit: { no: number; amount: number; invoice: boolean }[]; paySplit: {
no: number;
amount: number;
invoice: Invoice;
invoiceId: string;
}[];
payCondition: payCondition:
| 'Full' | 'Full'
| 'Split' | 'Split'
@ -397,6 +403,8 @@ export type ProductGroup = {
}; };
export type QuotationPaymentData = { export type QuotationPaymentData = {
invoiceId: string;
createdAt: Date;
paymentStatus: string; paymentStatus: string;
amount: number; amount: number;
remark: string; remark: string;

View file

@ -9,11 +9,52 @@ export function setLocale(locale: string) {
moment.locale(locale); 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( export function dateFormat(
date?: string | Date | null, date?: string | Date | null,
fullmonth = false, fullmonth = false,
time = false, time = false,
number = false, number = false,
days = false,
) { ) {
const m = moment(date); const m = moment(date);
@ -27,7 +68,7 @@ export function dateFormat(
const monthFormat = fullmonth ? 'MMMM' : 'MMM'; const monthFormat = fullmonth ? 'MMMM' : 'MMM';
const formattedDate = m.format( const formattedDate = m.format(
`DD ${monthFormat} YYYY ${time ? ' HH:mm' : ''}`, ` ${days ? '' : 'DD'} ${monthFormat} YYYY ${time ? ' HH:mm' : ''}`,
); );
return formattedDate; return formattedDate;