Merge branch 'feat/ocr' into develop

This commit is contained in:
Net 2024-08-28 17:47:59 +07:00
commit 7a56f205ec
19 changed files with 1452 additions and 495 deletions

View file

@ -32,6 +32,7 @@ defineProps<{
outlined?: boolean;
readonly?: boolean;
separator?: boolean;
ocr?: boolean;
prefixId: string;
}>();
@ -84,7 +85,35 @@ watch(
</script>
<template>
<div class="row col-12">
<div class="row col-12" v-if="ocr">
<div class="col-12 row">
<div class="col-6 flex flex-center">
<q-img
src="/images/customer-CORP-avartar.png "
style="height: 100px; max-width: 50%"
/>
</div>
<div class="col-6 column">
<q-input
lazy-rules="ondemand"
:for="`${prefixId}-input-passport-no`"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('customerEmployee.form.passportNo')"
v-model="passportNumber"
:rules="[
(val: string) =>
!!val || $t('inputValidate') + $t('formDialogInputPassportNo'),
]"
/>
</div>
</div>
</div>
<div class="row col-12" v-else>
<div class="col-12 q-pb-sm text-weight-bold text-body1">
<q-icon
flat

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import AppBox from './app/AppBox.vue';
import { CancelButton, SaveButton } from './button';
import { CancelButton, ClearButton, SaveButton } from './button';
defineExpose({ browse });
defineProps<{
@ -131,20 +131,15 @@ async function downloadImage(url: string) {
class="row items-center justify-end q-py-sm q-px-md bordered-t"
v-if="!hiddenFooter"
>
<q-btn
dense
unelevated
flat
:color="$q.dark.isActive ? 'grey-9' : 'grey-4'"
:text-color="$q.dark.isActive ? 'grey-1' : 'grey-10'"
:label="$t('general.clear')"
<ClearButton
outlined
@click="
inputFile && (inputFile.value = ''),
(imageUrl = defaultUrl || fallbackUrl || ''),
(file = null)
"
class="q-px-md q-mr-auto"
:disable="
:disabled="
clearButtonDisabled ||
imageUrl === fallbackUrl ||
imageUrl === defaultUrl ||

View file

@ -0,0 +1,26 @@
<script lang="ts" setup>
import MainButton from './MainButton.vue';
defineEmits<{
(e: 'click', v: MouseEvent): void;
}>();
defineProps<{
iconOnly?: boolean;
solid?: boolean;
outlined?: boolean;
disabled?: boolean;
dark?: boolean;
}>();
</script>
<template>
<MainButton
@click="(e) => $emit('click', e)"
v-bind="{ ...$props, ...$attrs }"
icon="mdi-broom"
color="var(--gray-7-hsl)"
:title="iconOnly ? $t('general.clear') : undefined"
>
{{ $t('general.clear') }}
</MainButton>
</template>

View file

@ -0,0 +1,26 @@
<script lang="ts" setup>
import MainButton from './MainButton.vue';
defineEmits<{
(e: 'click', v: MouseEvent): void;
}>();
defineProps<{
iconOnly?: boolean;
solid?: boolean;
outlined?: boolean;
disabled?: boolean;
dark?: boolean;
}>();
</script>
<template>
<MainButton
@click="(e) => $emit('click', e)"
v-bind="{ ...$props, ...$attrs }"
icon="mdi-close"
color="var(--gray-7-hsl)"
:title="iconOnly ? $t('general.clear') : undefined"
>
{{ $t('general.clear') }}
</MainButton>
</template>

View file

@ -6,3 +6,5 @@ export { default as DeleteButton } from './DeleteButton.vue';
export { default as BackButton } from './BackButton.vue';
export { default as UndoButton } from './UndoButton.vue';
export { default as ToggleButton } from './ToggleButton.vue';
export { default as ClearButton } from './ClearButton.vue';
export { default as CloseButton } from './CloseButton.vue';

View file

@ -0,0 +1,367 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { QSelect } from 'quasar';
import { selectFilterOptionRefMod } from 'stores/utils';
import { getRole } from 'src/services/keycloak';
import useOptionStore from 'stores/options';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import DatePicker from '../shared/DatePicker.vue';
const { locale } = useI18n();
import {
dateFormat,
calculateAge,
parseAndFormatDate,
disabledAfterToday,
} from 'src/utils/datetime';
import {
SaveButton,
EditButton,
DeleteButton,
UndoButton,
} from 'components/button';
defineProps<{
prefixId?: string;
outlined?: boolean;
readonly?: boolean;
create?: boolean;
actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS';
orc?: boolean;
}>();
defineEmits<{
(e: 'save'): void;
(e: 'edit'): void;
(e: 'delete'): void;
(e: 'cancel'): void;
}>();
const optionStore = useOptionStore();
const namePrefix = defineModel<string | null>('namePrefix');
const birthDate = defineModel<Date | string | null>('birthDate');
const gender = defineModel<string>('gender');
const address = defineModel<string>('address');
const firstName = defineModel<string>('firstName', { required: true });
const lastName = defineModel<string>('lastName', { required: true });
const firstNameEN = defineModel<string>('firstNameEn', { required: true });
const lastNameEN = defineModel<string>('lastNameEn', { required: true });
const citizenId = defineModel<string | undefined>('citizenId', {
required: true,
});
const nationality = defineModel<string>('nationality');
const religion = defineModel<string>('religion');
const branchOptions = defineModel<{ id: string; name: string }[]>(
'branchOptions',
{ default: [] },
);
const filteredBranchOptions = ref<Record<string, unknown>[]>([]);
let branchFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
onMounted(() => {
branchFilter = selectFilterOptionRefMod(
branchOptions,
filteredBranchOptions,
'name',
);
});
watch(
() => branchOptions.value,
() => {
branchFilter = selectFilterOptionRefMod(
branchOptions,
filteredBranchOptions,
'name',
);
},
);
const prefixNameOptions = ref<Record<string, unknown>[]>([]);
let prefixNameFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const prefixNameEnOptions = ref<Record<string, unknown>[]>([]);
let prefixNameEnFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const genderOptions = ref<Record<string, unknown>[]>([]);
let genderFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
onMounted(() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.gender),
genderOptions,
'label',
);
});
watch(
() => optionStore.globalOption,
() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.gender),
genderOptions,
'label',
);
},
);
function formatCode(input: string | undefined, type: 'code' | 'number') {
if (!input) return;
return input.slice(...(type === 'code' ? [0, -6] : [-6]));
}
</script>
<template>
<div class="row q-mb-sm">
<template v-if="orc">
<div class="row" style="gap: 10px">
<div class="col-4 flex flex-center">
<q-img
src="/images/customer-CORP-avartar.png"
width="80px"
height="80px"
></q-img>
</div>
<div class="col row" style="gap: 10px">
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
:label="$t('customer.form.citizenId')"
for="input-citizen-id"
v-model="citizenId"
/>
<DatePicker
:label="$t('form.birthDate')"
v-model="birthDate"
class="col-12"
:id="`${prefixId}-input-birth-date`"
:readonly="readonly"
clearable
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('general.nationality')"
for="input-nationality"
v-model="nationality"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('customer.form.religion')"
for="input-religion"
v-model="religion"
/>
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
class="col"
dense
:readonly="readonly"
:options="genderOptions"
:hide-dropdown-icon="readonly"
:for="`${prefixId}-select-gender`"
:label="$t('form.gender')"
@filter="genderFilter"
:model-value="readonly ? gender || '-' : gender"
@update:model-value="
(v) => (typeof v === 'string' ? (gender = v) : '')
"
@clear="gender = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="row q-col-gutter-sm">
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
lazy-rules="ondemand"
hide-dropdown-icon
class="col-2"
dense
:readonly="readonly"
:options="prefixNameOptions"
:for="`${prefixId}-select-prefix-name`"
:label="$t('form.prefixName')"
@filter="prefixNameFilter"
:model-value="readonly ? namePrefix || '-' : namePrefix"
@update:model-value="
(v) => {
typeof v === 'string' ? (namePrefix = v) : '';
}
"
@clear="namePrefix = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-5"
:label="$t('customer.form.firstName')"
for="input-first-name"
v-model="firstName"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-5"
:label="$t('customer.form.lastName')"
for="input-last-name"
v-model="lastName"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:disable="!readonly"
:readonly="readonly"
hide-bottom-space
class="col-2"
:label="$t('customer.form.prefixName')"
for="input-prefix-name"
:model-value="
namePrefix ? $t(`customer.form.prefix.${namePrefix}`) : '-'
"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-5"
:label="$t('customer.form.firstNameEN')"
for="input-first-name-en"
v-model="firstNameEN"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-5"
:label="$t('customer.form.lastNameEN')"
for="input-last-name-en"
v-model="lastNameEN"
/>
<q-input
lazy-rules="ondemand"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
:label="$t('general.address')"
for="input-address"
v-model="address"
/>
<DatePicker
:label="$t('customer.form.issueDate')"
v-model="firstName"
class="col-6"
:id="`${prefixId}-input-issue-date`"
:readonly="readonly"
clearable
/>
<DatePicker
:label="$t('customer.form.passportExpiryDate')"
v-model="firstName"
class="col-6"
:id="`${prefixId}-input-passport-expiry-date`"
:readonly="readonly"
clearable
/>
</div>
</div>
</template>
</div>
</template>

