refactor: use new select

This commit is contained in:
Thanaphon Frappet 2024-12-20 15:56:23 +07:00
parent 2ddc101b89
commit 7043c635d5
2 changed files with 811 additions and 618 deletions

View file

@ -21,20 +21,18 @@ import { useInvoice, useReceipt, usePayment } from 'stores/payment';
import useCustomerStore from 'stores/customer';
import useOptionStore from 'stores/options';
import { useQuotationForm } from './form';
import useOcrStore from 'stores/ocr';
import { deleteItem } from 'stores/utils';
import { runOcr, parseResultMRZ } from 'src/utils/ocr';
// NOTE Import Types
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
import { View } from './types.ts';
import {
EmployeeWorker,
PayCondition,
ProductServiceList,
QuotationPayload,
} from 'src/stores/quotations/types';
import { EmployeeWorker } from 'src/stores/quotations/types';
import { Employee } from 'src/stores/employee/types';
import { Employee, EmployeeWork } from 'src/stores/employee/types';
import { Receipt } from 'src/stores/payment/types';
import {
ProductGroup,
@ -46,24 +44,17 @@ import {
import TableRequest from './TableRequest.vue';
import SelectInput from 'components/shared/SelectInput.vue';
import SwitchItem from 'components/shared/SwitchItem.vue';
import FormEmployeePassport from 'components/03_customer-management/FormEmployeePassport.vue';
import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue';
import FormReferDocument from 'src/components/05_quotation/FormReferDocument.vue';
import { UploadFileGroup, NoticeJobEmployment } from 'components/upload-file';
import FormPerson from 'components/02_personnel-management/FormPerson.vue';
import ProductItem from 'components/05_quotation/ProductItem.vue';
import WorkerItem from 'components/05_quotation/WorkerItem.vue';
import ToggleButton from 'components/button/ToggleButton.vue';
import FormAbout from 'components/05_quotation/FormAbout.vue';
import SelectZone from 'components/shared/SelectZone.vue';
import PersonCard from 'components/shared/PersonCard.vue';
import ImportWorker from './ImportWorker.vue';
import {
AddButton,
SaveButton,
EditButton,
UndoButton,
DeleteButton,
CloseButton,
MainButton,
} from 'components/button';
@ -72,12 +63,6 @@ import QuotationFormProductSelect from './QuotationFormProductSelect.vue';
import QuotationFormInfo from './QuotationFormInfo.vue';
import QuotationFormWorkerSelect from './QuotationFormWorkerSelect.vue';
import QuotationFormWorkerAddDialog from './QuotationFormWorkerAddDialog.vue';
import ProfileBanner from 'components/ProfileBanner.vue';
import DialogForm from 'components/DialogForm.vue';
import {
uploadFileListEmployee,
columnsAttachment,
} from 'src/pages/03_customer-management/constant';
import UploadFileSection from 'src/components/upload-file/UploadFileSection.vue';
import { columnPaySplit } from './constants';
@ -111,7 +96,6 @@ const route = useRoute();
const useReceiptStore = useReceipt();
const configStore = useConfigStore();
const productServiceStore = useProductServiceStore();
const employeeFormStore = useEmployeeForm();
const customerStore = useCustomerStore();
const quotationForm = useQuotationForm();
const quotationStore = useQuotationStore();
@ -120,7 +104,6 @@ const paymentStore = usePayment();
const optionStore = useOptionStore();
const requestStore = useRequestList();
const { t, locale } = useI18n();
const ocrStore = useOcrStore();
const $q = useQuasar();
const openQuotation = ref<boolean>(false);
const formMetadata = ref();
@ -145,8 +128,6 @@ const receiptList = ref<Receipt[]>([]);
const templateForm = ref<string>('');
const templateFormOption = ref<{ label: string; value: string }[]>([]);
const refSelectZoneEmployee = ref<InstanceType<typeof SelectZone>>();
const mrz = ref<Awaited<ReturnType<typeof parseResultMRZ>>>();
const toggleWorker = ref(true);
const tempTableProduct = ref<ProductServiceList[]>([]);
const tempPaySplitCount = ref(0);
@ -155,14 +136,12 @@ const tempPaySplit = ref<
>([]);
const currentQuotationId = ref<string | undefined>(undefined);
const date = ref();
const preSelectedWorker = ref<Employee[]>([]);
const readonly = computed(() => {
return !(
quotationFormState.value.mode === 'create' ||
quotationFormState.value.mode === 'edit'
);
});
const test = ref<boolean>(false);
const selectedWorker = ref<
(Employee & {
@ -175,30 +154,47 @@ const selectedWorker = ref<
}[];
})[]
>([]);
const selectedWorkerItem = computed(() =>
selectedWorker.value.map((e) => ({
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName} ${e.lastName}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
const selectedWorkerItem = computed(() => {
return [
...selectedWorker.value.map((e) => ({
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
status: e.status,
})),
);
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName} ${e.lastName}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
status: e.status,
})),
...newWorkerList.value.map((v: any) => ({
foreignRefNo: v.passportNo,
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
})),
];
});
const workerList = ref<Employee[]>([]);
const selectedProductGroup = ref('');
@ -300,36 +296,7 @@ const pageState = reactive({
const productList = ref<Partial<Record<ProductGroupId, Product[]>>>({});
const serviceList = ref<Partial<Record<ProductGroupId, Service[]>>>({});
const productGroup = ref<ProductGroup[]>([]);
const { state: employeeFormState, currentFromDataEmployee } =
storeToRefs(employeeFormStore);
const selectedGroupSub = ref<'product' | 'service' | null>(null);
const formDataEmployee = ref<
EmployeeWorker & {
attachment?: {
name?: string;
group?: string;
url?: string;
file?: File;
_meta?: Record<string, any>;
}[];
}
>({
passportNo: '',
documentExpireDate: new Date(),
lastNameEN: '',
lastName: '',
middleNameEN: '',
middleName: '',
firstNameEN: '',
firstName: '',
namePrefix: '',
nationality: '',
gender: '',
dateOfBirth: new Date(),
attachment: [],
});
const productServiceList = ref<
Required<QuotationPayload['productServiceList'][number]>[]
@ -599,16 +566,17 @@ async function convertDataToFormSubmit() {
});
quotationFormData.value.worker = JSON.parse(
JSON.stringify(
selectedWorker.value.map((v) => {
if (v.id === undefined) {
const { attachment, ...payload } = v;
return payload;
} else {
JSON.stringify([
...selectedWorker.value.map((v) => {
{
return v.id;
}
}),
),
...newWorkerList.value.map((v) => {
const { attachment, ...payload } = v;
return payload;
}),
]),
);
quotationFormData.value.paySplit = JSON.parse(
@ -639,6 +607,7 @@ async function convertDataToFormSubmit() {
remark: quotationFormData.value.remark || '',
};
newWorkerList.value = [];
const res = await quotationForm.submitQuotation();
if (res === true) {
@ -673,26 +642,6 @@ async function getAllProduct(
if (ret) productList.value[groupId] = ret.result;
}
function setDefaultFormEmployee() {
formDataEmployee.value = {
passportNo: '',
documentExpireDate: new Date(),
lastNameEN: '',
lastName: '',
middleNameEN: '',
middleName: '',
firstNameEN: '',
firstName: '',
namePrefix: '',
nationality: '',
gender: '',
dateOfBirth: new Date(),
attachment: [],
};
employeeFormState.value.dialogModal = false;
}
async function getAllService(
groupId: string,
opts?: { force?: boolean; page?: number; pageSize?: number; query?: string },
@ -712,14 +661,6 @@ async function getAllService(
if (ret) serviceList.value[groupId] = ret.result;
}
function triggerCreateEmployee() {
employeeFormStore.resetFormDataEmployee(true);
setDefaultFormEmployee();
employeeFormState.value.dialogType = 'create';
employeeFormState.value.dialogModal = true;
employeeFormState.value.isEmployeeEdit = true;
}
async function triggerSelectEmployeeDialog() {
pageState.employeeModal = true;
await nextTick();
@ -2275,10 +2216,11 @@ watch(
v-model:open="pageState.employeeModal"
@success="
(v) => {
selectedWorker = v;
console.log(v.newWorker);
selectedWorker = v.worker;
newWorkerList = v.newWorker;
}
"
@trigger-create-employee="() => triggerCreateEmployee()"
/>
<!-- add product service -->
@ -2311,438 +2253,6 @@ watch(
></QuotationFormProductSelect>
</div>
<!-- NOTE: START - Employee Add Form -->
<DialogForm
hide-footer
ref="formDialogRef"
v-model:modal="employeeFormState.dialogModal"
:title="$t('form.title.create', { name: $t('customer.employee') })"
:submit="
() => {
quotationForm.injectNewEmployee({ data: formDataEmployee }, () =>
setDefaultFormEmployee(),
);
}
"
:close="
() => {
employeeFormState.dialogModal = false;
}
"
>
<div
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
'q-mx-md q-my-sm': !$q.screen.gt.sm,
}"
>
<ProfileBanner
prefix="dialog"
active
useToggle
color="white"
icon="mdi-account-plus-outline"
:bg-color="
employeeFormState.profileUrl
? 'white'
: 'linear-gradient(135deg, rgba(43,137,223,1) 0%, rgba(230,51,81,1) 100%)'
"
v-model:current-tab="employeeFormState.currentTab"
v-model:toggle-status="currentFromDataEmployee.status"
fallbackCover="/images/employee-banner.png"
:img="employeeFormState.profileUrl || `/images/employee-avatar.png`"
:toggleTitle="$t('status.title')"
hideFade
@view="
() => {
employeeFormState.imageDialog = true;
employeeFormState.isImageEdit = false;
}
"
@edit="
employeeFormState.imageDialog = employeeFormState.isImageEdit = true
"
@update:toggle-status="
() => {
currentFromDataEmployee.status =
currentFromDataEmployee.status === 'CREATED'
? 'INACTIVE'
: 'CREATED';
}
"
/>
</div>
<div
class="col"
:class="{
'q-px-lg q-pb-lg': $q.screen.gt.sm,
'q-px-md q-pb-sm': !$q.screen.gt.sm,
}"
>
<div
style="overflow-y: auto"
class="row full-width full-height surface-1 rounded bordered relative-position"
>
<div
:class="{
'q-py-md q-px-lg': $q.screen.gt.sm,
'q-py-sm q-px-lg': !$q.screen.gt.sm,
}"
style="position: absolute; z-index: 99999; top: 0; right: 0"
>
<div
v-if="currentFromDataEmployee.status !== 'INACTIVE'"
class="surface-1 row rounded"
>
<UndoButton
v-if="
employeeFormState.isEmployeeEdit &&
employeeFormState.dialogType !== 'create'
"
id="btn-info-basic-undo"
icon-only
@click="
() => {
employeeFormStore.resetFormDataEmployee();
employeeFormState.isEmployeeEdit = false;
employeeFormState.dialogType = 'info';
}
"
type="button"
/>
<SaveButton
v-if="employeeFormState.isEmployeeEdit"
id="btn-info-basic-save"
icon-only
type="submit"
/>
<EditButton
v-if="!employeeFormState.isEmployeeEdit"
id="btn-info-basic-edit"
icon-only
@click="
() => {
employeeFormState.isEmployeeEdit = true;
employeeFormState.dialogType = 'edit';
}
"
type="button"
/>
<DeleteButton
v-if="!employeeFormState.isEmployeeEdit"
id="btn-info-basic-delete"
icon-only
/>
</div>
</div>
<div
class="col-12 full-height q-col-gutter-sm q-py-md q-pl-md q-pr-sm"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-py-md q-px-lg': !$q.screen.gt.sm,
}"
id="branch-form"
style="overflow-y: auto"
>
<FormReferDocument
title="form.field.basicInformation"
prefixId="dialog"
dense
v-model:passport-no="formDataEmployee.passportNo"
v-model:document-expire-date="formDataEmployee.documentExpireDate"
class="q-mb-md"
/>
<FormPerson
id="form-personal"
prefix-id="form-employee"
dense
outlined
employee
separator
hideNameEn
title="personnel.form.personalInformation"
class="q-mb-md"
v-model:prefix-name="formDataEmployee.namePrefix"
v-model:first-name="formDataEmployee.firstName"
v-model:mid-name="formDataEmployee.middleName"
v-model:last-name="formDataEmployee.lastName"
v-model:birth-date="formDataEmployee.dateOfBirth"
v-model:gender="formDataEmployee.gender"
v-model:nationality="formDataEmployee.nationality"
/>
<UploadFileGroup
show-title
v-model="formDataEmployee.attachment"
hide-action
@submit="
async (group, allMeta) => {
if (allMeta === undefined) return;
if (group === 'passport') {
const fullName = allMeta['full_name'].split(' ');
let tempValue: {
oldData: { nameField: string; value: string }[];
newData: { nameField: string; value: string }[];
} = { oldData: [], newData: [] };
if (
formDataEmployee.gender !== '' &&
formDataEmployee.gender !== allMeta['sex']
) {
tempValue.oldData.push({
nameField: $t('form.gender'),
value: $t(`general.${formDataEmployee.gender}`),
});
tempValue.newData.push({
nameField: $t('form.gender'),
value: $t(`general.${allMeta['sex']}`),
});
}
if (formDataEmployee.firstName !== '') {
tempValue.oldData.push({
nameField: $t('personnel.form.firstName'),
value: formDataEmployee.firstName,
});
tempValue.newData.push({
nameField: $t('personnel.form.firstName'),
value: fullName[0],
});
}
if (formDataEmployee.lastName !== '') {
tempValue.oldData.push({
nameField: $t('personnel.form.lastName'),
value: formDataEmployee.lastName,
});
tempValue.newData.push({
nameField: $t('personnel.form.lastName'),
value: fullName[1],
});
}
if (formDataEmployee.passportNo !== '') {
tempValue.oldData.push({
nameField: $t('customerEmployee.form.passportNo'),
value: formDataEmployee.passportNo || '',
});
tempValue.newData.push({
nameField: $t('customerEmployee.form.passportNo'),
value: allMeta['doc_number'],
});
}
if (formDataEmployee.nationality !== '') {
tempValue.oldData.push({
nameField: $t('general.nationality'),
value: formDataEmployee.nationality || '',
});
tempValue.newData.push({
nameField: $t('general.nationality'),
value: allMeta['nationality'],
});
}
dialogCheckData({
action: async () => {
formDataEmployee.gender = allMeta['sex'];
formDataEmployee.firstName = fullName[0];
formDataEmployee.lastName = fullName[1];
formDataEmployee.passportNo = allMeta['doc_number'];
formDataEmployee.nationality = allMeta['nationality'];
},
checkData: () => {
return tempValue;
},
cancel: () => {
if (!formDataEmployee.gender) {
formDataEmployee.gender = allMeta['gender'];
}
if (!formDataEmployee.firstName) {
formDataEmployee.firstName = fullName[0];
}
if (!formDataEmployee.firstName) {
formDataEmployee.firstName = fullName[0];
}
if (!formDataEmployee.lastName) {
formDataEmployee.lastName = fullName[1];
}
if (!formDataEmployee.passportNo) {
formDataEmployee.passportNo = allMeta['doc_number'];
}
if (!formDataEmployee.nationality) {
formDataEmployee.nationality = allMeta['nationality'];
}
},
});
}
}
"
:menu="uploadFileListEmployee"
:columns="columnsAttachment"
:ocr="
async (group, file) => {
if (group === 'passport') {
mrz = await runOcr(file, parseResultMRZ);
if (mrz !== null) {
const mapMrz = Object.entries(mrz.result || {}).map(
([key, value]) => ({
name: key,
value: value,
}),
);
const tempValue = {
status: true,
group,
meta: mapMrz,
};
return tempValue;
}
}
if (group === 'visa') {
const res = await ocrStore.sendOcr({
file: file,
category: group,
});
if (res) {
const tempValue = {
status: true,
group,
meta: res.fields,
};
return tempValue;
}
}
return { status: true, group, meta: [] };
}
"
:delete-item="
async () => {
return true;
}
"
:get-file-list="
async (group: 'passport' | 'visa' | 'attachment') => {
if (!!currentFromDataEmployee.id && group !== 'attachment') {
const resMeta = await employeeStore.getMetaList({
parentId: currentFromDataEmployee.id,
group,
});
const tempValue = resMeta.map(async (i: any) => {
return {
_meta: { ...i },
name: `${group}-${dateFormat(i.expireDate)}` || '',
group: group,
url: await employeeStore.getFile({
parentId: currentFromDataEmployee.id || '',
group,
fileId: i.id,
}),
file: undefined,
};
});
return await waitAll(tempValue);
} else {
const res = await employeeStore.listAttachment({
parentId: currentFromDataEmployee.id || '',
});
const tempValue = (res as string[]).map(async (i: any) => {
return {
_meta: { id: i, name: i },
name: i || '',
group: group,
url: await employeeStore.getAttachment({
parentId: currentFromDataEmployee.id || '',
name: i,
}),
file: undefined,
};
});
return await waitAll(tempValue);
}
}
"
>
<template #form="{ mode, meta, isEdit }">
<FormEmployeePassport
v-if="mode === 'passport' && meta"
prefix-id="drawer-info-employee"
id="form-passport"
dense
outlined
separator
ocr
:title="$t('customerEmployee.form.group.passport')"
:readonly="!isEdit"
v-model:birth-country="meta.birthCountry"
v-model:previous-passportRef="meta.previousPassportRef"
v-model:issue-place="meta.issuePlace"
v-model:issue-country="meta.issueCountry"
v-model:issue-date="meta.issueDate"
v-model:type="meta.type"
v-model:expire-date="meta.expireDate"
v-model:birth-date="meta.birthDate"
v-model:worker-status="meta.workerStatus"
v-model:nationality="meta.nationality"
v-model:gender="meta.gender"
v-model:last-name-en="meta.lastNameEN"
v-model:last-name="meta.lastName"
v-model:middle-name-en="meta.middleNameEN"
v-model:middle-name="meta.middleName"
v-model:first-name-en="meta.firstNameEN"
v-model:first-name="meta.firstName"
v-model:name-prefix="meta.namePrefix"
v-model:passport-number="meta.number"
/>
<FormEmployeeVisa
v-if="mode === 'visa' && meta"
prefix-id="drawer-info-employee"
id="form-visa"
ocr
dense
outlined
title="customerEmployee.form.group.visa"
:readonly="!isEdit"
v-model:arrival-at="meta.arrivalAt"
v-model:arrival-tm-no="meta.arrivalTMNo"
v-model:arrival-tm="meta.arrivalTM"
v-model:mrz="meta.mrz"
v-model:entry-count="meta.entryCount"
v-model:issue-place="meta.issuePlace"
v-model:issue-country="meta.issueCountry"
v-model:issueDate="meta.issueDate"
v-model:type="meta.type"
v-model:expire-date="meta.expireDate"
v-model:visa-issue-date="meta.issueDate"
v-model:visa-expiry-date="meta.expireDate"
v-model:remark="meta.remark"
v-model:worker-type="meta.workerType"
v-model:number="meta.number"
/>
<NoticeJobEmployment v-if="mode === 'noticeJobEmployment'" />
</template>
</UploadFileGroup>
</div>
</div>
</div>
</DialogForm>
<!-- NOTE: END - Employee Add Form -->
<QuotationFormWorkerAddDialog
v-if="quotationFormState.source"
:disabled-worker-id="selectedWorker.map((v) => v.id)"

View file

@ -1,44 +1,116 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { reactive, ref, watch } from 'vue';
import { calculateAge } from 'src/utils/datetime';
// NOTE: Import stores
import { dialog } from 'stores/utils';
import useOptionStore from 'src/stores/options';
import useEmployeeStore from 'src/stores/employee';
import { useQuotationStore } from 'src/stores/quotations';
import { Employee } from 'src/stores/employee/types';
import { useEmployeeForm } from 'src/pages/03_customer-management/form';
import { waitAll } from 'src/stores/utils';
import { calculateAge, dateFormatJS } from 'src/utils/datetime';
import { dialogCheckData } from 'stores/utils';
import { useQuotationForm } from 'pages/05_quotation/form';
import { CancelButton, MainButton } from 'components/button';
// NOTE Import Types
import { Employee } from 'src/stores/employee/types';
import { EmployeeWorker } from 'src/stores/quotations/types';
import { runOcr, parseResultMRZ } from 'src/utils/ocr';
import useOcrStore from 'stores/ocr';
// NOTE: Import Components
import {
SaveButton,
EditButton,
UndoButton,
DeleteButton,
MainButton,
CancelButton,
} from 'components/button';
import DialogContainer from 'components/dialog/DialogContainer.vue';
import DialogHeader from 'components/dialog/DialogHeader.vue';
import ImportWorker from './ImportWorker.vue';
import PersonCard from 'src/components/shared/PersonCard.vue';
import { QuotationFull, EmployeeWorker } from 'src/stores/quotations/types';
import NewPersonCard from 'src/components/shared/NewPersonCard.vue';
import { Lang } from 'src/utils/ui';
import NoData from 'src/components/NoData.vue';
import FormEmployeePassport from 'components/03_customer-management/FormEmployeePassport.vue';
import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue';
import FormReferDocument from 'src/components/05_quotation/FormReferDocument.vue';
import { UploadFileGroup, NoticeJobEmployment } from 'components/upload-file';
import FormPerson from 'components/02_personnel-management/FormPerson.vue';
import ProfileBanner from 'components/ProfileBanner.vue';
import DialogForm from 'components/DialogForm.vue';
import {
uploadFileListEmployee,
columnsAttachment,
} from 'src/pages/03_customer-management/constant';
import { storeToRefs } from 'pinia';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const { t } = useI18n();
const employeeFormStore = useEmployeeForm();
const quotationForm = useQuotationForm();
const { locale } = useI18n();
const ocrStore = useOcrStore();
const props = defineProps<{
customerBranchId?: string;
disabledWorkerId?: string[];
preselectWorker?: Employee[];
}>();
const { state: employeeFormState, currentFromDataEmployee } =
storeToRefs(employeeFormStore);
const { newWorkerList } = storeToRefs(quotationForm);
const mrz = ref<Awaited<ReturnType<typeof parseResultMRZ>>>();
const formDataEmployee = ref<
EmployeeWorker & {
attachment?: {
name?: string;
group?: string;
url?: string;
file?: File;
_meta?: Record<string, any>;
}[];
}
>({
passportNo: '',
documentExpireDate: new Date(),
lastNameEN: '',
lastName: '',
middleNameEN: '',
middleName: '',
firstNameEN: '',
firstName: '',
namePrefix: '',
nationality: '',
gender: '',
dateOfBirth: new Date(),
attachment: [],
});
const props = withDefaults(
defineProps<{
customerBranchId?: string;
disabledWorkerId?: string[];
preselectWorker?: Employee[];
}>(),
{},
);
const emits = defineEmits<{
(e: 'triggerCreateEmployee'): void;
(e: 'success', workerSelected: Employee[]): void;
(
e: 'success',
data: {
worker: Employee[];
newWorker: EmployeeWorker[];
},
): void;
}>();
const optionStore = useOptionStore();
const employeeStore = useEmployeeStore();
const open = defineModel<boolean>('open', { default: false });
const newWorkerList = defineModel<EmployeeWorker[]>('newWorkerList', {
default: [],
});
const workerSelected = ref<Employee[]>([]);
const workerList = ref<Employee[]>([]);
const importWorkerCriteria = ref<{
@ -55,6 +127,49 @@ const state = reactive({
search: '',
});
function removeNewWorker(index: number) {
dialog({
color: 'negative',
icon: 'mdi-trash-can-outline',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
newWorkerList.value.splice(index, 1);
},
cancel: () => {},
});
}
function triggerCreateEmployee() {
employeeFormStore.resetFormDataEmployee(true);
setDefaultFormEmployee();
employeeFormState.value.dialogType = 'create';
employeeFormState.value.dialogModal = true;
employeeFormState.value.isEmployeeEdit = true;
}
function setDefaultFormEmployee() {
formDataEmployee.value = {
passportNo: '',
documentExpireDate: new Date(),
lastNameEN: '',
lastName: '',
middleNameEN: '',
middleName: '',
firstNameEN: '',
firstName: '',
namePrefix: '',
nationality: '',
gender: '',
dateOfBirth: new Date(),
attachment: [],
};
employeeFormState.value.dialogModal = false;
}
function clean() {
workerList.value = [];
workerSelected.value = [];
@ -163,7 +278,7 @@ watch(() => state.search, getWorkerList);
clickable
class="row items-center"
style="white-space: nowrap"
@click.stop="() => emits('triggerCreateEmployee')"
@click.stop="() => triggerCreateEmployee()"
>
<q-icon
size="xs"
@ -215,67 +330,168 @@ watch(() => state.search, getWorkerList);
</q-input>
</section>
<!-- wrapper -->
<div class="col scroll">
<section
:class="{ ['items-center']: workerList.length === 0 }"
class="row q-col-gutter-md"
<div class="col q-gutter-y-md scroll">
<q-expansion-item
v-if="newWorkerList.length !== 0"
for="item-up"
id="item-up"
dense
class="overflow-hidden"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
default-opened
>
<div
style="display: inline-block; margin-inline: auto"
v-if="workerList.length === 0"
>
<NoData :not-found="!!state.search" />
</div>
<div
:key="emp.id"
v-for="(emp, index) in workerList.map((data) => ({
...data,
_selectedIndex: selectedIndex(data),
}))"
class="col-2"
>
<button
class="selectable-item full-width"
:class="{
['selectable-item__selected']: emp._selectedIndex !== -1,
['selectable-item__disabled']: disabledWorkerId?.some(
(id) => id === emp.id,
),
}"
@click="toggleSelect(emp)"
<template v-slot:header>
<section class="row items-center full-width">
<div class="row items-center q-pr-md q-py-sm">
<span
class="text-weight-medium q-mr-md"
style="font-size: 18px"
>
{{ $t('quotation.newCustomer') }}
</span>
</div>
</section>
</template>
<div class="full-width q-pt-md">
<section
:class="{ ['items-center']: workerList.length === 0 }"
class="row q-col-gutter-md"
>
<span class="selectable-item__pos">
{{ emp._selectedIndex + 1 }}
</span>
<PersonCard
no-action
class="full-width"
:prefix-id="'employee-' + index"
:data="{
name:
locale === Lang.English
? `${emp.firstNameEN} ${emp.lastNameEN}`
: `${emp.firstName} ${emp.lastName}`,
code: emp.employeePassport?.at(0)?.number || '-',
female: emp.gender === 'female',
male: emp.gender === 'male',
img: getEmployeeImageUrl(emp),
fallbackImg: '/images/employee-avatar.png',
detail: [
{
icon: 'mdi-passport',
value: optionStore.mapOption(emp.nationality),
},
{
icon: 'mdi-clock-outline',
value: calculateAge(emp.dateOfBirth),
},
],
}"
/>
</button>
<div
style="display: inline-block; margin-inline: auto"
v-if="workerList.length === 0"
>
<NoData :not-found="!!state.search" />
</div>
<div
:key="emp.id"
v-for="(emp, index) in newWorkerList.map((data) => ({
...data,
}))"
class="col-2"
>
<button
class="selectable-item full-width selectable-item__selected"
>
<NewPersonCard
@cancel="(i) => removeNewWorker(i)"
no-action
class="full-width"
:prefix-id="'employee-' + index"
:data="{
name:
locale === Lang.English
? `${emp.firstNameEN} ${emp.lastNameEN}`
: `${emp.firstName} ${emp.lastName}`,
female: emp.gender === 'female',
male: emp.gender === 'male',
img: '/images/employee-avatar.png',
index: index,
detail: [
{
icon: 'mdi-passport',
value: optionStore.mapOption(emp.nationality),
},
{
icon: 'mdi-clock-outline',
value: calculateAge(emp.dateOfBirth),
},
],
}"
/>
</button>
</div>
</section>
</div>
</section>
</q-expansion-item>
<q-expansion-item
for="item-up"
id="item-up"
dense
class="overflow-hidden"
switch-toggle-side
default-opened
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class=""
>
<template v-slot:header>
<section class="row items-center full-width">
<div class="row items-center q-pr-md q-py-sm">
<span
class="text-weight-medium q-mr-md"
style="font-size: 18px"
>
{{ $t('quotation.customer') }}
</span>
</div>
</section>
</template>
<div class="full-width q-pt-md">
<section
:class="{ ['items-center']: workerList.length === 0 }"
class="row q-col-gutter-md scroll"
>
<div
style="display: inline-block; margin-inline: auto"
v-if="workerList.length === 0"
>
<NoData :not-found="!!state.search" />
</div>
<div
:key="emp.id"
v-for="(emp, index) in workerList.map((data) => ({
...data,
_selectedIndex: selectedIndex(data),
}))"
class="col-2"
>
<button
class="selectable-item full-width"
:class="{
['selectable-item__selected']: emp._selectedIndex !== -1,
['selectable-item__disabled']: disabledWorkerId?.some(
(id) => id === emp.id,
),
}"
@click="toggleSelect(emp)"
>
<PersonCard
no-action
class="full-width"
:prefix-id="'employee-' + index"
:data="{
name:
locale === Lang.English
? `${emp.firstNameEN} ${emp.lastNameEN}`
: `${emp.firstName} ${emp.lastName}`,
code: emp.employeePassport?.at(0)?.number || '-',
female: emp.gender === 'female',
male: emp.gender === 'male',
img: getEmployeeImageUrl(emp),
fallbackImg: '/images/employee-avatar.png',
detail: [
{
icon: 'mdi-passport',
value: optionStore.mapOption(emp.nationality),
},
{
icon: 'mdi-clock-outline',
value: calculateAge(emp.dateOfBirth),
},
],
}"
/>
</button>
</div>
</section>
</div>
</q-expansion-item>
</div>
</div>
<template #footer>
@ -285,16 +501,483 @@ watch(() => state.search, getWorkerList);
icon="mdi-check"
color="207 96% 32%"
solid
@click="emits('success', workerSelected), (open = false)"
@click="
emits('success', {
worker: workerSelected,
newWorker: newWorkerList,
}),
(open = false)
"
>
{{ $t('general.select', { msg: $t('quotation.employeeList') }) }}
</MainButton>
</div>
</template>
</DialogContainer>
<!-- NOTE: START - Employee Add Form -->
<DialogForm
hide-footer
ref="formDialogRef"
v-model:modal="employeeFormState.dialogModal"
:title="$t('form.title.create', { name: $t('customer.employee') })"
:submit="
() => {
quotationForm.injectNewEmployee({ data: formDataEmployee }, () =>
setDefaultFormEmployee(),
);
}
"
:close="
() => {
employeeFormState.dialogModal = false;
}
"
>
<div
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
'q-mx-md q-my-sm': !$q.screen.gt.sm,
}"
>
<ProfileBanner
prefix="dialog"
active
useToggle
color="white"
icon="mdi-account-plus-outline"
:bg-color="
employeeFormState.profileUrl
? 'white'
: 'linear-gradient(135deg, rgba(43,137,223,1) 0%, rgba(230,51,81,1) 100%)'
"
v-model:current-tab="employeeFormState.currentTab"
v-model:toggle-status="currentFromDataEmployee.status"
fallbackCover="/images/employee-banner.png"
:img="employeeFormState.profileUrl || `/images/employee-avatar.png`"
:toggleTitle="$t('status.title')"
hideFade
@view="
() => {
employeeFormState.imageDialog = true;
employeeFormState.isImageEdit = false;
}
"
@edit="
employeeFormState.imageDialog = employeeFormState.isImageEdit = true
"
@update:toggle-status="
() => {
currentFromDataEmployee.status =
currentFromDataEmployee.status === 'CREATED'
? 'INACTIVE'
: 'CREATED';
}
"
/>
</div>
<div
class="col"
:class="{
'q-px-lg q-pb-lg': $q.screen.gt.sm,
'q-px-md q-pb-sm': !$q.screen.gt.sm,
}"
>
<div
style="overflow-y: auto"
class="row full-width full-height surface-1 rounded bordered relative-position"
>
<div
:class="{
'q-py-md q-px-lg': $q.screen.gt.sm,
'q-py-sm q-px-lg': !$q.screen.gt.sm,
}"
style="position: absolute; z-index: 99999; top: 0; right: 0"
>
<div
v-if="currentFromDataEmployee.status !== 'INACTIVE'"
class="surface-1 row rounded"
>
<UndoButton
v-if="
employeeFormState.isEmployeeEdit &&
employeeFormState.dialogType !== 'create'
"
id="btn-info-basic-undo"
icon-only
@click="
() => {
employeeFormStore.resetFormDataEmployee();
employeeFormState.isEmployeeEdit = false;
employeeFormState.dialogType = 'info';
}
"
type="button"
/>
<SaveButton
v-if="employeeFormState.isEmployeeEdit"
id="btn-info-basic-save"
icon-only
type="submit"
/>
<EditButton
v-if="!employeeFormState.isEmployeeEdit"
id="btn-info-basic-edit"
icon-only
@click="
() => {
employeeFormState.isEmployeeEdit = true;
employeeFormState.dialogType = 'edit';
}
"
type="button"
/>
<DeleteButton
v-if="!employeeFormState.isEmployeeEdit"
id="btn-info-basic-delete"
icon-only
/>
</div>
</div>
<div
class="col-12 full-height q-col-gutter-sm q-py-md q-pl-md q-pr-sm"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-py-md q-px-lg': !$q.screen.gt.sm,
}"
id="branch-form"
style="overflow-y: auto"
>
<FormReferDocument
title="form.field.basicInformation"
prefixId="dialog"
dense
v-model:passport-no="formDataEmployee.passportNo"
v-model:document-expire-date="formDataEmployee.documentExpireDate"
class="q-mb-md"
/>
<FormPerson
id="form-personal"
prefix-id="form-employee"
dense
outlined
employee
separator
hideNameEn
title="personnel.form.personalInformation"
class="q-mb-md"
v-model:prefix-name="formDataEmployee.namePrefix"
v-model:first-name="formDataEmployee.firstName"
v-model:mid-name="formDataEmployee.middleName"
v-model:last-name="formDataEmployee.lastName"
v-model:birth-date="formDataEmployee.dateOfBirth"
v-model:gender="formDataEmployee.gender"
v-model:nationality="formDataEmployee.nationality"
/>
<UploadFileGroup
show-title
v-model="formDataEmployee.attachment"
hide-action
@submit="
async (group, allMeta) => {
if (allMeta === undefined) return;
if (group === 'passport') {
const fullName = allMeta['full_name'].split(' ');
let tempValue: {
oldData: { nameField: string; value: string }[];
newData: { nameField: string; value: string }[];
} = { oldData: [], newData: [] };
if (
formDataEmployee.gender !== '' &&
formDataEmployee.gender !== allMeta['sex']
) {
tempValue.oldData.push({
nameField: $t('form.gender'),
value: $t(`general.${formDataEmployee.gender}`),
});
tempValue.newData.push({
nameField: $t('form.gender'),
value: $t(`general.${allMeta['sex']}`),
});
}
if (formDataEmployee.firstName !== '') {
tempValue.oldData.push({
nameField: $t('personnel.form.firstName'),
value: formDataEmployee.firstName,
});
tempValue.newData.push({
nameField: $t('personnel.form.firstName'),
value: fullName[0],
});
}
if (formDataEmployee.lastName !== '') {
tempValue.oldData.push({
nameField: $t('personnel.form.lastName'),
value: formDataEmployee.lastName,
});
tempValue.newData.push({
nameField: $t('personnel.form.lastName'),
value: fullName[1],
});
}
if (formDataEmployee.passportNo !== '') {
tempValue.oldData.push({
nameField: $t('customerEmployee.form.passportNo'),
value: formDataEmployee.passportNo || '',
});
tempValue.newData.push({
nameField: $t('customerEmployee.form.passportNo'),
value: allMeta['doc_number'],
});
}
if (formDataEmployee.nationality !== '') {
tempValue.oldData.push({
nameField: $t('general.nationality'),
value: formDataEmployee.nationality || '',
});
tempValue.newData.push({
nameField: $t('general.nationality'),
value: allMeta['nationality'],
});
}
dialogCheckData({
action: async () => {
formDataEmployee.gender = allMeta['sex'];
formDataEmployee.firstName = fullName[0];
formDataEmployee.lastName = fullName[1];
formDataEmployee.passportNo = allMeta['doc_number'];
formDataEmployee.nationality = allMeta['nationality'];
},
checkData: () => {
return tempValue;
},
cancel: () => {
if (!formDataEmployee.gender) {
formDataEmployee.gender = allMeta['gender'];
}
if (!formDataEmployee.firstName) {
formDataEmployee.firstName = fullName[0];
}
if (!formDataEmployee.firstName) {
formDataEmployee.firstName = fullName[0];
}
if (!formDataEmployee.lastName) {
formDataEmployee.lastName = fullName[1];
}
if (!formDataEmployee.passportNo) {
formDataEmployee.passportNo = allMeta['doc_number'];
}
if (!formDataEmployee.nationality) {
formDataEmployee.nationality = allMeta['nationality'];
}
},
});
}
}
"
:menu="uploadFileListEmployee"
:columns="columnsAttachment"
:ocr="
async (group, file) => {
if (group === 'passport') {
mrz = await runOcr(file, parseResultMRZ);
if (mrz !== null) {
const mapMrz = Object.entries(mrz.result || {}).map(
([key, value]) => ({
name: key,
value: value,
}),
);
const tempValue = {
status: true,
group,
meta: mapMrz,
};
return tempValue;
}
}
if (group === 'visa') {
const res = await ocrStore.sendOcr({
file: file,
category: group,
});
if (res) {
const tempValue = {
status: true,
group,
meta: res.fields,
};
return tempValue;
}
}
return { status: true, group, meta: [] };
}
"
:delete-item="
async () => {
return true;
}
"
:get-file-list="
async (group: 'passport' | 'visa' | 'attachment') => {
if (!!currentFromDataEmployee.id && group !== 'attachment') {
const resMeta = await employeeStore.getMetaList({
parentId: currentFromDataEmployee.id,
group,
});
const tempValue = resMeta.map(async (i: any) => {
return {
_meta: { ...i },
name: `${group}-${dateFormatJS(i.expireDate)}` || '',
group: group,
url: await employeeStore.getFile({
parentId: currentFromDataEmployee.id || '',
group,
fileId: i.id,
}),
file: undefined,
};
});
return await waitAll(tempValue);
} else {
const res = await employeeStore.listAttachment({
parentId: currentFromDataEmployee.id || '',
});
const tempValue = (res as string[]).map(async (i: any) => {
return {
_meta: { id: i, name: i },
name: i || '',
group: group,
url: await employeeStore.getAttachment({
parentId: currentFromDataEmployee.id || '',
name: i,
}),
file: undefined,
};
});
return await waitAll(tempValue);
}
}
"
>
<template #form="{ mode, meta, isEdit }">
<FormEmployeePassport
v-if="mode === 'passport' && meta"
prefix-id="drawer-info-employee"
id="form-passport"
dense
outlined
separator
ocr
:title="$t('customerEmployee.form.group.passport')"
:readonly="!isEdit"
v-model:birth-country="meta.birthCountry"
v-model:previous-passportRef="meta.previousPassportRef"
v-model:issue-place="meta.issuePlace"
v-model:issue-country="meta.issueCountry"
v-model:issue-date="meta.issueDate"
v-model:type="meta.type"
v-model:expire-date="meta.expireDate"
v-model:birth-date="meta.birthDate"
v-model:worker-status="meta.workerStatus"
v-model:nationality="meta.nationality"
v-model:gender="meta.gender"
v-model:last-name-en="meta.lastNameEN"
v-model:last-name="meta.lastName"
v-model:middle-name-en="meta.middleNameEN"
v-model:middle-name="meta.middleName"
v-model:first-name-en="meta.firstNameEN"
v-model:first-name="meta.firstName"
v-model:name-prefix="meta.namePrefix"
v-model:passport-number="meta.number"
/>
<FormEmployeeVisa
v-if="mode === 'visa' && meta"
prefix-id="drawer-info-employee"
id="form-visa"
ocr
dense
outlined
title="customerEmployee.form.group.visa"
:readonly="!isEdit"
v-model:arrival-at="meta.arrivalAt"
v-model:arrival-tm-no="meta.arrivalTMNo"
v-model:arrival-tm="meta.arrivalTM"
v-model:mrz="meta.mrz"
v-model:entry-count="meta.entryCount"
v-model:issue-place="meta.issuePlace"
v-model:issue-country="meta.issueCountry"
v-model:issueDate="meta.issueDate"
v-model:type="meta.type"
v-model:expire-date="meta.expireDate"
v-model:visa-issue-date="meta.issueDate"
v-model:visa-expiry-date="meta.expireDate"
v-model:remark="meta.remark"
v-model:worker-type="meta.workerType"
v-model:number="meta.number"
/>
<NoticeJobEmployment v-if="mode === 'noticeJobEmployment'" />
</template>
</UploadFileGroup>
</div>
</div>
</div>
</DialogForm>
<!-- NOTE: END - Employee Add Form -->
</template>
<style scoped>
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
color: hsl(var(--text-mute));
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}
:deep(.q-editor__toolbar-group):nth-child(2) {
margin-left: auto !important;
}
:deep(.q-editor__toolbar.row.no-wrap.scroll-x) {
background-color: var(--surface-2) !important;
}
:deep(.q-editor__toolbar) {
border-color: var(--surface-3) !important;
}
:deep(
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
.q-focus-helper
) {
visibility: hidden;
}
.selectable-item {
padding: 0;
appearance: none;