jws-frontend/src/pages/08_request-list/DocumentExpansion.vue
2024-12-25 14:00:02 +07:00

621 lines
21 KiB
Vue

<script setup lang="ts">
import { reactive, ref } from 'vue';
import OcrDialog from 'src/components/upload-file/OcrDialog.vue';
import { parseResultMRZ, runOcr } from 'src/utils/ocr';
import { useRequestList } from 'src/stores/request-list';
import {
DocStatus,
RowDocument,
Attributes,
} from 'src/stores/request-list/types';
import MainButton from 'src/components/button/MainButton.vue';
import FormGroupHead from './FormGroupHead.vue';
import BadgeComponent from 'src/components/BadgeComponent.vue';
import FormEmployeePassport from 'components/03_customer-management/FormEmployeePassport.vue';
import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue';
import { CancelButton, EditButton } from 'src/components/button';
import { docColumn } from './constants';
import { computed } from 'vue';
import { QTableSlots } from 'quasar';
import {
EmployeePassportPayload,
EmployeeVisaPayload,
} from 'stores/employee/types';
type Data = {
id: string;
key: string;
type: 'customer' | 'employee';
data: RowDocument & { fileName: string };
};
defineEmits<{
(e: 'changeStatus', v: Partial<Data> & { status: DocStatus }): void;
(e: 'viewDoc', v: Data): void;
(
e: 'upload',
v: {
id: string;
file: File;
form?: EmployeePassportPayload | EmployeeVisaPayload;
group: string;
type: 'customer' | 'employee';
},
done: typeof fetchRequestAttachment,
): void;
(e: 'download', v: Data): void;
}>();
const group = ref('passport');
const requestListStore = useRequestList();
const props = withDefaults(
defineProps<{
readonly?: boolean;
listDocument: string[];
currentId: { customer: string; employee: string };
}>(),
{
listDocument: () => [],
},
);
const attributes = defineModel<Attributes>('attributes', {
default: {
customer: {},
employee: {},
},
});
const state = reactive<{
isEdit: boolean;
splitPercent: number;
tab: 'customer' | 'employee';
}>({
isEdit: false,
splitPercent: 15,
tab: 'customer',
});
const docStatus = [
DocStatus.AwaitReview,
DocStatus.UploadedAwaitReview,
DocStatus.ReviewedAwaitUpload,
DocStatus.Reviewed,
DocStatus.ApprovedReview,
];
const attachmentList = ref<
Awaited<ReturnType<typeof requestListStore.getAttachmentRequest>>
>({});
async function ocr(file: File): Promise<Record<string, any> | false> {
const result = await ocrGroup(file);
if (result) ocrResultToVariable(result);
return result;
}
async function ocrGroup(file: File) {
switch (group.value) {
case 'passport':
return ocrPassport(file);
case 'visa':
return ocrPassport(file);
default:
return false;
}
}
async function ocrPassport(file: File): Promise<Record<string, any> | false> {
const mrz = await runOcr(file, parseResultMRZ);
return mrz?.result || false;
}
function ocrResultToVariable(
data: Record<string, any>,
): EmployeePassportPayload | EmployeeVisaPayload {
// TODO: assign ocr result to variable
let form: EmployeePassportPayload | EmployeeVisaPayload;
if (group.value === 'passport') {
form = {
birthCountry: data.birthCountry,
previousPassportRef: data.previousPassportRef,
issuePlace: data.nationality,
issueCountry: data.country,
issueDate: new Date(data.issue_date),
type: data.doc_type,
expireDate: new Date(data.expire_date),
birthDate: new Date(data.birthDate),
workerStatus: data.workerStatus,
nationality: data.nationality,
gender: data.sex,
lastNameEN: data.lastNameEN,
lastName: data.last_name,
middleNameEN: data.middleNameEN,
middleName: data.middleName,
firstNameEN: data.firstNameEN,
firstName: data.first_name,
namePrefix: data.namePrefix,
number: data.doc_number,
};
} else {
form = {
arrivalAt: data.arrivalAt,
arrivalTMNo: data.arrivalTMNo,
arrivalTM: data.arrivalTM,
mrz: data.mrz,
entryCount: data.entryCount,
issuePlace: data.issue_place,
issueCountry: data.country,
issueDate: new Date(data.valid_until),
type: data.visa_type || '',
expireDate: new Date(data.expire_date),
remark: data.remark,
workerType: data.workerType,
number: data.doc_number,
};
}
return form;
}
defineExpose({
fetchRequestAttachment,
});
async function fetchRequestAttachment(type: 'customer' | 'employee') {
const res = await requestListStore.getAttachmentRequest(
{
customer: props.currentId.customer,
employee: props.currentId.employee,
}[type] || '',
type,
);
if (res) {
attachmentList.value = res;
}
}
const currentListDocument = computed(() => {
return props.listDocument
.map((v, i) => {
const fileIds = attachmentList.value[v]
? Object.values(attachmentList.value[v])
: [];
const fileNames = fileIds.map((id) => String(id));
return {
no: i + 1,
documentType: v,
fileName: fileNames,
amount: fileIds.length,
documentInSystem: fileIds.length > 0,
status: attributes.value
? (attributes.value[state.tab]?.[v] ?? docStatus[0])
: docStatus[0],
};
})
.filter((v) => {
const filterFileCustomer = ['passport', 'visa', 'tm6'];
const filterFileEmployee = ['citizen'];
if (state.tab === 'customer') {
return !filterFileCustomer.includes(v.documentType);
} else {
return !filterFileEmployee.includes(v.documentType);
}
});
});
function triggerCancel() {
state.isEdit = false;
}
function triggerEdit() {
state.isEdit = true;
}
function changeCustomerTab(opts: { tab: 'customer' | 'employee' }) {
state.tab = opts.tab;
}
</script>
<template>
<q-expansion-item
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="surface-1 q-py-sm text-medium text-body1"
@click="fetchRequestAttachment(state.tab)"
>
<template #header>
<span>
{{ $t('menu.order.documentCheck') }}
</span>
<nav class="q-ml-auto row" v-if="!readonly">
<CancelButton
v-if="state.isEdit"
id="btn-info-basic-undo"
icon-only
type="button"
@click.stop="triggerCancel"
/>
<EditButton
v-if="!state.isEdit"
id="btn-info-basic-edit"
icon-only
@click.stop="triggerEdit"
type="button"
/>
</nav>
</template>
<FormGroupHead>
{{ $t('requestList.relatedDoc') }}
</FormGroupHead>
<main>
<q-splitter
v-model="state.splitPercent"
:limits="[0, 100]"
style="width: 100%"
class="col"
before-class="overflow-hidden"
after-class="overflow-hidden"
>
<template v-slot:before>
<section class="q-px-md q-py-sm column" style="gap: var(--size-1)">
<q-item
v-for="tab in ['customer', 'employee'] as const"
v-close-popup
clickable
:key="tab"
dense
class="no-padding items-center rounded full-width"
active-class="active"
:active="state.tab === tab"
@click="
changeCustomerTab({ tab });
fetchRequestAttachment(tab);
"
:id="`btn-${tab}`"
>
<span class="q-px-md ellipsis">
{{
`${$t('general.document')}${tab === 'customer' ? $t('customer.employer') : $t('customer.employee')}`
}}
</span>
</q-item>
</section>
</template>
<template v-slot:after>
<section class="q-px-md q-py-sm">
<q-table
:columns="docColumn"
:rows="currentListDocument"
bordered
flat
hide-pagination
:rows-per-page-options="[0]"
:no-data-label="$t('general.noDataTable')"
class="full-width"
>
<template v-slot:header="props">
<q-tr
style="background-color: hsla(var(--info-bg) / 0.07)"
:props="props"
>
<q-th
v-for="col in props.cols"
:class="`text-${col.align}`"
:key="col.name"
:props="props"
>
{{ col.label && $t(col.label) }}
</q-th>
<q-th></q-th>
</q-tr>
</template>
<template
v-slot:body="props: {
row: (typeof currentListDocument.value)[number];
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
>
<q-tr
:class="{
dark: $q.dark.isActive,
}"
class="text-center"
>
<q-td>
{{ props.rowIndex + 1 }}
</q-td>
<q-td class="text-left">
{{
$t(`customerEmployee.fileType.${props.row.documentType}`)
}}
</q-td>
<q-td
class="text-left q-gutter-xs"
style="
max-width: 400px;
white-space: normal;
word-break: break-word;
"
>
<span
:class="{
'app-text-muted': !props.row.fileName,
}"
>
<div class="q-gutter-y-xs">
<BadgeComponent
hide-icon
v-for="v in props.row.fileName"
:key="v"
:title="v ? v : $t('requestList.noDocumentYet')"
hsla-color="--gray-8-hsl"
>
<template #label>
<span
:class="{
'cursor-pointer': props.row.fileName,
}"
@click.stop="
$emit('viewDoc', {
key: props.row.documentType,
data: { ...props.row, fileName: v },
id:
{
customer: currentId.customer,
employee: currentId.employee,
}[state.tab] || '',
type: state.tab,
})
"
>
{{ v ? $t(v) : $t('requestList.noDocumentYet') }}
</span>
</template>
<template #append>
<q-icon
:class="{
'cursor-pointer': props.row.fileName,
}"
style="color: hsla(207% 96% 32%)"
name="mdi-tray-arrow-down"
size="xs"
class="app-text-muted q-ml-xs"
@click.stop="
$emit('download', {
key: props.row.documentType,
data: { ...props.row, fileName: v },
id:
{
customer: currentId.customer,
employee: currentId.employee,
}[state.tab] || '',
type: state.tab,
})
"
/>
</template>
</BadgeComponent>
</div>
</span>
</q-td>
<q-td>
{{ props.row.amount }}
</q-td>
<q-td>
<q-icon
:name="`mdi-${props.row.documentInSystem ? 'check' : 'close'}-circle`"
size="sm"
:class="{
'app-text-positive': props.row.documentInSystem,
'app-text-negative': !props.row.documentInSystem,
}"
/>
</q-td>
<q-td class="text-left">
<q-btn-dropdown
:disabled="readonly"
dense
unelevated
:label="
$t(
`requestList.status.${props.row.status || 'AwaitReview'}`,
)
"
class="text-capitalize text-weight-regular doc-status"
:class="{
await: props.row.status === DocStatus.AwaitReview,
'in-progress':
props.row.status === DocStatus.ReviewedAwaitUpload ||
props.row.status === DocStatus.UploadedAwaitReview,
completed:
props.row.status === DocStatus.Reviewed ||
props.row.status === DocStatus.ApprovedReview,
}"
:menu-offset="[0, 8]"
dropdown-icon="mdi-chevron-down"
content-class="bordered rounded"
@click.stop
>
<q-list dense>
<template v-for="s in docStatus" :key="s">
<q-item
v-if="s !== props.row.status"
clickable
v-close-popup
@click="
$emit('changeStatus', {
key: props.row.documentType,
status: s,
type: state.tab,
})
"
>
{{ $t(`requestList.status.${s}`) }}
</q-item>
</template>
</q-list>
</q-btn-dropdown>
</q-td>
<q-td>
<span class="row justify-end no-wrap">
<OcrDialog
@submit="
(file, meta) => {
$emit(
'upload',
{
id: {
customer: currentId.customer,
employee: currentId.employee,
}[state.tab] as string,
file,
form:
meta !== undefined
? ocrResultToVariable(meta)
: undefined,
group: props.row.documentType,
type: state.tab,
},
fetchRequestAttachment,
);
}
"
:ocr
>
<template #trigger="{ browse }">
<MainButton
v-if="!!state.isEdit"
iconOnly
icon="mdi-tray-arrow-up"
color="var(--positive-bg)"
:title="$t('general.upload')"
@click.stop="
group = props.row.documentType;
browse();
"
/>
</template>
<template #body="{ metadata, isRunning }">
<FormEmployeePassport
v-if="group === 'passport' && metadata"
:title="$t('customerEmployee.form.group.passport')"
dense
outlined
separator
ocr
prefix-id="ocr"
v-model:birth-country="metadata.birthCountry"
v-model:previous-passportRef="
metadata.previousPassportRef
"
v-model:issue-place="metadata.nationality"
v-model:issue-country="metadata.country"
v-model:issue-date="metadata.issue_date"
v-model:type="metadata.doc_type"
v-model:expire-date="metadata.expire_date"
v-model:birth-date="metadata.birthDate"
v-model:worker-status="metadata.workerStatus"
v-model:nationality="metadata.nationality"
v-model:gender="metadata.sex"
v-model:last-name-en="metadata.lastNameEN"
v-model:last-name="metadata.last_name"
v-model:middle-name-en="metadata.middleNameEN"
v-model:middle-name="metadata.middleName"
v-model:first-name-en="metadata.firstNameEN"
v-model:first-name="metadata.first_name"
v-model:name-prefix="metadata.namePrefix"
v-model:passport-number="metadata.doc_number"
/>
<FormEmployeeVisa
v-if="group === 'visa' && metadata"
:title="$t('customerEmployee.form.group.visa')"
ocr
dense
outlined
prefix-id="ocr"
v-model:arrival-at="metadata.arrivalAt"
v-model:arrival-tm-no="metadata.arrivalTMNo"
v-model:arrival-tm="metadata.arrivalTM"
v-model:mrz="metadata.mrz"
v-model:entry-count="metadata.entryCount"
v-model:issue-place="metadata.issue_place"
v-model:issue-country="metadata.country"
v-model:visa-issue-date="metadata.valid_until"
v-model:visa-type="metadata.visa_type"
v-model:expire-date="metadata.expire_date"
v-model:visa-expiry-date="metadata.expireDate"
v-model:remark="metadata.remark"
v-model:worker-type="metadata.workerType"
v-model:visa-number="metadata.doc_number"
/>
<div
v-if="isRunning"
class="full-height flex flex-center"
>
<q-circular-progress
indeterminate
rounded
size="50px"
color="light-blue"
class="q-ma-md"
/>
</div>
</template>
</OcrDialog>
</span>
</q-td>
</q-tr>
</template>
</q-table>
</section>
</template>
</q-splitter>
</main>
</q-expansion-item>
</template>
<style scoped>
:deep(tr:nth-child(2n)) {
background: #f9fafc;
&.dark {
background: hsl(var(--gray-11-hsl) / 0.2);
}
}
.active {
background-color: hsla(var(--info-bg) / 0.1);
color: hsl(var(--info-bg));
}
.doc-status {
padding-left: 8px;
border-radius: 20px;
&.await {
color: var(--yellow-6);
background: hsla(var(--yellow-6-hsl) / 0.15);
}
&.in-progress {
color: var(--orange-5);
background: hsla(var(--orange-5-hsl) / 0.15);
}
&.completed {
color: var(--green-5);
background: hsla(var(--green-5-hsl) / 0.15);
}
}
</style>