jws-frontend/src/pages/05_quotation/ReceiptDialog.vue
2024-10-21 14:04:21 +07:00

589 lines
19 KiB
Vue

<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useConfigStore } from 'stores/config';
import {
formatNumberDecimal,
commaInput,
deleteItem,
convertFileSize,
} from 'stores/utils';
import DialogForm from 'src/components/DialogForm.vue';
import { reactive, ref, watch } from 'vue';
import { useQuotationPayment } from 'src/stores/quotations';
import { Quotation, QuotationPaymentData } from 'src/stores/quotations/types';
import { dateFormat } from 'src/utils/datetime';
import { QFile } from 'quasar';
const configStore = useConfigStore();
const quotationPayment = useQuotationPayment();
const { data: config } = storeToRefs(configStore);
defineEmits<{
(e: 'upload', index: number): void;
}>();
const model = defineModel<boolean>({ default: false, required: true });
const data = defineModel<Quotation | undefined>('data', { required: true });
const refQFile = ref<InstanceType<typeof QFile>[]>([]);
const paymentData = ref<QuotationPaymentData[]>([]);
const payAll = ref<boolean>(false);
const slipFile = ref<
{
quotationId: string;
paymentId: string;
file: File[];
}[]
>([]);
const state = reactive({
waitExpansion: true,
payExpansion: [] as boolean[],
});
function monthDisplay(date: Date | string) {
const arr = dateFormat(date, true).split(' ');
arr.shift();
return arr.join(' ');
}
function pickFile(index: number) {
refQFile.value[index].pickFiles();
}
watch(
() => model.value,
async (open) => {
if (!data.value) return;
if (!open) {
paymentData.value = [];
} else {
const ret = await quotationPayment.getQuotationPayment(data.value.id);
if (ret) {
paymentData.value = ret.quotationPaymentData;
slipFile.value = paymentData.value.map((v) => ({
quotationId: ret.id,
paymentId: v.id,
file: [],
}));
// console.log(ret);
// console.log(paymentData.value);
// console.log(slipFile.value);
}
}
},
);
</script>
<template>
<DialogForm
:title="$t('quotation.receipt')"
v-model:modal="model"
width="65%"
>
<div
v-if="data"
class="col column no-wrap"
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
'q-mx-md q-my-sm': !$q.screen.gt.sm,
}"
>
<!-- PRICE DETAIL -->
<section class="bordered rounded surface-1" style="overflow: hidden">
<q-expansion-item
hide-expand-icon
v-model="state.waitExpansion"
header-style="padding:0px"
>
<template v-slot:header>
<div class="column full-width bordered-b">
<header class="bg-color-orange text-bold text-body1 q-pa-sm">
<q-avatar class="surface-1 q-mr-sm" size="10px" />
{{ $t('quotation.receiptDialog.paymentWait') }}
</header>
<span class="q-pa-md row items-center">
<q-img
src="/images/quotation-avatar.png"
width="3rem"
class="q-mr-lg"
/>
<div class="column col">
<span class="text-bold text-body1">
{{ data.workName }}
</span>
<span
class="row items-center"
:class="data.urgent ? 'urgent' : 'app-text-muted'"
>
{{ data.code }}
<q-icon
v-if="data.urgent"
class="q-pl-sm"
name="mdi-fire"
size="xs"
/>
<span class="q-ml-auto" style="color: var(--foreground)">
฿ {{ formatNumberDecimal(data.finalPrice, 2) || '0.00' }}
</span>
</span>
</div>
</span>
</div>
</template>
<span class="app-text-muted-2 q-px-md q-py-sm column">
<span class="row">
{{ $t('general.total') }}
<span class="q-ml-auto" style="color: var(--foreground)">
฿
{{ formatNumberDecimal(data.totalPrice, 2) || '0.00' }}
</span>
</span>
<span class="row">
{{ $t('quotation.discountList') }}
<span class="q-ml-auto" style="color: var(--foreground)">
฿
{{ formatNumberDecimal(data.totalDiscount, 2) || '0.00' }}
</span>
</span>
<span class="row">
{{ $t('general.totalAfterDiscount') }}
<span class="q-ml-auto" style="color: var(--foreground)">
฿
{{
formatNumberDecimal(
data.totalPrice - data.totalDiscount,
2,
) || '0.00'
}}
</span>
</span>
<span class="row">
{{ $t('general.totalVatExcluded') }}
<span class="q-ml-auto" style="color: var(--foreground)">
฿ {{ formatNumberDecimal(data.vatExcluded, 2) || '0.00' }}
</span>
</span>
<span class="row">
{{
$t('general.vat', {
msg: `${config && Math.round(config.vat * 100)}%`,
})
}}
<span class="q-ml-auto" style="color: var(--foreground)">
฿ {{ formatNumberDecimal(data.vat, 2) || '0.00' }}
</span>
</span>
<span class="row">
{{ $t('general.totalVatIncluded') }}
<span class="q-ml-auto" style="color: var(--foreground)">
฿
{{
formatNumberDecimal(
data.totalPrice - data.totalDiscount + data.vat,
2,
) || '0.00'
}}
</span>
</span>
<span class="row">
{{ $t('general.discountAfterVat') }}
<span class="q-ml-auto" style="color: var(--foreground)">
฿ {{ formatNumberDecimal(data.discount, 2) || '0.00' }}
</span>
</span>
</span>
</q-expansion-item>
<q-separator inset v-if="state.waitExpansion" />
<div
class="q-py-sm q-px-md row items-center justify-end app-text-muted-2"
>
{{ $t('quotation.totalPriceBaht') }}:
<span class="q-px-sm" style="color: var(--foreground)">
{{ formatNumberDecimal(data.finalPrice, 2) || '0.00' }}
</span>
<q-btn
dense
flat
rounded
padding="0"
:icon="`mdi-chevron-${state.waitExpansion ? 'down' : 'up'}`"
@click.stop="state.waitExpansion = !state.waitExpansion"
/>
</div>
</section>
<!-- PAYMENT DETAIL -->
<section
class="surface-1 rounded bordered column q-mt-md q-pa-md col scroll no-wrap"
>
<span class="text-weight-bold text-body1">
{{ $t('general.payment') }}
</span>
<div class="row items-center q-pb-md">
<span class="app-text-muted-2">{{ $t('quotation.payType') }}</span>
<div
class="badge-card q-ml-auto rounded"
:class="`badge-card__${data.payCondition}`"
>
{{ $t(`quotation.type.${data.payCondition}`) }}
</div>
</div>
<!-- split summary -->
<div
v-if="
data.payCondition === 'BillSplit' || data.payCondition === 'Split'
"
class="row items-center q-pb-sm"
>
<span class="app-text-muted-2">
{{ $t('quotation.paySplitCount') }}
</span>
<span class="q-ml-auto">
{{ $t('quotation.receiptDialog.total') }}
</span>
<span class="bordered rounded surface-2 number-box q-mx-sm">
{{ data.paySplitCount }}
</span>
{{ $t('quotation.receiptDialog.installments') }}
{{ $i18n.locale === 'eng' ? ',' : '' }}
{{ $t('quotation.receiptDialog.paid') }}
<span class="bordered rounded surface-2 number-box q-mx-sm">
{{
paymentData.reduce(
(c, i) => (i.paymentStatus === 'PaymentSuccess' ? c + 1 : c),
0,
)
}}
</span>
{{
$i18n.locale === 'tha'
? $t('quotation.receiptDialog.installments')
: ','
}}
{{ $t('quotation.receiptDialog.remain') }}
<span class="bordered rounded surface-2 number-box q-mx-sm">
{{
paymentData.reduce(
(c, i) => (i.paymentStatus === 'PaymentWait' ? c + 1 : c),
0,
)
}}
</span>
{{
$i18n.locale === 'tha'
? $t('quotation.receiptDialog.installments')
: ''
}}
</div>
<!-- summary total, paid, remain -->
<div class="row items-center">
<span
class="row col rounded q-px-sm q-py-md"
style="border: 1px solid hsl(var(--info-bg))"
>
{{ $t('quotation.receiptDialog.totalAmount') }}
<span class="q-ml-auto">
{{ formatNumberDecimal(data.finalPrice, 2) }}
</span>
</span>
<span
class="row col rounded q-px-sm q-py-md q-mx-md"
style="border: 1px solid hsl(var(--positive-bg))"
>
{{ $t('quotation.receiptDialog.paid') }}
<span class="q-ml-auto">
{{
formatNumberDecimal(
paymentData.reduce(
(c, i) =>
i.paymentStatus === 'PaymentSuccess' ? c + i.amount : c,
0,
),
2,
)
}}
</span>
</span>
<span
class="row col rounded q-px-sm q-py-md"
style="border: 1px solid hsl(var(--warning-bg))"
>
{{ $t('quotation.receiptDialog.remain') }}
<span class="q-ml-auto">
{{
data.payCondition === 'BillSplit' ||
data.payCondition === 'Split'
? formatNumberDecimal(
paymentData.reduce(
(c, i) =>
i.paymentStatus === 'PaymentWait' ? c + i.amount : c,
0,
),
2,
)
: data.quotationStatus === 'PaymentPending'
? paymentData[0]?.amount &&
formatNumberDecimal(paymentData[0]?.amount, 2)
: '0.00'
}}
</span>
</span>
</div>
<!-- bill -->
<span class="app-text-muted-2 q-pt-md">
{{ $t('quotation.receiptDialog.billOfPayment') }}
</span>
<q-checkbox
v-if="
data.payCondition === 'BillSplit' || data.payCondition === 'Split'
"
size="xs"
v-model="payAll"
:label="$t('quotation.receiptDialog.payAll')"
/>
<!-- payment item -->
<section class="row">
<div
v-for="(p, i) in paymentData"
:key="i"
class="bordered rounded surface-1 q-mb-md col-12"
style="overflow: hidden"
>
<q-expansion-item
hide-expand-icon
v-model="state.payExpansion[i]"
header-style="padding:0px"
>
<template v-slot:header>
<div class="column full-width">
<section
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>
</span>
<div
class="q-ml-auto items-center flex q-py-xs q-px-sm rounded"
:class="{
'payment-success': p.paymentStatus === 'PaymentSuccess',
'payment-wait':
p.paymentStatus === 'PaymentWait' ||
p.paymentStatus === 'PaymentPending',
}"
>
<q-icon size="6px" class="q-mr-sm" name="mdi-circle" />
{{
p.paymentStatus === 'PaymentWait' ||
p.paymentStatus === 'PaymentPending'
? $t('quotation.receiptDialog.notYetPaid')
: $t('quotation.receiptDialog.alreadyPaid')
}}
</div>
</section>
<section class="row items-center q-px-md q-py-sm">
{{ $t('quotation.receiptDialog.amountToBePaid') }}
<span
class="q-px-sm q-ml-auto"
style="color: var(--foreground)"
>
{{ formatNumberDecimal(p.amount, 2) }}
</span>
<q-btn
dense
flat
rounded
padding="0"
:icon="`mdi-chevron-${state.payExpansion[i] ? 'down' : 'up'}`"
@click.stop="
state.payExpansion[i] = !state.payExpansion[i]
"
/>
</section>
</div>
</template>
<div
class="q-px-md q-py-xs text-weight-medium row items-center"
style="background-color: hsla(var(--info-bg) / 0.1)"
>
{{
$t('general.upload', {
msg: $t('quotation.receiptDialog.slip'),
})
}}
</div>
<div class="surface-2 q-pa-md">
<div
class="upload-section column rounded q-py-md full-height items-center justify-center no-wrap"
>
<q-img src="/images/upload.png" width="150px" />
{{ $t('general.upload', { msg: ' E-slip' }) }}
{{
$t('general.or', {
msg: $t('general.upload', {
msg: $t('quotation.receiptDialog.paymentDocs'),
}),
})
}}
<q-btn
unelevated
:label="$t('general.upload')"
rounded
class="app-bg-info q-mt-sm"
@click.stop="() => pickFile(i)"
/>
<q-file
ref="refQFile"
v-show="false"
v-model="slipFile[i].file"
label="Pick files"
filled
multiple
append
style="max-width: 300px"
/>
</div>
</div>
<section class="surface-2 row q-px-md">
<div
v-for="(file, j) in slipFile[i].file"
:key="j"
class="col-12 q-pa-md bordered rounded row items-center"
:class="{ 'q-mb-md': i === 5, 'q-mb-sm': i < 5 }"
>
<q-icon
name="mdi-file-image-outline"
size="lg"
class="app-text-muted"
/>
<article class="col column q-pl-md">
<span>{{ file.name }}</span>
<span class="text-caption app-text-muted-2">
{{ convertFileSize(file.size) }}
<q-spinner-ios
v-if="false"
class="q-mx-xs"
color="primary"
size="1.5em"
/>
<q-icon
v-else
name="mdi-check-circle"
color="positive"
size="1rem"
/>
{{ false ? `Uploading...` : 'Completed' }}
</span>
</article>
<q-btn
icon="mdi-close"
flat
rounded
size="sm"
padding="0"
class="q-ml-auto self-start"
style="color: hsl(var(--text-mute))"
@click="deleteItem(slipFile[i].file, j)"
/>
<q-linear-progress
:value="40"
class="q-mt-sm rounded"
color="info"
/>
</div>
</section>
</q-expansion-item>
</div>
</section>
</section>
</div>
</DialogForm>
</template>
<style scoped>
.bg-color-orange {
--_color: var(--yellow-7-hsl);
color: white;
background: hsla(var(--_color));
}
.dark .bg-color-orange {
--_color: var(--orange-6-hsl);
}
.bg-color-orange-light {
--_color: var(--yellow-7-hsl);
background: hsla(var(--_color) / 0.2);
}
.dark .bg-color-orange {
--_color: var(--orange-6-hsl / 0.2);
}
.payment-success {
color: hsl(var(--positive-bg));
background: hsla(var(--positive-bg) / 0.1);
}
.payment-wait {
color: hsl(var(--warning-bg));
background: hsla(var(--warning-bg) / 0.15);
}
.urgent {
color: hsl(var(--red-6-hsl));
}
.number-box {
text-align: center;
width: 25px;
height: 25px;
}
.badge-card {
padding: 4px 8px;
color: hsla(var(--gray-0-hsl) / 1);
background: hsla(var(--_color) / 1);
}
.badge-card__Full {
--_color: var(--red-6-hsl);
}
.badge-card__Split {
--_color: var(--blue-6-hsl);
}
.badge-card__BillFull {
--_color: var(--jungle-8-hsl);
}
.badge-card__BillSplit {
--_color: var(--purple-7-hsl);
}
.dark .badge-card__Split {
--_color: var(--blue-10-hsl);
}
.upload-section {
border: 3px solid var(--border-color);
border-style: dashed;
}
</style>