View file

@ -1,29 +1,40 @@
<script setup lang="ts">
import { ref } from 'vue';
import { SaveButton, UndoButton } from 'components/button';
import { computed, ref, watch } from 'vue';
import { SaveButton, UndoButton, CloseButton } from 'components/button';
import { dialog } from 'stores/utils';
import { VuePDF, usePDF } from '@tato30/vue-pdf';
const currentFileUrl = defineModel<string>('currentFileUrl');
const { pdf, pages } = usePDF(currentFileUrl);
const currentFileSelected = ref<string>('');
const file = defineModel<
{
group?: string;
url?: string;
file?: File;
}[]
>('file', {
default: [],
});
const selected = defineModel<string>('selected');
const file = defineModel<File | null>('file');
const currentFile = computed(() => file.value.at(currentIndex.value));
const statusOcr = defineModel<boolean>('statusOcr', { default: false });
const currentMode = ref<string>('');
const currentIndex = ref(0);
const scale = ref(1);
const page = ref(1);
const currentTab = ref<string>('information');
const currentIndexDropdownList = ref(0);
const props = withDefaults(
defineProps<{
tree?: { label: string; children: { label: string }[] }[];
dropdownList?: string[];
treeFile: { label: string; file: { label: string }[] }[];
readonly?: boolean;
dropdownList?: { label: string; value: string }[];
hideAction?: boolean;
}>(),
{
tree: () => [],
treeFile: () => [],
},
);
@ -47,73 +58,114 @@ function change(e: Event) {
const reader = new FileReader();
reader.readAsDataURL(_file);
reader.onload = () => {
currentFileUrl.value = reader.result as string;
if (file.value[currentIndex.value]) {
file.value[currentIndex.value].url = reader.result as string;
}
};
if (_file) file.value = _file;
if (_file && file.value[currentIndex.value]) {
file.value[currentIndex.value].file = _file;
file.value[currentIndex.value].group =
props.dropdownList?.[currentIndexDropdownList.value].value;
} else {
file.value.push({
group: props.dropdownList?.[currentIndexDropdownList.value].value,
file: _file,
});
}
statusOcr.value = true;
emit(
'sendOcr',
props.dropdownList?.[currentIndexDropdownList.value] || '',
props.dropdownList?.[currentIndexDropdownList.value].value || '',
inputFile?.files?.[0],
);
}
}
const tabsList = [
{
label: 'information',
name: 'information',
},
{
label: 'document',
name: 'document',
},
];
watch(currentFileSelected, () => {
file.value.findIndex((v, i) => {
if (v.url?.includes(currentFileSelected.value)) {
currentIndex.value = i;
currentMode.value =
props.dropdownList?.[currentIndexDropdownList.value].label || 'other';
}
});
});
const emit = defineEmits<{
(e: 'sendOcr', dropdown: string, file?: File): void;
(e: 'save', เgroup: string, file?: File): void;
(e: 'deleteFile', filename: string): void;
}>();
const { pdf, pages } = usePDF(computed(() => currentFile.value?.url));
</script>
<template>
<div class="full-height full-width row">
<div>
<div class="full-width row no-wrap wrapper">
<div class="col column no-wrap">
<div class="q-pa-sm text-center bordered" style="height: 50px">
<q-btn-dropdown icon="mdi-upload" color="info" label="อัปโหลดเอกสาร">
<q-list v-for="(v, i) in dropdownList" :key="v">
<q-btn-dropdown
:disable="readonly"
icon="mdi-upload"
color="info"
label="อัปโหลดเอกสาร"
>
<q-list v-for="(v, i) in dropdownList" :key="v.value">
<q-item
clickable
v-close-popup
@click="
() => {
currentIndexDropdownList = i;
const _idx = file.findIndex(
(v) => v.group === dropdownList?.[i].value,
);
if (_idx !== -1) {
currentIndex = _idx;
} else {
file.push({
group: dropdownList?.[i].value || 'other',
});
currentIndex = file.length - 1;
}
browse();
}
"
>
<q-item-section>
<q-item-label>{{ v }}</q-item-label>
<q-item-label>{{ $t(v.label) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<div class="full-height bordered-l bordered-b q-pa-sm">
<div class="bordered-l bordered-b q-pa-sm col full-height scroll">
<q-tree
:nodes="tree || []"
:nodes="treeFile || []"
node-key="label"
label-key="label"
children-key="file"
selected-color="primary"
v-model:selected="selected"
v-model:selected="currentFileSelected"
default-expand-all
/>
>
<template v-slot:default-header="prop">
<div class="ellipsis">
<q-tooltip>{{ prop.node.label }}</q-tooltip>
{{ prop.node.label }}
</div>
</template>
</q-tree>
</div>
</div>
<div class="col">
<div class="col column no-wrap">
<div
class="bordered row items-center justify-evenly q-pa-sm"
class="bordered row items-center justify-evenly q-pa-sm no-wrap"
style="height: 50px"
>
<q-btn
@ -125,7 +177,7 @@ const emit = defineEmits<{
id="btn-prev-page-top"
/>
<div>Page {{ page }} of {{ pages }}</div>
<div class="ellipsis">Page {{ page }} of {{ pages }}</div>
<q-btn
@click="scale = scale > 0.25 ? scale - 0.25 : scale"
@ -149,7 +201,7 @@ const emit = defineEmits<{
icon="mdi-magnify-plus-outline"
@click="scale = scale < 2 ? scale + 0.25 : scale"
>
<q-tooltip>{{ $t('zoomIn') }}</q-tooltip>
<q-tooltip>{{ $t('general.zoomIn') }}</q-tooltip>
</q-btn>
<q-btn
@ -165,52 +217,97 @@ const emit = defineEmits<{
<div
class="flex flex-center surface-2 bordered-l bordered-r bordered-b full-height scroll"
>
<VuePDF
v-if="file?.type === 'application/pdf'"
class="q-py-md"
:pdf="pdf"
:page="page"
:scale="scale"
/>
<q-img v-else class="q-py-md full-width" :src="currentFileUrl" />
<template v-if="statusOcr">
<q-spinner color="primary" size="3em" :thickness="2" />
</template>
<template v-else>
<VuePDF
v-if="
currentFile?.url?.split('?').at(0)?.endsWith('.pdf') ||
currentFile?.file?.type === 'application/pdf'
"
class="q-py-md"
:pdf="pdf"
:page="page"
:scale="scale"
/>
<q-img v-else class="q-py-md full-width" :src="currentFile?.url" />
</template>
</div>
</div>
<div class="col-4">
<div class="col-5 column no-wrap">
<div
class="bordered row items-center justify-between q-pa-sm"
style="height: 50px"
>
อมลหนงสอเดนทาง
<div class="row">
{{ $t(currentMode) }}
<div class="row" v-if="!hideAction">
<UndoButton icon-only type="button" />
<SaveButton icon-only type="button" />
<SaveButton
icon-only
type="button"
@click="
$emit(
'save',
dropdownList?.[currentIndexDropdownList].value || '',
inputFile?.files?.[0],
)
"
/>
</div>
</div>
<div class="bordered-r bordered-b full-height">
<q-tabs
dense
inline-label
mobile-arrows
v-model="currentTab"
active-class="active-tab text-weight-bold"
class="app-text-muted full-width"
align="left"
>
<q-tab
:id="`tab-${tab.label}`"
v-for="tab in tabsList"
v-bind:key="tab.name"
class="content-tab text-capitalize"
:name="tab.name"
:label="$t(tab.label)"
<div class="q-pa-sm bordered-r bordered-b full-height col scroll">
<slot name="form" :mode="currentMode" />
<div class="row items-center">
{{ currentFileSelected }}
<CloseButton
icon-only
v-if="!readonly"
type="button"
class="q-ml-sm"
@click="
() => {
const tempValue = treeFile.find(
(v) => v.label === $t(`customer.typeFile.${currentMode}`),
);
if (!tempValue) return;
const idx = tempValue.file?.findIndex(
(v) => v.label === currentFileSelected,
);
dialog({
color: 'negative',
icon: 'mdi-alert',
title: $t('dialog.title.confirmDelete'),
actionText: $t('general.delete'),
persistent: true,
message: $t('dialog.message.confirmDelete'),
action: async () => {
$emit('deleteFile', currentFileSelected);
currentFileSelected = tempValue.file?.[idx - 1].label || '';
},
cancel: () => {},
});
}
"
/>
</q-tabs>
</div>
</div>
</div>
</div>
</template>
<style lang="scss"></style>
<style lang="scss">
.wrapper > * {
height: 300px;
}
</style>

View file

@ -1 +1,2 @@
export { default as UploadFile } from './UploadFile.vue';
export { default as FormCitizen } from './FormCitizen.vue';

View file

@ -54,6 +54,7 @@ export default {
age: 'Age',
nationality: 'Nationalality',
times: 'No. {number}',
uploadFile: 'Upload File',
},
menu: {
dashboard: 'Dashboard',
@ -238,6 +239,17 @@ export default {
},
},
customer: {
typeFile: {
citizenId: 'National ID card',
registrationBook: 'Household registration',
houseMap: 'House map',
businessRegistration: 'Commercial registration',
dbdCertificate: 'DBD Certificate',
VatRegistrationCertificate: 'VAT Registration Certificate',
powerOfAttorney: 'Power of Attorney',
others: 'Others',
},
employer: 'Employer',
employerLegalEntity: 'Legal Entity',
employerNaturalPerson: 'Natrual Person',
@ -255,6 +267,11 @@ export default {
miss: 'Miss.',
},
citizenId: 'Citizen ID',
religion: 'Religion',
issueDate: 'Issue Date',
passportExpiryDate: 'Passport Expiry Date',
firstName: 'First Name in Thai',
lastName: 'Last Name in Thai',
firstNameEN: 'First Name in English',
@ -385,6 +402,20 @@ export default {
mother: 'Mother',
motherBirthPlace: 'Mother Birth Place',
},
fileType: {
passport: 'Passport',
visa: 'VISA',
tm6: 'TM.6',
workPermit: 'Work Permit',
noticeJobEmployment: 'Foreign Worker Employment Notification Form',
noticeJobEntry:
'Foreign Worker Employment Commencement Notification Form',
historyJob: 'Employment History Form',
acceptJob:
'Acknowledgement of Foreign Worker Employment Notification Form',
receipt: 'Receipt',
other: 'Other',
},
},
customerBranch: {
tab: {
@ -392,7 +423,7 @@ export default {
address: 'Address',
business: 'Business',
contact: 'Contact',
attachment: 'Attachment',
attachment: 'Upload Document',
},
form: {
title: 'Branch',

View file

@ -54,6 +54,7 @@ export default {
age: 'อายุ',
nationality: 'สัญชาติ',
times: 'ครั้งที่ {number}',
uploadFile: 'อัปโหลดไฟล์',
},
menu: {
dashboard: 'แดชบอร์ด',
@ -239,6 +240,16 @@ export default {
},
},
customer: {
typeFile: {
citizenId: 'บัตรประจำตัวประชาชน',
registrationBook: 'ทะเบียนบ้าน',
houseMap: 'แผนที่ (บ้าน)',
businessRegistration: 'ทะเบียนพาณิชย์',
dbdCertificate: 'หนังสือรับรอง (DBD)',
VatRegistrationCertificate: 'ภ.พ.20',
powerOfAttorney: 'หนังสือมอบอำนาจ',
others: 'อื่นๆ',
},
employer: 'นายจ้าง',
employee: 'ลูกจ้าง',
employerLegalEntity: 'นิติบุคคล',
@ -256,6 +267,11 @@ export default {
miss: 'Miss.',
},
citizenId: 'บัตรประจำตัวประชาชน',
religion: 'ศาสนา',
issueDate: 'วันที่ออกหนังสือ',
passportExpiryDate: 'วันหiมดอายุหนังสือเดินทาง',
firstName: 'ชื่อ ภาษาไทย',
lastName: 'นามสกุล ภาษาไทย',
firstNameEN: 'ชื่อ ภาษาอังกฤษ',
@ -385,6 +401,18 @@ export default {
mother: 'มารดา',
motherBirthPlace: 'สถานที่เกิดของมารดา',
},
fileType: {
passport: 'ข้อมูลหนังสือการเดินทาง',
visa: 'ข้อมูลการตรวจลงตรา',
tm6: 'ตม.6',
workPermit: 'ใบอนุญาตทำงาน',
noticeJobEmployment: 'แบบแจ้งการจ้างคนต่างด้าวทำงาน',
noticeJobEntry: 'แบบแจ้งเข้าทำงานของคนต่างด้าว',
historyJob: 'ใบคัดประวัติระบบจัดหางาน',
acceptJob: 'ใบตอบรับการแจ้งเกี่ยวกับการทำงานของคนต่างด้าว',
receipt: 'ใบเสร็จรับเงิน',
other: 'อื่นๆ',
},
},
customerBranch: {
tab: {
@ -392,7 +420,7 @@ export default {
address: 'ที่อยู่',
business: 'ธุรกิจ',
contact: 'ติดต่อ',
attachment: 'เอกสารเพิ่มเติม',
attachment: 'อัปโหลดเอกสาร',
},
form: {
title: 'สาขา',

View file

@ -54,7 +54,8 @@ import {
columnsCustomer,
columnsEmployee,
formMenuIconEmployee,
uploadFileList,
uploadFileListEmployee,
uploadFileListCustomer,
} from './constant';
import { useCustomerForm, useEmployeeForm } from './form';
import { storeToRefs } from 'pinia';
@ -1871,6 +1872,7 @@ const emptyCreateDialog = ref(false);
<EmployerFormBranch
v-if="!!customerFormState.editCustomerId"
:index="idx"
v-model:customer="customerFormData"
v-model:customer-branch="customerFormData.customerBranch[idx]"
:customer-type="customerFormData.customerType"
:customer-name="`${customerFormData.firstName} ${customerFormData.lastName}`"
@ -2049,6 +2051,11 @@ const emptyCreateDialog = ref(false);
anchor: 'form-visa',
tab: 'personalInfo',
},
{
name: $t('general.uploadFile'),
anchor: 'drawer-info-file-upload',
tab: 'personalInfo',
},
...(currentFromDataEmployee.employeeCheckup?.map((v, i) => ({
name: $t('general.times', { number: i + 1 }),
anchor: `form-employee-checkup-${i}`,
@ -2160,6 +2167,7 @@ const emptyCreateDialog = ref(false);
employee
separator
title="customerEmployee.form.group.personalInfo"
:readonly="!employeeFormState.isEmployeeEdit"
v-model:open="employeeFormState.dialogModal"
v-model:prefixName="currentFromDataEmployee.namePrefix"
v-model:firstName="currentFromDataEmployee.firstName"
@ -2176,6 +2184,7 @@ const emptyCreateDialog = ref(false);
<AddressForm
id="form-personal-address"
prefix-id="form-employee"
:readonly="!employeeFormState.isEmployeeEdit"
v-model:same-with-employer="
employeeFormState.formDataEmployeeSameAddr
"
@ -2197,6 +2206,7 @@ const emptyCreateDialog = ref(false);
outlined
separator
:title="$t('customerEmployee.form.group.passport')"
:readonly="!employeeFormState.isEmployeeEdit"
v-model:passport-type="currentFromDataEmployee.passportType"
v-model:passport-number="currentFromDataEmployee.passportNumber"
v-model:passport-issue-date="
@ -2223,6 +2233,7 @@ const emptyCreateDialog = ref(false);
dense
outlined
title="customerEmployee.form.group.visa"
:readonly="!employeeFormState.isEmployeeEdit"
v-model:visa-type="currentFromDataEmployee.visaType"
v-model:visa-number="currentFromDataEmployee.visaNumber"
v-model:visa-issue-date="currentFromDataEmployee.visaIssueDate"
@ -2237,6 +2248,84 @@ const emptyCreateDialog = ref(false);
v-model:entry-date="currentFromDataEmployee.entryDate"
class="q-mb-xl"
/>
<div class="row q-mb-md" id="drawer-info-file-upload">
<div class="col-12 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-upload"
style="background-color: var(--surface-3)"
/>
{{ $t(`general.uploadFile`) }}
</div>
<UploadFile
:tree-file="
Object.values(
currentFromDataEmployee.file?.reduce<
Record<
string,
{ label: string; file: { label: string }[] }
>
>((a, c) => {
const _group = c.group || 'other';
if (!a[_group]) {
a[_group] = {
label: $t(
uploadFileListEmployee.find(
(v) => v.value === _group,
)?.label || _group,
),
file: [
{
label:
c.name ||
`${c.group}-${c.file?.name || Date.now()}`,
},
],
};
} else {
a[_group].file.push({
label:
c.name ||
`${c.group}-${c.file?.name || Date.now()}`,
});
}
return a;
}, {}) || {},
)
"
v-model:file="currentFromDataEmployee.file"
hide-action
v-model:status-ocr="employeeFormState.ocr"
:readonly="!employeeFormState.isEmployeeEdit"
:dropdown-list="uploadFileListEmployee"
@delete-file="
async (filename) => {
if (currentFromDataEmployee.id) {
const result = await employeeStore.deleteAttachment(
currentFromDataEmployee.id,
filename,
);
if (result) {
currentFromDataEmployee.file =
currentFromDataEmployee.file?.filter(
(v) => v.name !== filename,
);
}
}
}
"
@send-ocr="
async (_, file) => {
if (file) await ocrStore.sendOcr({ file });
employeeFormState.ocr = false;
}
"
/>
</div>
</template>
<template v-if="employeeFormState.currentTab === 'healthCheck'">
@ -2338,6 +2427,7 @@ const emptyCreateDialog = ref(false);
}
"
/>
a
</template>
<template v-if="employeeFormState.currentTab === 'other'">
@ -2466,8 +2556,8 @@ const emptyCreateDialog = ref(false);
"
:show="
async () => {
await fetchListOfOptionBranch();
customerFormStore.resetForm(customerFormState.dialogType === 'create');
// await fetchListOfOptionBranch();
// customerFormStore.resetForm(true);
}
"
:before-close="
@ -2477,6 +2567,7 @@ const emptyCreateDialog = ref(false);
return false;
} else {
fetchListCustomer();
customerFormStore.resetForm(true);
customerFormState.branchIndex = -1;
}
return false;
@ -2515,10 +2606,15 @@ const emptyCreateDialog = ref(false);
name: $t('form.field.basicInformation'),
anchor: 'form-basic-info-customer',
},
{
name: $t('customerBranch.tab.attachment'),
anchor: 'form-upload-file-customer',
},
{
name: $t('customer.form.group.branch'),
anchor: 'form-branch-customer-branch',
},
...(customerFormData.customerBranch?.map((v, i) => ({
name:
i === 0
@ -2546,7 +2642,6 @@ const emptyCreateDialog = ref(false);
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<EmployerFormBasicInfo
class="q-mb-xl"
:readonly="
(customerFormState.dialogType === 'edit' &&
customerFormState.readonly === true) ||
@ -2577,6 +2672,19 @@ const emptyCreateDialog = ref(false);
v-model:birth-date="customerFormData.birthDate"
/>
<!-- <div class="row q-col-gutter-sm q-mb-xl">
<UploadFile
id="form-upload-file-customer"
class="q-mb-xl"
hide-action
:dropdown-list="uploadFileList"
v-model:file="customerFormState.file"
@send-ocr="
(group: any, file: any) => ocrStore.sendOcr({ file })
"
/>
</div> -->
<div class="row q-col-gutter-sm" id="form-branch-customer-branch">
<div
class="col-12 text-weight-bold text-body1 row items-center"
@ -2663,9 +2771,33 @@ const emptyCreateDialog = ref(false);
);
}
customerFormData.customerBranch[idx].file?.forEach(
async (v) => {
if (!v.file) return;
const ext = v.file.name.split('.').at(-1);
let filename = v.group + '-' + new Date().getTime();
if (ext) filename += `.${ext}`;
const res = await customerStore.putAttachment({
branchId:
customerFormData.customerBranch?.[idx].id || '',
file: v.file,
filename,
});
if (res) {
await customerFormStore.assignFormData(
customerFormState.editCustomerId,
);
}
},
);
await customerFormStore.assignFormData(
customerFormState.editCustomerId,
);
customerFormStore.resetForm();
}
}
@ -2674,9 +2806,11 @@ const emptyCreateDialog = ref(false);
<EmployerFormBranch
v-if="!!customerFormState.editCustomerId"
:index="idx"
v-model:customer="customerFormData"
v-model:customer-branch="
customerFormData.customerBranch[idx]
"
:tree-file="customerFormState.treeFile"
:customer-type="customerFormData.customerType"
:customer-name="`${customerFormData.firstName} ${customerFormData.lastName}`"
:action-disabled="
@ -2869,7 +3003,7 @@ const emptyCreateDialog = ref(false);
},
{
name: $t('อัปโหลดไฟล์'),
name: $t('general.uploadFile'),
anchor: 'drawer-upload-file',
tab: 'personalInfo',
},
@ -3062,17 +3196,83 @@ const emptyCreateDialog = ref(false);
class="q-mb-xl"
/>
<UploadFile
:tree="[]"
:dropdown-list="uploadFileList"
@send-ocr="
async (v: any, f: any) => {
console.log(v, f);
await ocrStore.sendOcr({ file: f });
}
"
/>
<div class="row" id="drawer-upload-file">
<div class="col-12 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-upload"
style="background-color: var(--surface-3)"
/>
{{ $t(`general.uploadFile`) }}
</div>
<UploadFile
:tree-file="
Object.values(
currentFromDataEmployee.file?.reduce<
Record<
string,
{ label: string; file: { label: string }[] }
>
>((a, c) => {
const _group = c.group || 'other';
if (!a[_group]) {
a[_group] = {
label: $t(
uploadFileListEmployee.find(
(v) => v.value === _group,
)?.label || _group,
),
file: [
{
label:
c.name ||
`${c.group}-${c.file?.name || Date.now()}`,
},
],
};
} else {
a[_group].file.push({
label:
c.name ||
`${c.group}-${c.file?.name || Date.now()}`,
});
}
return a;
}, {}) || {},
)
"
v-model:file="currentFromDataEmployee.file"
hide-action
v-model:status-ocr="employeeFormState.ocr"
:readonly="!employeeFormState.isEmployeeEdit"
:dropdown-list="uploadFileListEmployee"
@delete-file="
async (filename) => {
if (currentFromDataEmployee.id) {
const result = await employeeStore.deleteAttachment(
currentFromDataEmployee.id,
filename,
);
if (result) {
currentFromDataEmployee.file =
currentFromDataEmployee.file?.filter(
(v) => v.name !== filename,
);
}
}
}
"
@send-ocr="
async (_, file) => {
if (file) await ocrStore.sendOcr({ file });
employeeFormState.ocr = false;
}
"
/>
</div>
</template>
<template v-if="employeeFormState.currentTab === 'healthCheck'">
<FormEmployeeHealthCheck

View file

@ -5,18 +5,32 @@ import EmployerFormBusiness from './EmployerFormBusiness.vue';
import EmployerFormContact from './EmployerFormContact.vue';
import { CustomerCreate } from 'stores/customer/types';
import EmployerFormAbout from './EmployerFormAbout.vue';
import EmployerFormAttachment from './EmployerFormAttachment.vue';
import { useCustomerForm } from 'src/pages/03_customer-management/form';
const customerFormStore = useCustomerForm();
import { FormCitizen } from 'components/upload-file/';
import useOcrStore from 'stores/ocr';
const ocrStore = useOcrStore();
import {
SaveButton,
EditButton,
DeleteButton,
UndoButton,
} from 'components/button';
import UploadFile from 'src/components/upload-file/UploadFile.vue';
import { uploadFileListCustomer } from '../../constant';
const statusOcr = ref(false);
const customer = defineModel<CustomerCreate>('customer', { required: true });
const item = defineModel<NonNullable<CustomerCreate['customerBranch']>[number]>(
'customerBranch',
{ required: true },
);
const tab = ref('main');
defineEmits<{
@ -33,6 +47,7 @@ defineProps<{
prefixId?: string;
actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS';
treeFile?: { label: string; file: { label: string }[] }[];
}>();
</script>
@ -160,10 +175,103 @@ defineProps<{
/>
</q-tab-panel>
<q-tab-panel name="attachment">
<EmployerFormAttachment
<UploadFile
hide-action
:readonly="readonly"
:dropdown-list="uploadFileListCustomer"
v-model:status-ocr="statusOcr"
v-model:file="item.file"
:tree-file="
Object.values(
item.file?.reduce<
Record<string, { label: string; file: { label: string }[] }>
>((a, c) => {
const _group = c.group || 'other';
console.log(c);
if (!a[_group]) {
a[_group] = {
label: $t(
uploadFileListCustomer.find((v) => v.value === _group)
?.label || _group,
),
file: [
{
label:
c.name ||
`${c.group}-${c.file?.name || Date.now()}`,
},
],
};
} else {
a[_group].file.push({
label:
c.name || `${c.group}-${c.file?.name || Date.now()}`,
});
}
return a;
}, {}) || {},
)
"
@send-ocr="
async (v: any, f: any) => {
const res = await ocrStore.sendOcr({ file: f });
if (res) {
const map = res.fields.reduce<Record<string, string>>(
(a, c) => {
a[c.name] = c.value;
return a;
},
{},
);
if (!item.citizenId) item.citizenId = map['citizen_id'] || '';
if (!item.address) item.address = map['address'] || '';
if (!customer.firstName)
customer.firstName = map['firstname'] || '';
if (!customer.lastName)
customer.lastName = map['lastname'] || '';
if (!customer.firstNameEN)
customer.firstNameEN = map['firstname_en'] || '';
if (!customer.lastNameEN)
customer.lastNameEN = map['lastname_en'] || '';
if (!customer.birthDate)
customer.birthDate = new Date(map['birth_date'] || '');
}
statusOcr = false;
}
"
@delete-file="
(filename) => {
if (!item.id) return;
customerFormStore.deleteAttachment(
{ branchId: item.id, customerId: item.customerId },
filename,
);
}
"
>
<template #form="{ mode }">
<FormCitizen
v-if="mode === 'citizenId'"
orc
v-model:citizen-id="item.citizenId"
v-model:birth-date="customer.birthDate"
v-model:first-name="customer.firstName"
v-model:first-name-en="customer.firstNameEN"
v-model:last-name="customer.lastName"
v-model:last-name-en="customer.lastNameEN"
v-model:address="item.address"
/>
</template>
</UploadFile>
<!-- <EmployerFormAttachment
:readonly="readonly"
v-model:attachment="item.file"
/>
/> -->
</q-tab-panel>
</q-tab-panels>
</div>

View file

@ -1,16 +1,93 @@
import { QTableProps } from 'quasar';
export const uploadFileList: string[] = [
'ข้อมูลหนังสือการเดินทาง',
'ข้อมูลการตรวจลงตรา',
'ตม.6',
'ใบอนุญาตทำงาน',
'แบบแจ้งการจ้างคนต่างด้าวทำงาน',
'แบบแจ้งเข้าทำงานของคนต่างด้าว',
'ใบคัดประวัติระบบจัดหางาน',
'ใบตอบรับการแจ้งเกี่ยวกับการทำงานของคนต่างด้าว',
'ใบเสร็จรับเงิน',
'อื่นๆ',
export const uploadFileListCustomer: {
label: string;
value: string;
}[] = [
{
label: 'customer.typeFile.citizenId',
value: 'citizenId',
},
{
label: 'customer.typeFile.registrationBook',
value: 'registrationBook',
},
{
label: 'customer.typeFile.houseMap',
value: 'houseMap',
},
{
label: 'customer.typeFile.businessRegistration',
value: 'businessRegistration',
},
{
label: 'customer.typeFile.dbdCertificate',
value: 'dbdCertificate',
},
{
label: 'customer.typeFile.vatRegistrationCertificate',
value: 'vatRegistrationCertificate',
},
{
label: 'customer.typeFile.powerOfAttorney',
value: 'powerOfAttorney',
},
{
label: 'customer.typeFile.others',
value: 'others',
},
];
export const uploadFileListEmployee: {
label: string;
value: string;
}[] = [
{
label: 'customerEmployee.fileType.passport',
value: 'passport',
},
{
label: 'customerEmployee.fileType.visa',
value: 'visa',
},
{
label: 'customerEmployee.fileType.tm6',
value: 'tm6',
},
{
label: 'customerEmployee.fileType.workPermit',
value: 'workPermit',
},
{
label: 'customerEmployee.fileType.noticeJobEmployment',
value: 'noticeJobEmployment',
},
{
label: 'customerEmployee.fileType.noticeJobEntry',
value: 'noticeJobEntry',
},
{
label: 'customerEmployee.fileType.historyJob',
value: 'historyJob',
},
{
label: 'customerEmployee.fileType.acceptJob',
value: 'acceptJob',
},
{
label: 'customerEmployee.fileType.receipt',
value: 'receipt',
},
{
label: 'customerEmployee.fileType.other',
value: 'other',
},
];
export const formMenuIconEmployee = [

View file

@ -2,6 +2,7 @@ import { ref, toRaw, watch } from 'vue';
import { defineStore } from 'pinia';
import { CustomerBranchCreate, CustomerCreate } from 'stores/customer/types';
import { Employee, EmployeeCreate } from 'stores/employee/types';
import { useI18n } from 'vue-i18n';
import useMyBranch from 'stores/my-branch';
import useCustomerStore from 'stores/customer';
@ -9,6 +10,7 @@ import useEmployeeStore from 'stores/employee';
import useFlowStore from 'stores/flow';
export const useCustomerForm = defineStore('form-customer', () => {
const { t } = useI18n();
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const customerStore = useCustomerStore();
@ -46,6 +48,8 @@ export const useCustomerForm = defineStore('form-customer', () => {
editCustomerId?: string;
editCustomerCode?: string;
editCustomerBranchId?: string;
treeFile: { label: string; file: { label: string }[] }[];
formDataOcr: Record<string, any>;
}>({
dialogType: 'info',
dialogOpen: false,
@ -59,6 +63,8 @@ export const useCustomerForm = defineStore('form-customer', () => {
editCustomerId: '',
editCustomerBranchId: '',
defaultCustomerImageUrl: '',
treeFile: [],
formDataOcr: {},
});
watch(
@ -66,6 +72,16 @@ export const useCustomerForm = defineStore('form-customer', () => {
(v) => (defaultFormData.customerType = v.customerType),
);
async function deleteAttachment(
id: { branchId: string; customerId: string },
filename: string,
) {
const res = await customerStore.deleteAttachment(id.branchId, filename);
if (res) {
assignFormData(id.customerId);
}
}
function isFormDataDifferent() {
return (
JSON.stringify(resetFormData) !== JSON.stringify(currentFormData.value)
@ -82,6 +98,8 @@ export const useCustomerForm = defineStore('form-customer', () => {
resetFormData = structuredClone(defaultFormData);
resetFormData.registeredBranchId = branchStore.currentMyBranch?.id || '';
state.value.editCustomerId = '';
state.value.treeFile = [];
return;
}
@ -127,41 +145,59 @@ export const useCustomerForm = defineStore('form-customer', () => {
resetFormData.birthDate = new Date(data.birthDate);
resetFormData.image = null;
resetFormData.customerBranch = data.branch.map((v) => ({
id: v.id,
code: v.code || '',
customerCode: '',
provinceId: v.provinceId,
districtId: v.districtId,
subDistrictId: v.subDistrictId,
wageRate: v.wageRate,
payDate: new Date(v.payDate), // Convert the string to a Date object
saleEmployee: v.saleEmployee,
jobDescription: v.jobDescription,
jobPositionEN: v.jobPositionEN,
jobPosition: v.jobPosition,
businessTypeEN: v.businessTypeEN,
businessType: v.businessType,
employmentOffice: v.employmentOffice,
telephoneNo: v.telephoneNo,
email: v.email,
addressEN: v.addressEN,
address: v.address,
workplaceEN: v.workplaceEN,
workplace: v.workplace,
status: v.status,
customerId: v.customerId,
citizenId: v.citizenId || '',
authorizedCapital: v.authorizedCapital || '',
registerDate: new Date(v.registerDate), // Convert the string to a Date object
registerNameEN: v.registerNameEN || '',
registerName: v.registerName || '',
legalPersonNo: v.legalPersonNo || '',
registerCompanyName: '',
statusSave: true,
contactName: v.contactName || '',
file: undefined,
}));
resetFormData.customerBranch = await Promise.all(
data.branch.map(async (v) => ({
id: v.id,
code: v.code || '',
customerCode: '',
provinceId: v.provinceId,
districtId: v.districtId,
subDistrictId: v.subDistrictId,
wageRate: v.wageRate,
payDate: new Date(v.payDate), // Convert the string to a Date object
saleEmployee: v.saleEmployee,
jobDescription: v.jobDescription,
jobPositionEN: v.jobPositionEN,
jobPosition: v.jobPosition,
businessTypeEN: v.businessTypeEN,
businessType: v.businessType,
employmentOffice: v.employmentOffice,
telephoneNo: v.telephoneNo,
email: v.email,
addressEN: v.addressEN,
address: v.address,
workplaceEN: v.workplaceEN,
workplace: v.workplace,
status: v.status,
customerId: v.customerId,
citizenId: v.citizenId || '',
authorizedCapital: v.authorizedCapital || '',
registerDate: new Date(v.registerDate), // Convert the string to a Date object
registerNameEN: v.registerNameEN || '',
registerName: v.registerName || '',
legalPersonNo: v.legalPersonNo || '',
registerCompanyName: '',
statusSave: true,
contactName: v.contactName || '',
file: await customerStore.listAttachment(v.id).then(async (r) => {
if (r) {
return await Promise.all(
r.map(async (item) => {
const fragment = item.split('-');
const group = fragment.length === 1 ? 'other' : fragment.at(0);
return {
url: await customerStore.getAttachment(v.id, item),
name: item,
group: group,
};
}),
);
}
return [];
}),
})),
);
currentFormData.value = structuredClone(resetFormData);
}
@ -269,11 +305,13 @@ export const useCustomerForm = defineStore('form-customer', () => {
assignFormData,
submitFormCustomer,
addCurrentCustomerBranch,
deleteAttachment,
};
});
export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
const customerStore = useCustomerStore();
const customerFormStore = useCustomerForm();
const defaultFormData: CustomerBranchCreate & { id?: string } = {
code: '',
@ -404,6 +442,10 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
if (!currentFormData.value.id) {
const res = await customerStore.createBranch({
...currentFormData.value,
citizenId:
customerFormStore.currentFormData.customerType === 'CORP'
? undefined
: currentFormData.value.citizenId,
customerId: state.value.currentCustomerId,
});
if (res) return res;
@ -418,8 +460,8 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
return {
state,
initForm,
currentFormData,
initForm,
isFormDataDifferent,
submitForm,
};
@ -467,6 +509,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
zipCode: string;
}
| undefined;
ocr: boolean;
}>({
currentIndex: -1,
statusSavePersonal: false,
@ -484,6 +527,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
editReadonly: false,
infoEmployeePersonCard: [],
formDataEmployeeOwner: undefined,
ocr: false,
});
const defaultFormData: EmployeeCreate = {
@ -783,10 +827,12 @@ export const useEmployeeForm = defineStore('form-employee', () => {
return;
}
const res = await employeeStore.fetchById(id);
const _data = await employeeStore.fetchById(id);
if (res) {
state.value.currentEmployee = res;
if (_data) {
const _attach = await employeeStore.listAttachment(_data.id);
state.value.currentEmployee = _data;
const {
createdAt,
@ -800,47 +846,61 @@ export const useEmployeeForm = defineStore('form-employee', () => {
createdBy,
updatedBy,
profileImageUrl,
...playlond
} = res;
...payload
} = _data;
resetEmployeeData = {
...playlond,
...payload,
provinceId: province?.id,
districtId: district?.id,
subDistrictId: subDistrict?.id,
employeeCheckup: structuredClone(
playlond.employeeCheckup?.length === 0
payload.employeeCheckup?.length === 0
? defaultFormData.employeeCheckup
: playlond.employeeCheckup?.map((item) => ({
: payload.employeeCheckup?.map((item) => ({
...item,
statusSave: true,
})),
),
employeeOtherInfo: structuredClone(
{
...playlond.employeeOtherInfo,
statusSave: !!playlond.employeeOtherInfo?.id ? true : false,
...payload.employeeOtherInfo,
statusSave: !!payload.employeeOtherInfo?.id ? true : false,
} || {},
),
employeeWork: structuredClone(
playlond.employeeWork?.length === 0
payload.employeeWork?.length === 0
? defaultFormData.employeeWork
: playlond.employeeWork?.map((item) => ({
: payload.employeeWork?.map((item) => ({
...item,
statusSave: true,
})),
),
file: _attach
? await Promise.all(
_attach.map(async (name) => {
const fragment = name.split('-');
const group = fragment.length === 1 ? 'other' : fragment.at(0);
return {
url: await employeeStore.getAttachment(_data.id, name),
name,
group,
};
}),
)
: [],
image: null,
};
currentFromDataEmployee.value = structuredClone(resetEmployeeData);
const foundBranch = await customerStore.fetchListCustomeBranchById(
playlond.customerBranchId,
payload.customerBranchId,
);
state.value.currentEmployeeCode = playlond.code;
state.value.currentEmployeeCode = payload.code;
state.value.profileUrl = profileImageUrl || ' ';
state.value.profileUrl = profileImageUrl || '';
profileImageUrl
? (state.value.profileSubmit = true)
@ -849,8 +909,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.formDataEmployeeOwner = { ...foundBranch };
if (
foundBranch.address === playlond.address &&
foundBranch.zipCode === playlond.zipCode
foundBranch.address === payload.address &&
foundBranch.zipCode === payload.zipCode
) {
state.value.formDataEmployeeSameAddr = true;
} else {

View file

@ -16,6 +16,7 @@ import {
import axios from 'axios';
import useFlowStore from '../flow';
import { Employee } from '../employee/types';
import { baseUrl } from '../utils';
const useCustomerStore = defineStore('api-customer', () => {
const flowStore = useFlowStore();
@ -208,6 +209,64 @@ const useCustomerStore = defineStore('api-customer', () => {
return res.data;
}
async function putAttachment(opts: {
branchId: string;
file: File;
filename?: string;
}) {
const res = await api.put(
`/customer-branch/${opts.branchId}/attachment/${opts.filename || opts.file.name}`,
opts.file,
{
headers: { 'X-Rtid': flowStore.rtid, 'Content-Type': opts.file.type },
onUploadProgress: (e) => console.log(e),
},
);
if (res.status >= 400) return false;
return true;
}
async function deleteAttachment(id: string, filename: string) {
const res = await api.delete(
`/customer-branch/${id}/attachment/${filename}`,
);
if (res.status >= 400) return false;
return true;
}
async function listAttachment(id: string) {
const res = await api.get<string[]>(`/customer-branch/${id}/attachment`, {
headers: { 'X-Rtid': flowStore.rtid },
});
if (res.status >= 400) return false;
return res.data;
}
async function getAttachment(id: string, filename: string, download = false) {
const url = `${baseUrl}/customer-branch/${id}/attachment/${filename}`;
const res = await api.get<string>(url);
if (download) {
fetch(res.data)
.then(async (res) => await res.blob())
.then((blob) => {
let a = document.createElement('a');
a.download = filename;
a.href = window.URL.createObjectURL(blob);
a.click();
a.remove();
});
}
return res.data;
}
async function create(
data: CustomerCreate,
flow?: {
@ -395,7 +454,7 @@ const useCustomerStore = defineStore('api-customer', () => {
});
if (!res) return false;
if (file) await addBranchAttachment(res.data.id, { file });
// if (file) await addBranchAttachment(res.data.id, { file });
return res.data;
}
@ -443,7 +502,7 @@ const useCustomerStore = defineStore('api-customer', () => {
},
);
if (file) await addBranchAttachment(id, { file });
// if (file) await addBranchAttachment(id, { file });
if (!res) return false;
@ -472,68 +531,6 @@ const useCustomerStore = defineStore('api-customer', () => {
return false;
}
async function addBranchAttachment(
branchId: string,
payload: BranchAttachmentCreate,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const list: { name: string; file: File }[] = [];
payload.file.forEach((v) => {
let filename = v.name;
if (list.some((v) => v.name === filename)) {
const dotIndex = filename.lastIndexOf('.');
const originalName =
dotIndex !== -1 && !filename.startsWith('.')
? filename.slice(0, dotIndex)
: filename;
const extension =
dotIndex !== -1 && !filename.startsWith('.')
? filename.slice(dotIndex)
: '';
let i = 0;
while (list.some((v) => v.name === filename)) {
filename = `${originalName} (${++i})`;
if (dotIndex !== -1) filename += extension;
}
}
list.push({ name: filename, file: v });
});
const res = await api.post<(BranchAttachment & { uploadUrl: string })[]>(
`/customer-branch/${branchId}/attachment`,
{ file: list.map((v) => v.name) },
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
},
);
await Promise.all(
res.data.map(async (a) => {
const found = list.find((b) => b.name === a.name)!;
await axios
.put(a.uploadUrl, found.file, {
headers: { 'Content-Type': found.file.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e));
}),
);
}
async function fetchListCustomeBranchById(
branchId: string,
flow?: {
@ -576,6 +573,11 @@ const useCustomerStore = defineStore('api-customer', () => {
fetchListCustomeBranchById,
fetchBranchEmployee,
listAttachment,
getAttachment,
putAttachment,
deleteAttachment,
};
});

View file

@ -106,7 +106,12 @@ export type CustomerBranchCreate = {
registerCompanyName: string;
statusSave?: boolean;
file?: File[];
file?: {
name?: string;
group?: string;
url?: string;
file?: File;
}[];
// id?: string;
// code?: string;

View file

@ -16,6 +16,7 @@ import {
import { CustomerBranch } from '../customer/types';
import axios from 'axios';
import useFlowStore from '../flow';
import { baseUrl } from '../utils';
const useEmployeeStore = defineStore('api-employee', () => {
const flowStore = useFlowStore();
@ -31,22 +32,15 @@ const useEmployeeStore = defineStore('api-employee', () => {
return false;
}
async function fetchList(
opts?: {
page?: number;
pageSize?: number;
query?: string;
gender?: string;
status?: Status;
zipCode?: string;
customerId?: string;
},
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function fetchList(opts?: {
page?: number;
pageSize?: number;
query?: string;
gender?: string;
status?: Status;
zipCode?: string;
customerId?: string;
}) {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(opts || {})) {
@ -58,11 +52,7 @@ const useEmployeeStore = defineStore('api-employee', () => {
const res = await api.get<Pagination<Employee[]>>(
`/employee${(params && '?'.concat(query)) || ''}`,
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
},
);
@ -73,58 +63,54 @@ const useEmployeeStore = defineStore('api-employee', () => {
return false;
}
async function create(
data: EmployeeCreate,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const { id, code, image, ...payload } = data;
async function create(data: EmployeeCreate) {
const { id, code, image, file, ...payload } = data;
const res = await api.post<
Employee & { profileImageUrl: string; profileImageUploadUrl: string }
>('/employee', payload, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
});
image &&
(await axios
.put(res.data.profileImageUploadUrl, image, {
headers: { 'Content-Type': image?.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e)));
if (!res) return false;
if (res.data.id) {
if (image) {
await api
.put(`/employee/${res.data.id}/image`, image, {
headers: { 'Content-Type': image?.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e));
}
if (file) {
const attachmentUpload = file.map(async ({ group, file }) => {
if (file) {
const _name = file.name;
const _ext = _name.split('.').at(-1);
let filename = (group || 'other') + '-' + Date.now();
if (_ext) filename = filename + '.' + _ext;
await uploadAttachment(res.data.id, file, filename);
}
});
await Promise.all(attachmentUpload);
}
}
return res.data;
}
async function createEmployeeCheckup(
employeeId: string,
data: EmployeeCheckupCreate,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const { id, statusSave, ...payload } = data;
const res = await api.post<EmployeeCheckupCreate>(
`/employee/${employeeId}/checkup`,
{ ...payload },
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
},
{ headers: { 'X-Rtid': flowStore.rtid } },
);
if (!res) return false;
@ -135,23 +121,12 @@ const useEmployeeStore = defineStore('api-employee', () => {
async function createEmployeeWork(
employeeId: string,
data: EmployeeWorkCreate,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const { id, ...payload } = data;
const res = await api.post<EmployeeWorkCreate>(
`/employee/${employeeId}/work`,
{ ...payload },
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
},
{ headers: { 'X-Rtid': flowStore.rtid } },
);
if (!res) return false;
@ -162,22 +137,13 @@ const useEmployeeStore = defineStore('api-employee', () => {
async function createEmployeeOtherInfo(
employeeId: string,
data: EmployeeOtherCreate,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const { id, statusSave, ...payload } = data;
const res = await api.post<EmployeeOtherCreate>(
`/employee/${employeeId}/other-info`,
{ ...payload },
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
},
);
@ -189,11 +155,6 @@ const useEmployeeStore = defineStore('api-employee', () => {
async function editByIdEmployeeCheckup(
employeeOfId: string,
data: Partial<EmployeeCheckupCreate>,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const {
id,
@ -208,13 +169,7 @@ const useEmployeeStore = defineStore('api-employee', () => {
const res = await api.put<EmployeeCheckupCreate>(
`/employee/${employeeOfId}/checkup/${id}`,
{ ...payload },
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
},
{ headers: { 'X-Rtid': flowStore.rtid } },
);
if (!res) return false;
@ -225,11 +180,6 @@ const useEmployeeStore = defineStore('api-employee', () => {
async function editByIdEmployeeWork(
employeeOfId: string,
data: Partial<EmployeeWorkCreate>,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const {
id,
@ -244,13 +194,7 @@ const useEmployeeStore = defineStore('api-employee', () => {
const res = await api.put<EmployeeWorkCreate>(
`/employee/${employeeOfId}/work/${id}`,
{ ...payload },
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
},
{ headers: { 'X-Rtid': flowStore.rtid } },
);
if (!res) return false;
@ -261,11 +205,6 @@ const useEmployeeStore = defineStore('api-employee', () => {
async function editByIdEmployeeOtherInfo(
employeeOfId: string,
data: Partial<EmployeeOtherCreate>,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const {
id,
@ -280,13 +219,7 @@ const useEmployeeStore = defineStore('api-employee', () => {
const res = await api.put<EmployeeOtherCreate>(
`/employee/${employeeOfId}/other-info/${id}`,
{ ...payload },
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
},
{ headers: { 'X-Rtid': flowStore.rtid } },
);
if (!res) return false;
@ -294,59 +227,50 @@ const useEmployeeStore = defineStore('api-employee', () => {
return res.data;
}
async function editById(
employeeId: string,
data: Partial<EmployeeCreate>,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const { id, code, image, ...payload } = data;
async function editById(employeeId: string, data: Partial<EmployeeCreate>) {
const { id, code, image, file, ...payload } = data;
const res = await api.put<
Employee & { imageUrl: string; profileImageUploadUrl: string }
>(`/employee/${employeeId}`, payload, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
});
image &&
(await axios
.put(res.data.profileImageUploadUrl, image, {
headers: { 'Content-Type': image.type },
if (!res) return false;
if (image) {
await api
.put(`/employee/${employeeId}/image`, image, {
headers: { 'Content-Type': image?.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e)));
.catch((e) => console.error(e));
}
if (file) {
const attachmentUpload = file.map(async ({ group, file }) => {
if (file) {
const _name = file.name;
const _ext = _name.split('.').at(-1);
if (!res) return false;
let filename = (group || 'other') + '-' + Date.now();
if (_ext) filename = filename + '.' + _ext;
await uploadAttachment(employeeId, file, filename);
}
});
await Promise.all(attachmentUpload);
}
return res.data;
}
async function deleteByIdCheckUp(
opts: {
employeeId: string;
checkUpId?: string;
},
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function deleteByIdCheckUp(opts: {
employeeId: string;
checkUpId?: string;
}) {
const res = await api.delete<Employee>(
`/employee/${opts.employeeId}/checkup/${opts.checkUpId}`,
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
},
{ headers: { 'X-Rtid': flowStore.rtid } },
);
if (!res) return false;
@ -355,25 +279,11 @@ const useEmployeeStore = defineStore('api-employee', () => {
return false;
}
async function deleteByIdWork(
opts: {
employeeId: string;
workId?: string;
},
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function deleteByIdWork(opts: { employeeId: string; workId?: string }) {
const res = await api.delete<Employee>(
`/employee/${opts.employeeId}/work/${opts.workId}`,
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
},
);
@ -383,20 +293,9 @@ const useEmployeeStore = defineStore('api-employee', () => {
return false;
}
async function deleteById(
id: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function deleteById(id: string) {
const res = await api.delete<Employee>(`/employee/${id}`, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
});
if (!res) return false;
@ -405,82 +304,38 @@ const useEmployeeStore = defineStore('api-employee', () => {
return false;
}
async function fetchCheckup(
id: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function fetchCheckup(id: string) {
const res = await api.get<EmployeeCheckup[]>(`/employee/${id}/checkup`, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
});
if (res && res.status === 200) {
return res.data;
}
}
async function fetchWork(
id: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function fetchWork(id: string) {
const res = await api.get<EmployeeWork[]>(`/employee/${id}/work`, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
});
if (res && res.status === 200) {
return res.data;
}
}
async function fetchOther(
id: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function fetchOther(id: string) {
const res = await api.get<EmployeeOther>(`/employee/${id}/other-info`, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
});
if (res && res.status === 200) {
return res.data;
}
}
async function getStatsEmployee(
customerBranchId?: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function getStatsEmployee(customerBranchId?: string) {
const res = await api.get(
`/employee/stats${customerBranchId ? '?customerBranchId=' + customerBranchId : ''}/`,
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
},
);
if (res && res.status === 200) {
@ -488,19 +343,11 @@ const useEmployeeStore = defineStore('api-employee', () => {
}
}
async function getStatsEmployeeGender(
opts?: {
customerBranchId?: string;
status?: 'CREATED' | 'ACTIVE' | 'INACTIVE';
query?: string;
},
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function getStatsEmployeeGender(opts?: {
customerBranchId?: string;
status?: 'CREATED' | 'ACTIVE' | 'INACTIVE';
query?: string;
}) {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(opts || {})) {
@ -512,11 +359,7 @@ const useEmployeeStore = defineStore('api-employee', () => {
const res = await api.get(
`/employee/stats/gender${(params && '?'.concat(query)) || ''}`,
{
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
},
);
if (res && res.status === 200) {
@ -524,26 +367,66 @@ const useEmployeeStore = defineStore('api-employee', () => {
}
}
async function getEditHistory(
employeeId: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
async function getEditHistory(employeeId: string) {
const res = await api.get(`/employee/${employeeId}/edit-history`, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
headers: { 'X-Rtid': flowStore.rtid },
});
if (res && res.status === 200) {
return res.data;
}
}
async function listAttachment(employeeId: string) {
const res = await api.get<string[]>(`/employee/${employeeId}/attachment`);
return !res || res.status >= 400 ? false : res.data;
}
async function getAttachment(
employeeId: string,
filename: string,
download = false,
) {
const url = `${baseUrl}/employee/${employeeId}/attachment/${filename}`;
const res = await api.get<string>(url);
if (download) {
fetch(res.data)
.then(async (res) => await res.blob())
.then((blob) => {
let a = document.createElement('a');
a.download = filename;
a.href = window.URL.createObjectURL(blob);
a.click();
a.remove();
});
}
return res.data;
}
async function uploadAttachment(
employeeId: string,
file: File,
filename?: string,
) {
const res = await api.put(
`/employee/${employeeId}/attachment/${filename || file.name}`,
file,
{
headers: {
'X-Rtid': flowStore.rtid,
'Content-Type': file.type,
},
},
);
return !(!res || res.status >= 400);
}
async function deleteAttachment(employeeId: string, filename: string) {
const res = await api.delete(
`/employee/${employeeId}/attachment/${filename}`,
{ headers: { 'X-Rtid': flowStore.rtid } },
);
return !(!res || res.status >= 400);
}
return {
data,
globalOption,
@ -571,6 +454,11 @@ const useEmployeeStore = defineStore('api-employee', () => {
createEmployeeOtherInfo,
editByIdEmployeeOtherInfo,
listAttachment,
getAttachment,
uploadAttachment,
deleteAttachment,
};
});

View file

@ -11,6 +11,7 @@ export type Employee = {
createdByUserId: string;
updatedByUserId: string;
statusOrder: string;
namePrefix: string;
firstName: string;
firstNameEN: string;
lastName: string;
@ -107,6 +108,13 @@ export type EmployeeCreate = {
employeeCheckup?: EmployeeCheckupCreate[];
employeeOtherInfo?: EmployeeOtherCreate;
file?: {
name?: string;
group?: string;
url?: string;
file?: File;
}[];
};
export type EmployeeUpdate = {

View file

@ -18,12 +18,19 @@ const useOcr = defineStore('useOcrStore', () => {
},
) {
const res = await axios
.post(`${baseUrl}/`, payload.file, {
.post<{
document: string;
fields: { name: string; value: string }[];
result: string;
}>(`${baseUrl}/`, payload.file, {
headers: { 'Content-Type': payload.file?.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e));
// ]);
if (!res || res.status >= 400) return false;
return res.data;
}
return {