jws-frontend/src/pages/09_task-order/document_view/MainPage.vue
2025-01-24 11:56:07 +07:00

585 lines
14 KiB
Vue

<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { onMounted, nextTick, ref } from 'vue';
import ThaiBahtText from 'thai-baht-text';
// NOTE: Import stores
import { formatNumberDecimal } from 'stores/utils';
import { useConfigStore } from 'stores/config';
import { precisionRound } from 'src/utils/arithmetic';
// NOTE Import Types
import { Branch } from 'stores/branch/types';
// NOTE: Import Components
import ViewHeader from './ViewHeader.vue';
import ViewFooter from './ViewFooter.vue';
import PrintButton from 'src/components/button/PrintButton.vue';
import {
TaskOrder,
TaskOrderStatus,
TaskStatus,
} from 'src/stores/task-order/types';
import { useRoute } from 'vue-router';
import { useTaskOrderStore } from 'src/stores/task-order';
import { RequestWork } from 'src/stores/request-list';
import { convertTemplate } from 'src/utils/string-template';
const route = useRoute();
const taskOrder = useTaskOrderStore();
const configStore = useConfigStore();
const config = storeToRefs(configStore).data;
const viewType = ref<'docOrder' | 'docReceive'>('docOrder');
type Data = TaskOrder;
type ProductItem = {
id: string;
code: string;
detail: string;
amount: number;
priceUnit: number;
discount: number;
vat: number;
value: number;
};
type SummaryPrice = {
totalPrice: number;
totalDiscount: number;
vat: number;
vatExcluded: number;
finalPrice: number;
};
const branch = ref<Branch>();
const product = ref<ProductItem[]>([]);
const elements = ref<HTMLElement[]>([]);
const chunks = ref<ProductItem[][]>([[]]);
const data = ref<Data>();
const summaryPrice = ref<SummaryPrice>({
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatExcluded: 0,
finalPrice: 0,
});
async function assignData() {
for (let i = 0; i < product.value.length; i++) {
let el = elements.value.at(-1);
if (!el) return;
if (getHeight(el) < 500) {
chunks.value.at(-1)?.push(product.value[i]);
} else {
chunks.value.push([]);
i--;
}
await nextTick();
}
}
function getHeight(el: HTMLElement) {
const shadow = document.createElement('div');
shadow.style.opacity = '0';
shadow.style.position = 'absolute';
shadow.style.top = '-999999px';
shadow.style.left = '-999999px';
shadow.style.pointerEvents = 'none';
document.body.appendChild(shadow);
shadow.appendChild(el.cloneNode(true));
const height = shadow.offsetHeight;
document.body.removeChild(shadow);
return height;
}
const STORAGE_KEY = 'task-order-preview';
const taskListGroup = ref<
{
product: RequestWork['productService']['product'];
list: (RequestWork & { _status: TaskStatus })[];
}[]
>([]);
onMounted(async () => {
if (route.params['id'] && typeof route.params['id'] === 'string') {
viewType.value = route.name as 'docOrder' | 'docReceive';
const jsonObject = await taskOrder.getTaskOrderById(route.params['id']);
if (!jsonObject) return;
data.value = jsonObject;
branch.value = jsonObject.registeredBranch;
} else {
let jsonString: string | null;
jsonString = localStorage.getItem(STORAGE_KEY);
jsonString = jsonString || sessionStorage.getItem(STORAGE_KEY);
if (!jsonString) return;
localStorage.removeItem(STORAGE_KEY);
const jsonObject: Data = JSON.parse(jsonString);
if (jsonObject) sessionStorage.setItem(STORAGE_KEY, jsonString);
data.value = jsonObject;
branch.value = jsonObject.registeredBranch;
}
const _taskListGroup = data.value?.taskList.reduce<
{
product: RequestWork['productService']['product'];
list: (RequestWork & { _status: TaskStatus })[];
}[]
>((acc, curr) => {
if (
data.value?.taskOrderStatus === TaskOrderStatus.Complete &&
curr.taskStatus !== TaskStatus.Complete
) {
return acc;
}
const step = curr.requestWorkStep;
if (!step) return acc;
if (step.requestWork) {
let exist = acc.find(
(item) => step.requestWork.productService.productId == item.product.id,
);
const requestWork = Object.assign(step.requestWork, {
_status: curr.taskStatus,
});
if (exist) {
exist.list.push(requestWork);
} else {
acc.push({
product: step.requestWork.productService.product,
list: [requestWork],
});
}
}
return acc;
}, []);
product.value = [];
taskListGroup.value = _taskListGroup;
summaryPrice.value = _taskListGroup
.flatMap((v) => {
const list =
(viewType.value === 'docReceive'
? v.list.filter((item) => item._status === TaskStatus.Complete)
: v.list) || [];
return {
product: v.product,
pricePerUnit: v.product.serviceCharge,
discount:
data.value?.taskProduct.find(
({ productId }) => productId === v.product.id,
)?.discount || 0,
amount: list.length,
};
})
.reduce(
(a, c) => {
const priceNoVat = c.product.vatIncluded
? c.pricePerUnit / (1 + (config.value?.vat || 0.07))
: c.pricePerUnit;
const priceDiscountNoVat = priceNoVat * c.amount - c.discount;
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07);
const rawVat = rawVatTotal / c.amount;
product.value.push({
id: c.product.id,
code: c.product.code,
detail: c.product.name,
priceUnit: precisionRound(priceNoVat),
amount: c.amount,
discount: c.discount,
vat: c.product.calcVat ? precisionRound(rawVat) : 0,
value: precisionRound(
priceNoVat * c.amount + (c.product.calcVat ? rawVatTotal : 0),
),
});
a.totalPrice = a.totalPrice + priceDiscountNoVat;
a.totalDiscount = a.totalDiscount + Number(c.discount);
a.vat = c.product.calcVat ? a.vat + rawVatTotal : a.vat;
a.vatExcluded = c.product.calcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + priceDiscountNoVat);
a.finalPrice = a.totalPrice - a.totalDiscount + a.vat;
return a;
},
{
totalPrice: 0,
totalDiscount: 0,
vat: 0,
vatExcluded: 0,
finalPrice: 0,
},
);
assignData();
});
function print() {
window.print();
}
</script>
<template>
<div class="toolbar">
<PrintButton solid @click="print" />
</div>
<div
class="row justify-between container"
:class="{
'color-task-receive': viewType === 'docReceive',
'color-task-order': viewType === 'docOrder',
}"
>
<section class="content" v-for="chunk in chunks">
<ViewHeader
v-if="!!branch && data"
:view-type="viewType"
:branch="branch"
:institution="data.institution"
:details="{
code: data.code,
name: data.taskName,
contactName: data.contactName,
contactTel: data.contactTel,
}"
/>
<span
class="q-mb-sm q-mt-md"
style="
font-weight: 800;
font-size: 16px;
color: var(--main);
display: block;
border-bottom: 2px solid var(--main);
"
>
{{ $t('taskOrder.goodReceipt') }}
</span>
<table ref="elements" class="q-mb-sm" cellpadding="0" style="width: 100%">
<tbody class="color-tr">
<tr>
<th>{{ $t('preview.rank') }}</th>
<th>{{ $t('preview.productCode') }}</th>
<th>{{ $t('general.detail') }}</th>
<th>{{ $t('general.amount') }}</th>
<th>{{ $t('preview.pricePerUnit') }}</th>
<th>{{ $t('preview.discount') }}</th>
<th>{{ $t('preview.vat') }}</th>
<th>{{ $t('preview.value') }}</th>
</tr>
<tr v-for="(v, i) in chunk">
<td class="text-center">{{ i + 1 }}</td>
<td>{{ v.code }}</td>
<td>{{ v.detail }}</td>
<td style="text-align: right">{{ v.amount }}</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.priceUnit, 2) }}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.discount, 2) }}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.vat, 2) }}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.value, 2) }}
</td>
</tr>
</tbody>
</table>
<table
style="width: 40%; margin-left: auto"
class="q-mb-md"
cellpadding="0"
>
<tbody class="color-tr">
<tr>
<td>{{ $t('general.total') }}</td>
<td class="text-right">
{{ formatNumberDecimal(summaryPrice.totalPrice, 2) }}
฿
</td>
</tr>
<tr>
<td>{{ $t('general.discount') }}</td>
<td class="text-right">
{{ formatNumberDecimal(summaryPrice.totalDiscount, 2) || 0 }} ฿
</td>
</tr>
<tr>
<td>{{ $t('general.totalAfterDiscount') }}</td>
<td class="text-right">
{{
formatNumberDecimal(
summaryPrice.totalPrice - summaryPrice.totalDiscount,
2,
)
}}
฿
</td>
</tr>
<tr>
<td>{{ $t('general.totalVatExcluded') }}</td>
<td class="text-right">
{{ formatNumberDecimal(summaryPrice.vatExcluded, 2) || 0 }}
฿
</td>
</tr>
<tr>
<td>{{ $t('general.vat', { msg: '7%' }) }}</td>
<td class="text-right">
{{ formatNumberDecimal(summaryPrice.vat, 2) }} ฿
</td>
</tr>
<tr>
<td>{{ $t('general.totalVatIncluded') }}</td>
<td class="text-right">
{{
formatNumberDecimal(
summaryPrice.totalPrice -
summaryPrice.totalDiscount +
summaryPrice.vat,
2,
)
}}
฿
</td>
</tr>
</tbody>
</table>
<div class="row justify-between q-mb-md" style="width: 100%">
<div
class="column set-width bg-color full-height"
style="padding: 12px"
>
({{ ThaiBahtText(summaryPrice.finalPrice) || 'ศูนย์บาทถ้วน' }})
</div>
<div
class="row text-right border-5 items-center"
style="width: 40%; background: var(--main); padding: 8px"
>
<span style="color: white; font-weight: 600">
{{ $t('quotation.totalPrice') }}
</span>
<span
class="border-5"
style="
width: 70%;
margin-left: auto;
background: white;
padding: 4px;
"
>
{{
formatNumberDecimal(Math.max(summaryPrice.finalPrice, 0), 2) || 0
}}
฿
</span>
</div>
</div>
</section>
<section class="content" v-if="data">
<ViewHeader
v-if="!!branch"
:view-type="viewType"
:branch="branch"
:institution="data.institution"
:details="{
code: data.code,
name: data.taskName,
contactName: data.contactName,
contactTel: data.contactTel,
}"
/>
<span
class="q-mb-sm q-mt-md"
style="
font-weight: 800;
font-size: 16px;
color: var(--main);
display: block;
border-bottom: 2px solid var(--main);
"
>
{{ $t('general.remark') }}
</span>
<div
class="border-5 surface-0 detail-note q-mb-md"
style="width: 100%; padding: 8px 16px; white-space: pre-wrap"
>
<div
v-html="
convertTemplate(data?.remark || '', {
'order-detail': {
items: taskListGroup,
itemsDiscount: data.taskProduct || [],
},
}) || '-'
"
></div>
</div>
<ViewFooter
:data="{
name: '',
company: branch?.name || '',
buyer: '',
buyDate: '',
approveDate: '',
approver: '',
}"
/>
</section>
</div>
</template>
<style scoped>
.color-task-receive {
--main: var(--yellow-6);
--main-hsl: var(--yellow-6-hsl);
}
.color-task-order {
--main: var(--pink-6);
--main-hsl: var(--pink-6-hsl);
}
.toolbar {
width: 100%;
position: sticky;
top: 0;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
padding: 1rem;
background: white;
border-bottom: 1px solid var(--gray-3);
}
table {
border-collapse: collapse;
}
th {
background: var(--main);
color: white;
padding: 4px;
}
td {
padding: 4px 8px;
}
.border-5 {
border-radius: 5px;
}
.set-width {
width: 50%;
}
.bg-color {
background-color: hsla(var(--main-hsl) / 0.1);
}
.color-tr > tr:nth-child(odd) {
background-color: hsla(var(--main-hsl) / 0.1);
}
.container {
padding: 1rem;
display: flex;
gap: 1rem;
margin-inline: auto;
background: var(--gray-3);
width: calc(8.3in + 1rem);
}
.container :deep(*) {
font-size: 95%;
}
.content {
width: 100%;
padding: 0.5in;
align-items: center;
background: white;
page-break-after: always;
break-after: page;
height: 11.7in;
max-height: 11.7in;
}
.position-bottom {
margin-top: auto;
}
.detail-note {
display: flex;
flex-direction: column;
gap: 8px;
& > * {
display: flex;
flex-direction: column;
}
}
hr {
border-style: solid;
border-color: var(--main);
}
@media print {
.toolbar {
display: none;
}
.container {
padding: 0;
gap: 0;
width: 100%;
background: white;
}
.content {
padding: 0;
height: unset;
}
}
</style>