jws-frontend/src/pages/08_request-list/RequestListView.vue

792 lines
25 KiB
Vue

<script setup lang="ts">
// NOTE: Library
import { onMounted, reactive, ref, watch, computed, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
// NOTE: Components
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
import DocumentExpansion from './DocumentExpansion.vue';
import FormExpansion from './FormExpansion.vue';
import PropertiesExpansion from './PropertiesExpansion.vue';
import FormGroupHead from './FormGroupHead.vue';
// NOTE: Store
import { dateFormatJS } from 'src/utils/datetime';
import { useRequestList } from 'src/stores/request-list';
import {
RequestData,
RequestWork,
Attributes,
DocStatus,
Step,
RequestWorkStatus,
} from 'src/stores/request-list/types';
import useOptionStore from 'src/stores/options';
import ProductExpansion from './ProductExpansion.vue';
import { useRoute } from 'vue-router';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
import { initLang, initTheme, Lang } from 'src/utils/ui';
import {
EmployeePassportPayload,
EmployeeVisaPayload,
} from 'stores/employee/types';
import {
PropDate,
PropNumber,
PropOptions,
PropString,
} from 'src/stores/product-service/types';
import { Invoice } from 'src/stores/payment/types';
const { locale } = useI18n();
// NOTE: Variable
const route = useRoute();
const optionStore = useOptionStore();
const requestListStore = useRequestList();
const flowTemplateStore = useWorkflowTemplate();
const workList = ref<RequestWork[]>();
const statusFile = ref<Attributes>({
customer: {},
employee: {},
});
const refDocumentExpansion = ref<InstanceType<typeof DocumentExpansion>[]>([]);
const data = ref<RequestData>();
const flow = ref<WorkflowTemplate>();
const pageState = reactive({
hideMetaData: false,
currentStep: 1,
});
// NOTE: Function
async function fetchRequestWorkList(opts: { requestDataId: string }) {
const res = await requestListStore.getRequestWorkList({
requestDataId: opts.requestDataId,
pageSize: 9999,
});
if (res) {
workList.value = res.result;
}
}
function getCustomerName(
record: RequestData,
opts?: {
locale?: string;
noCode?: boolean;
},
) {
const customer = record.quotation.customerBranch;
return (
{
['CORP']: {
[Lang.English]: customer.registerNameEN,
[Lang.Thai]: customer.registerName,
}[opts?.locale || 'eng'],
['PERS']:
{
[Lang.English]: `${optionStore.mapOption(customer.namePrefix)} ${customer.firstNameEN} ${customer.lastNameEN}`,
[Lang.Thai]: `${optionStore.mapOption(customer.namePrefix)} ${customer.firstName} ${customer.lastName}`,
}[opts?.locale || Lang.English] || '-',
}[customer.customer.customerType] +
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
);
}
function getEmployeeName(
record: RequestData,
opts?: {
locale?: string;
},
) {
const employee = record.employee;
return (
{
[Lang.English]: `${optionStore.mapOption(employee.namePrefix)} ${employee.firstNameEN} ${employee.lastNameEN}`,
[Lang.Thai]: `${optionStore.mapOption(employee.namePrefix)} ${employee.firstName} ${employee.lastName}`,
}[opts?.locale || Lang.English] || '-'
);
}
async function getData() {
const current = route.params['requestListId'];
if (typeof current === 'string') {
const res = await requestListStore.getRequestData(current);
if (res) {
data.value = res;
await fetchRequestWorkList({ requestDataId: current });
await getFlow();
}
}
}
async function getFlow() {
if (!workList.value) return;
const attr = workList.value.find((v) => !!v.productService.work?.attributes)
?.productService.work?.attributes;
if (attr && Object.hasOwn(attr, 'workflowId')) {
const workflowId = attr['workflowId'];
const res = await flowTemplateStore.getWorkflowTemplate(workflowId);
if (res) flow.value = res;
}
}
onMounted(async () => {
initTheme();
initLang();
// get data
await getData();
});
watch(() => route.params['requestListId'], getData);
async function triggerChangeStatusWork(step: Step) {
const res = await requestListStore.editStatusRequestWork(step);
if (res) {
const indexWork = workList.value?.findIndex(
(v) => v.id === step.requestWorkId,
);
if (indexWork === -1 || indexWork === undefined) return;
if (workList.value === undefined) return;
const indexStep = workList.value[indexWork].stepStatus.findIndex(
(v) => v.step === step.step,
);
if (indexStep === -1) {
workList.value[indexWork].stepStatus.push(res);
}
if (indexStep !== -1) {
workList.value[indexWork].stepStatus[indexStep].workStatus =
res.workStatus;
}
}
await nextTick();
if (successAll.value) {
await requestListStore.editStatusRequestWork(step, !!successAll.value);
}
}
async function triggerChangeStatusFile(opt: {
index: number;
id: string;
documentType: string;
status: DocStatus;
type: 'customer' | 'employee';
}) {
if (!workList.value) return;
const workItem = workList.value[opt.index];
if (!workItem || !workItem.attributes) {
if (workItem) {
workItem.attributes = { customer: {}, employee: {} };
} else {
return;
}
}
const attributes = workItem.attributes;
statusFile.value = attributes;
if (!statusFile.value[opt.type]) {
statusFile.value[opt.type] = {};
}
statusFile.value[opt.type]![opt.documentType] = opt.status;
const res = await requestListStore.editRequestWork({
id: opt.id,
attributes: statusFile.value,
});
if (res) {
workList.value[opt.index].attributes = res.attributes;
}
}
async function triggerUpload(opt: {
id: string;
type: 'customer' | 'employee';
group: string;
file: File;
form?: EmployeePassportPayload | EmployeeVisaPayload;
}) {
const newName = `${opt.group}-${Date.now()}-${opt.file.name}`;
const res = await requestListStore.uploadAttachmentRequest({
...opt,
name: newName,
});
return !!res;
}
async function triggerViewFile(opt: {
id: string;
fileName: string;
type: 'customer' | 'employee';
group: string;
download?: boolean;
}) {
let url;
url = await requestListStore.viewAttachmentRequest({
id: opt.id,
name: opt.fileName,
type: opt.type,
group: opt.group,
download: opt.download,
});
if (!opt.download) window.open(url, '_blank');
}
const successAll = computed(() => {
return !!flow.value?.step.every((_, i) => {
return workList.value
?.filter((v) => {
return v.productService.work?.attributes.workflowStep?.[
i
]?.productsId.includes(v.productService.productId);
})
.every((v) => {
const status = v.stepStatus.find(
({ step }) => step === i + 1,
)?.workStatus;
return (
status === RequestWorkStatus.Completed ||
status === RequestWorkStatus.Ended
);
});
});
});
function getInstallmentInfo() {
if (
data.value?.quotation.payCondition === 'Full' ||
data.value?.quotation.payCondition === 'BillFull'
) {
return undefined;
}
const total = data.value?.quotation.paySplitCount || 0;
const paid = data.value?.quotation.invoice?.reduce((a, c) => {
if (c.payment?.paymentStatus === 'PaymentSuccess') {
a += c.installments.length || 0;
}
return a;
}, 0);
return { total, paid };
}
function isInstallmentPaySuccess(installmentNo: number) {
if (
data.value?.quotation.payCondition === 'Full' ||
data.value?.quotation.payCondition === 'BillFull'
) {
return true;
}
const invoice = data.value?.quotation.invoice?.find((lhs) => {
return lhs.installments?.some((rhs) => rhs.no === installmentNo);
});
return !!(invoice?.payment?.paymentStatus === 'PaymentSuccess');
}
</script>
<template>
<div class="column surface-0 fullscreen" v-if="data">
<!-- SEC: Header -->
<header class="row q-px-md q-py-sm items-center full justify-between">
<div style="flex: 1" class="row items-center">
<RouterLink to="/request-list">
<q-img src="/icons/favicon-512x512.png" width="3rem" />
</RouterLink>
<span class="column text-h6 text-bold q-ml-md">
{{ $t('requestList.title') }}
{{ data.code || '' }}
<span class="text-caption text-regular app-text-muted">
{{
$t('quotation.processOn', {
msg: dateFormatJS({ date: data.createdAt }),
})
}}
</span>
</span>
</div>
</header>
<!-- SEC: Body -->
<main class="col full-width q-pa-md" style="flex-grow: 1; overflow-y: auto">
<section class="col-sm col-12">
<div class="col q-gutter-y-md" :key="pageState.currentStep">
<!-- step -->
<nav
v-if="flow"
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
style="gap: 10px"
>
<span
v-if="
workList?.every((v) =>
v.productService.work?.attributes?.workflowStep?.every(
(s) => {
s.attributes?.properties.length === 0;
},
),
)
"
class="app-text-muted q-py-sm"
>
{{ $t('requestList.noWorkflowTemplate') }}
</span>
<template v-for="(value, i) in flow.step" :key="value.id">
<button
v-if="
workList?.some(
(v) =>
v.productService.work?.attributes?.workflowStep?.[i]
?.attributes.properties.length > 0,
)
"
class="status-color q-pa-sm bordered row items-center cursor-pointer no-wrap"
style="text-wrap: nowrap"
:class="{
['status-color-done']: workList
?.filter((v) => {
return v.productService.work?.attributes.workflowStep?.[
i
]?.productsId.includes(v.productService.productId);
})
.every((v) => {
const status = v.stepStatus.find(
({ step }) => step === i + 1,
)?.workStatus;
return (
status === RequestWorkStatus.Completed ||
status === RequestWorkStatus.Ended
);
}),
['status-color-doing']: true,
['step-status-active']: pageState.currentStep === value.order,
}"
@click="() => (pageState.currentStep = value.order)"
>
<!-- 'quotation-status-active': value.active?.(), -->
<!-- @click="'waiting' !== 'waiting' && value.handler()" -->
<div class="q-px-sm">
<q-icon
class="icon-color quotation-status"
style="border-radius: 50%"
:name="`${pageState.currentStep === value.order ? 'mdi-circle-slice-8' : 'mdi-checkbox-blank-circle-outline'}`"
/>
</div>
<div class="text-left">{{ value.name }}</div>
</button>
</template>
</nav>
<!-- meta data -->
<article class="surface-1 rounded">
<div
class="text-weight-bold row items-center no-wrap q-pa-sm"
style="gap: 16px"
>
<q-img src="/images/quotation-avatar.png" width="42px" />
<span class="ellipsis" style="font-size: 18px">
{{ data.quotation.workName || '-' }}
</span>
<q-btn
class="q-ml-sm"
icon="mdi-pin-outline"
color="primary"
size="sm"
flat
dense
rounded
@click="pageState.hideMetaData = !pageState.hideMetaData"
:style="pageState.hideMetaData ? 'rotate: 90deg' : ''"
style="transition: 0.1s ease-in-out"
/>
</div>
<transition name="slide">
<section
v-if="!pageState.hideMetaData"
class=""
:class="{ row: $q.screen.gt.sm, column: $q.screen.lt.md }"
>
<FormGroupHead class="col-12">
{{ $t('requestList.ref') }}
</FormGroupHead>
<div
class="col-12 q-pa-sm"
:class="{
row: $q.screen.gt.sm,
'column q-gutter-y-sm': $q.screen.lt.md,
}"
>
<DataDisplay
class="col"
icon="mdi-file-document-outline"
:label="$t('requestList.quotationCode')"
:value="data.quotation.code || '-'"
/>
<DataDisplay
class="col"
icon="mdi-file-document-outline"
tooltip
:label="$t('requestList.invoiceCode')"
:value="
data.quotation?.invoice
?.map((i: Invoice) => i.code)
.join(', ') || '-'
"
/>
<DataDisplay
class="col"
icon="mdi-file-document-outline"
tooltip
:label="$t('requestList.receiptCode')"
:value="
data.quotation?.invoice
?.flatMap((i: Invoice) => i.payment?.code || [])
.join(', ') || '-'
"
/>
<div v-if="$q.screen.gt.sm" class="col"></div>
</div>
<FormGroupHead class="col-12">
{{ $t('quotation.employee') }}
</FormGroupHead>
<div
class="col-12 q-pa-sm"
:class="{
row: $q.screen.gt.sm,
'column q-gutter-y-sm': $q.screen.lt.md,
}"
>
<DataDisplay
class="col"
icon="mdi-account-settings-outline"
:label="$t('customer.employer')"
:value="
getCustomerName(data, { locale: locale, noCode: true }) ||
'-'
"
/>
<DataDisplay
class="col"
icon="mdi-account-settings-outline"
:label="$t('customer.employee')"
:value="
getEmployeeName(data, { locale: $i18n.locale }) || '-'
"
/>
<DataDisplay
class="col"
icon="mdi-passport"
:label="$t('customerEmployee.form.passportNo')"
:value="data.employee.employeePassport?.[0]?.number || '-'"
/>
<div v-if="$q.screen.gt.sm" class="col"></div>
</div>
<!-- <div
class="col q-gutter-y-md"
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
>
<DataDisplay
class="col"
icon="mdi-file-document-outline"
:label="$t('requestList.requestListCode')"
:value="data.code || '-'"
/>
<DataDisplay
class="col"
icon="mdi-account-settings-outline"
:label="$t('customer.employer')"
:value="
getCustomerName(data, { locale: locale, noCode: true }) ||
'-'
"
/>
</div>
<div
class="col q-gutter-y-md"
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
>
<DataDisplay
class="col"
icon="mdi-file-document-outline"
:label="$t('requestList.quotationCode')"
:value="data.quotation.code || '-'"
/>
<DataDisplay
class="col"
icon="mdi-account-settings-outline"
:label="$t('customer.employee')"
:value="
getEmployeeName(data, { locale: $i18n.locale }) || '-'
"
/>
</div>
<div
class="col q-gutter-y-md"
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
>
<DataDisplay
class="col"
icon="mdi-file-document-outline"
tooltip
:label="$t('requestList.invoiceCode')"
:value="
data.quotation?.invoice
?.map((i: Invoice) => i.code)
.join(', ') || '-'
"
/>
<span class="col"></span>
</div>
<div
class="col q-gutter-y-md"
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
>
<DataDisplay
class="col"
icon="mdi-file-document-outline"
tooltip
:label="$t('requestList.receiptCode')"
:value="
data.quotation?.invoice
?.flatMap((i: Invoice) => i.payment?.code || [])
.join(', ') || '-'
"
/>
<DataDisplay
class="col"
icon="mdi-passport"
:label="$t('customerEmployee.form.passportNo')"
:value="data.employee.employeePassport?.[0]?.number || '-'"
/>
</div>
<div
class="col q-gutter-y-md"
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
>
<DataDisplay
class="col"
icon="mdi-card-account-details-outline"
:label="$t('requestList.alienIdCard')"
:value="data.employee.nrcNo"
/>
<DataDisplay
class="col"
icon="mdi-account-settings-outline"
:label="$t('flow.responsiblePerson')"
:value="'-'"
/>
</div> -->
</section>
</transition>
</article>
<!-- product -->
<template
v-for="(value, index) in workList
?.filter((v) =>
v.productService.work?.attributes.workflowStep?.[
pageState.currentStep - 1
]?.productsId.includes(v.productService.productId),
)
.sort(
(lhs, rhs) =>
lhs.productService.installmentNo -
rhs.productService.installmentNo,
)"
:key="value"
>
<ProductExpansion
:installment-info="getInstallmentInfo()"
:pay-success="
isInstallmentPaySuccess(value.productService.installmentNo)
"
:status="
value.stepStatus?.find((v) => v.step === pageState.currentStep)
"
:installment-no="value.productService.installmentNo"
:pay-condition="data?.quotation.payCondition"
:img-url="`/product/${value.productService.productId}/image/${value.productService.product.selectedImage}`"
:name="value.productService.product.name"
:code="value.productService.product.code"
:product="value.productService.product"
@change-status="
(v) => {
triggerChangeStatusWork({
workStatus: v.requestWorkStatus,
step:
v.step === undefined
? pageState.currentStep
: v.step.step,
requestWorkId: value.id || '',
});
}
"
>
<template v-slot="{ product }">
<section
class="column surface-1 q-px-sm bordered-t q-pb-sm q-gutter-y-sm"
>
<DocumentExpansion
v-if="
value.productService.work?.attributes?.workflowStep[
pageState.currentStep - 1
].attributes?.properties.some(
(v: PropString | PropNumber | PropDate | PropOptions) =>
v.fieldName === 'documentCheck',
)
"
ref="refDocumentExpansion"
:attributes="value.attributes"
@change-status="
(opt) => {
triggerChangeStatusFile({
index,
id: value.id || '',
documentType: opt.key || '',
status: opt.status,
type: opt.type || 'customer',
});
}
"
@view-doc="
(opt) => {
triggerViewFile({
id: opt.id,
fileName: opt.data.fileName,
type: opt.type,
group: opt.data.documentType,
});
}
"
@upload="
async (opt, done) => {
await triggerUpload({ ...opt });
await done(opt.type || 'customer');
}
"
@download="
(opt) => {
triggerViewFile({
id: opt.id,
fileName: opt.data.fileName,
type: opt.type,
group: opt.data.documentType,
download: true,
});
}
"
:current-id="{
customer: value.request.quotation.customerBranchId,
employee: value.request.employeeId,
}"
:listDocument="product?.document"
/>
<FormExpansion
v-if="
value.productService.work?.attributes?.workflowStep[
pageState.currentStep - 1
].attributes?.properties.some(
(v: PropString | PropNumber | PropDate | PropOptions) =>
v.fieldName === 'designForm',
)
"
:step="{
step: pageState.currentStep,
requestWorkId: value.id || '',
}"
:id="value.id"
:attributes-form="
value.stepStatus?.[pageState.currentStep - 1]
"
:responsible-area-district-id="
data.quotation.customerBranch.districtId
"
/>
<PropertiesExpansion
:id="value.id"
:properties-to-show="
value.productService.work?.attributes.workflowStep[
pageState.currentStep - 1
].attributes.properties
"
:attributes="value.attributes"
/>
</section>
</template>
</ProductExpansion>
</template>
</div>
</section>
</main>
</div>
</template>
<style scoped>
.status-color {
--_color: var(--gray-0);
border-color: hsla(var(--_color));
background: hsla(var(--_color) / 0.05);
border-radius: 4px;
.icon-color {
color: hsla(var(--_color));
}
&.status-color-waiting {
--_color: var(--gray-4-hsl);
color: var(--foreground);
}
&.status-color-doing {
--_color: var(--blue-5-hsl);
color: var(--foreground);
}
&.status-color-done {
--_color: var(--green-5-hsl);
color: var(--foreground);
}
}
.step-status-active {
opacity: 1;
font-weight: 600;
transition: 1s box-shadow ease-in-out;
animation: status 1s infinite;
}
@keyframes status {
0% {
box-shadow: 0x 0px 0px hsla(var(--_color) / 0.5);
}
50% {
box-shadow: 0px 0px 1px 4px hsla(var(--_color) / 0.2);
}
100% {
box-shadow: 0px 0px 4px 7px hsla(var(--_color) / 0);
}
}
</style>