448 lines
12 KiB
Vue
448 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { QTableProps } from 'quasar';
|
|
import { ref, toRaw } from 'vue';
|
|
import { dialog } from 'stores/utils';
|
|
import { useI18n } from 'vue-i18n';
|
|
import TableComponents from 'src/components/TableComponents.vue';
|
|
import ShowAttachent from 'src/components/ShowAttachent.vue';
|
|
import DialogForm from 'components/DialogForm.vue';
|
|
import { dateFormat } from 'src/utils/datetime';
|
|
|
|
const isEdit = ref(false);
|
|
const allMeta = ref<Record<string, string>>();
|
|
const { t } = useI18n();
|
|
const currentId = defineModel<string>('currentId');
|
|
const groupList = defineModel<
|
|
{
|
|
label: string;
|
|
group: string;
|
|
value: string;
|
|
_meta?: Record<string, any>;
|
|
}[]
|
|
>('groupList');
|
|
const obj = defineModel<
|
|
{
|
|
_meta?: Record<string, any>;
|
|
name?: string;
|
|
group?: string;
|
|
url?: string;
|
|
file?: File;
|
|
}[]
|
|
>({
|
|
default: [],
|
|
});
|
|
|
|
const modalDialog = ref<boolean>(false);
|
|
|
|
const splitAttachment = ref<number>(50);
|
|
|
|
const currentIndex = ref<number>(-1);
|
|
|
|
const statusOcr = ref<boolean>(false);
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
readonly?: boolean;
|
|
showTitle?: boolean;
|
|
ocr?: (
|
|
group: any,
|
|
file: File,
|
|
) => void | Promise<{
|
|
status: boolean;
|
|
group: string;
|
|
meta: { name: string; value: string }[];
|
|
}>;
|
|
getFileList?: (group: any) => Promise<typeof obj.value>;
|
|
deleteItem?: (obj: any) => void | Promise<boolean>;
|
|
download?: (obj: any) => void;
|
|
save?: (
|
|
group: any,
|
|
meta: any,
|
|
file: File | undefined,
|
|
) => void | Promise<boolean>;
|
|
autoSave?: boolean;
|
|
hideAction?: boolean;
|
|
columns: QTableProps['columns'];
|
|
menu?: {
|
|
label: string;
|
|
group: string;
|
|
value: string;
|
|
_meta?: Record<string, any>;
|
|
}[];
|
|
}>(),
|
|
{},
|
|
);
|
|
|
|
async function triggerDelete(item: any) {
|
|
dialog({
|
|
color: 'negative',
|
|
icon: 'mdi-alert',
|
|
title: t('dialog.title.confirmDelete'),
|
|
actionText: t('general.delete'),
|
|
message: t('dialog.message.confirmDelete'),
|
|
action: async () => {
|
|
if (
|
|
!props.autoSave ||
|
|
!obj.value[currentIndex.value]?._meta?.hasOwnProperty('id')
|
|
) {
|
|
obj.value.splice(currentIndex.value, 1);
|
|
} else {
|
|
await props.deleteItem?.(item);
|
|
await fileList();
|
|
}
|
|
},
|
|
cancel: () => {},
|
|
});
|
|
}
|
|
|
|
function browse() {
|
|
inputFile?.click();
|
|
}
|
|
|
|
const inputFile = (() => {
|
|
const _element = document.createElement('input');
|
|
_element.type = 'file';
|
|
_element.accept = 'image/jpeg,image/png';
|
|
_element.addEventListener('change', change);
|
|
return _element;
|
|
})();
|
|
|
|
const selectedMenu = ref<NonNullable<typeof props.menu>[number]>();
|
|
|
|
async function change(e: Event) {
|
|
const _element = e.target as HTMLInputElement | null;
|
|
const _file = _element?.files?.[0];
|
|
|
|
if (_file) {
|
|
const newFileName = `${selectedMenu.value?.group}-${dateFormat(new Date().toISOString())}-${_file.name}`;
|
|
const renamedFile = new File([_file], newFileName, { type: _file.type });
|
|
|
|
if (!obj.value[currentIndex.value] && selectedMenu.value) {
|
|
currentIndex.value = obj.value.length;
|
|
obj.value = [
|
|
...obj.value,
|
|
{
|
|
_meta: structuredClone(toRaw(selectedMenu.value)._meta || {}),
|
|
group: selectedMenu.value?.group,
|
|
file: renamedFile,
|
|
},
|
|
];
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(renamedFile);
|
|
reader.onload = () => {
|
|
if (obj.value[currentIndex.value]) {
|
|
obj.value[currentIndex.value].url = reader.result as string;
|
|
}
|
|
};
|
|
|
|
statusOcr.value = true;
|
|
const resOcr = await props.ocr?.(selectedMenu.value?.value, _file);
|
|
|
|
if (resOcr?.status) {
|
|
modalDialog.value = true;
|
|
const map = resOcr.meta.reduce<Record<string, string>>((a, c) => {
|
|
a[c.name] = c.value;
|
|
return a;
|
|
}, {});
|
|
|
|
allMeta.value = map;
|
|
|
|
if (resOcr.group === 'citizen') {
|
|
obj.value[currentIndex.value]._meta = {
|
|
citizenId: map['citizen_id'],
|
|
firstName: map['firstname'],
|
|
lastName: map['lastname'],
|
|
firstNameEN: map['firstname_en'],
|
|
lastNameEN: map['lastname_en'],
|
|
birthDate: map['birth_date'],
|
|
religion: map['religion'],
|
|
issueDate: map['issue_date'],
|
|
expireDate: map['expire_date'],
|
|
};
|
|
}
|
|
|
|
if (resOcr.group === 'passport') {
|
|
obj.value[currentIndex.value]._meta = {
|
|
type: map['doc_type'],
|
|
number: map['doc_number'],
|
|
gender: map['sex'],
|
|
firstName: map['first_name'],
|
|
lastName: map['last_name'],
|
|
issueDate: map['issue_date'],
|
|
expireDate: map['expire_date'],
|
|
issuePlace: map['nationality'],
|
|
issueCountry: map['country'],
|
|
};
|
|
}
|
|
|
|
if (resOcr.group === 'visa') {
|
|
obj.value[currentIndex.value]._meta = {
|
|
type: map['visa_type'] || '',
|
|
number: map['visa_no'] || '',
|
|
issueDate: map['valid_until'],
|
|
expireDate: map['expire_date'],
|
|
issuePlace: map['issue_place'],
|
|
};
|
|
}
|
|
|
|
statusOcr.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function fileList() {
|
|
if (props.getFileList) {
|
|
const res = await props.getFileList(selectedMenu.value?.value);
|
|
|
|
if (res && Array.isArray(res)) {
|
|
obj.value = res
|
|
.filter((v) => v.name?.includes(selectedMenu.value?.group || ''))
|
|
.map((item) => {
|
|
return {
|
|
...item,
|
|
name: `${item.name?.split('-')[1]}-${item.name?.split('-')[2] || ''}`,
|
|
};
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
defineEmits<{
|
|
(e: 'submit', obj: any, allMate: Record<string, string> | undefined): void;
|
|
}>();
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="!!showTitle" class="col-11 q-pb-sm text-weight-bold text-body1">
|
|
<q-icon
|
|
flat
|
|
size="xs"
|
|
class="q-pa-sm rounded q-mr-xs"
|
|
color="info"
|
|
name="mdi-account-outline"
|
|
style="background-color: var(--surface-3)"
|
|
/>
|
|
{{ $t('general.upload', { msg: $t('general.attachment') }) }}
|
|
</div>
|
|
|
|
<div
|
|
class="relative-position full-width row no-wrap wrapper"
|
|
style="height: 250px"
|
|
>
|
|
<div class="col-3 full-height column no-wrap">
|
|
<div class="q-pa-sm text-center bordered" style="height: 50px">
|
|
<q-btn-dropdown
|
|
:disable="readonly"
|
|
icon="mdi-upload"
|
|
color="info"
|
|
:label="$t('general.uploadFile')"
|
|
>
|
|
<q-list v-for="(v, i) in menu" :key="v.value">
|
|
<q-item
|
|
clickable
|
|
v-close-popup
|
|
@click="
|
|
() => {
|
|
isEdit = true;
|
|
selectedMenu = menu?.[i];
|
|
currentIndex = obj.length;
|
|
|
|
browse();
|
|
}
|
|
"
|
|
>
|
|
<q-item-section>
|
|
<q-item-label>{{ $t(v.label) }}</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-btn-dropdown>
|
|
</div>
|
|
|
|
<div class="bordered-l bordered-b q-pa-sm col full-height scroll">
|
|
<template v-if="menu != undefined">
|
|
<q-item
|
|
clickable
|
|
v-for="v in menu"
|
|
:key="v.value"
|
|
dense
|
|
type="button"
|
|
class="no-padding items-center rounded full-width"
|
|
active-class="menu-active"
|
|
:active="selectedMenu?.group === v.group"
|
|
@click="
|
|
async () => {
|
|
selectedMenu = v;
|
|
if (autoSave) {
|
|
fileList();
|
|
}
|
|
}
|
|
"
|
|
>
|
|
<span class="q-px-md ellipsis q-pa-sm" style="font-size: 16px">
|
|
{{ $t(v.label) }}
|
|
</span>
|
|
</q-item>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="obj"
|
|
class="bordered col surface-2 column items-center no-wrap scroll"
|
|
>
|
|
<slot name="content">
|
|
<template v-if="columns !== undefined">
|
|
<div class="full-width q-pa-md">
|
|
<TableComponents
|
|
buttomDownload
|
|
:readonly="readonly"
|
|
@download="
|
|
(index) => {
|
|
download?.(obj[index]);
|
|
}
|
|
"
|
|
@delete="
|
|
async (index) => {
|
|
currentIndex = index;
|
|
await triggerDelete(obj[index]);
|
|
}
|
|
"
|
|
@view="
|
|
(index) => {
|
|
isEdit = false;
|
|
currentIndex = index;
|
|
modalDialog = true;
|
|
}
|
|
"
|
|
@edit="
|
|
(index) => {
|
|
isEdit = true;
|
|
currentIndex = index;
|
|
modalDialog = true;
|
|
}
|
|
"
|
|
:rows="
|
|
obj
|
|
.filter((v) => {
|
|
if (!autoSave && v.group !== selectedMenu?.group) {
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.map((v, index) => {
|
|
return {
|
|
id: v._meta?.id,
|
|
branchNo: index + 1,
|
|
attachmentName: v.file?.name || v.name,
|
|
uploadDate: '',
|
|
};
|
|
})
|
|
"
|
|
:columns="
|
|
columns.filter((v) => {
|
|
if (selectedMenu?.value === 'attachment') {
|
|
return v.name !== 'ัexpireDate';
|
|
} else {
|
|
return v.name !== 'createdAt';
|
|
}
|
|
})
|
|
"
|
|
></TableComponents>
|
|
</div>
|
|
</template>
|
|
</slot>
|
|
</div>
|
|
<q-inner-loading
|
|
:showing="statusOcr"
|
|
label="Please wait..."
|
|
label-class="text-teal"
|
|
label-style="font-size: 1.1em"
|
|
/>
|
|
</div>
|
|
<DialogForm
|
|
edit
|
|
hideDelete
|
|
:is-edit="isEdit"
|
|
:readonly="readonly"
|
|
style="position: absolute"
|
|
height="100vh"
|
|
weight="90%"
|
|
v-model:modal="modalDialog"
|
|
title="ดูตัวอย่าง"
|
|
hideCloseEvent
|
|
v-if="obj.length > 0"
|
|
:undo="
|
|
() => {
|
|
isEdit = false;
|
|
}
|
|
"
|
|
:edit-data="
|
|
() => {
|
|
isEdit = !isEdit;
|
|
}
|
|
"
|
|
:close="
|
|
() => {
|
|
if (!autoSave || !obj[currentIndex]?._meta?.hasOwnProperty('id')) {
|
|
obj.splice(currentIndex, 1);
|
|
}
|
|
modalDialog = false;
|
|
}
|
|
"
|
|
:submit="
|
|
async () => {
|
|
modalDialog = false;
|
|
|
|
$emit('submit', obj[currentIndex].group, allMeta);
|
|
if (autoSave === true) {
|
|
const statusSave = await save?.(
|
|
obj[currentIndex].group,
|
|
obj[currentIndex]._meta,
|
|
obj[currentIndex].file,
|
|
);
|
|
|
|
if (statusSave) {
|
|
fileList();
|
|
}
|
|
}
|
|
}
|
|
"
|
|
>
|
|
<q-splitter class="full-height" v-model="splitAttachment">
|
|
<template v-slot:before>
|
|
<div class="full-height">
|
|
<ShowAttachent
|
|
v-if="obj[currentIndex]"
|
|
:url="obj[currentIndex].url || ''"
|
|
:file="obj[currentIndex].file"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<template v-slot:after>
|
|
<div class="q-pa-md">
|
|
<slot
|
|
v-if="obj[currentIndex]"
|
|
name="form"
|
|
:mode="obj[currentIndex].group"
|
|
:meta="obj[currentIndex]._meta"
|
|
:isEdit="isEdit"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</q-splitter>
|
|
</DialogForm>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.wrapper > * {
|
|
height: 300px;
|
|
}
|
|
|
|
.menu-active {
|
|
background-color: hsla(var(--info-bg) / 0.1);
|
|
color: hsl(var(--info-bg));
|
|
}
|
|
</style>
|