619 lines
15 KiB
Vue
619 lines
15 KiB
Vue
<script lang="ts" setup>
|
|
import { storeToRefs } from 'pinia';
|
|
import { onMounted, nextTick, ref } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import ThaiBahtText from 'thai-baht-text';
|
|
|
|
// NOTE: Import stores
|
|
import { dialogWarningClose, 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 { CancelButton } from 'components/button';
|
|
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 { t } = useI18n();
|
|
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 vatFactor = c.product.serviceChargeCalcVat
|
|
? (config.value?.vat ?? 0.07)
|
|
: 0;
|
|
|
|
const pricePerUnit =
|
|
precisionRound(c.product.serviceCharge * (1 + vatFactor)) /
|
|
(1 + vatFactor);
|
|
|
|
const amount = c.amount;
|
|
const discount = c.discount || 0;
|
|
|
|
const price =
|
|
(pricePerUnit * amount * (1 + vatFactor) - discount) /
|
|
(1 + vatFactor);
|
|
|
|
const vat = price * vatFactor;
|
|
|
|
product.value.push({
|
|
id: c.product.id,
|
|
code: c.product.code,
|
|
detail: c.product.name,
|
|
priceUnit: precisionRound(pricePerUnit),
|
|
amount: c.amount,
|
|
discount: c.discount,
|
|
vat: c.product.serviceChargeCalcVat ? precisionRound(vat) : 0,
|
|
value: precisionRound(
|
|
pricePerUnit * c.amount +
|
|
(c.product.serviceChargeCalcVat ? vat : 0),
|
|
),
|
|
});
|
|
|
|
a.totalPrice = precisionRound(a.totalPrice + price + discount);
|
|
a.totalDiscount = precisionRound(a.totalDiscount + discount);
|
|
a.vat = precisionRound(a.vat + vat);
|
|
a.vatExcluded = c.product.serviceChargeCalcVat
|
|
? a.vatExcluded
|
|
: precisionRound(a.vatExcluded + price);
|
|
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
|
|
return a;
|
|
},
|
|
{
|
|
totalPrice: 0,
|
|
totalDiscount: 0,
|
|
vat: 0,
|
|
vatExcluded: 0,
|
|
finalPrice: 0,
|
|
},
|
|
);
|
|
|
|
assignData();
|
|
});
|
|
|
|
function print() {
|
|
window.print();
|
|
}
|
|
|
|
async function closeTab() {
|
|
dialogWarningClose(t, {
|
|
message: t('dialog.message.close'),
|
|
action: () => {
|
|
window.close();
|
|
},
|
|
cancel: () => {},
|
|
});
|
|
}
|
|
|
|
function closeAble() {
|
|
return window.opener !== null;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="toolbar">
|
|
<PrintButton solid @click="print" />
|
|
<CancelButton
|
|
outlined
|
|
id="btn-close"
|
|
@click="closeTab()"
|
|
:label="$t('dialog.action.close')"
|
|
v-if="closeAble()"
|
|
/>
|
|
</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.codeProductReceived ?? 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('preview.productList') }}
|
|
</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.vatExcluded,
|
|
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: space-between;
|
|
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>
|