jws-frontend/src/components/upload-file/UploadFileGroup.vue
Thanaphon Frappet dd09a8cb23
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
fix: uploand error
2025-07-04 15:15:06 +07:00

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>