621 lines
21 KiB
Vue
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>
|