jws-frontend/src/pages/05_quotation/MainPage.vue
Aif 15a812b50e
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
feat: Refresh quotation list on window focus
2025-11-12 11:27:55 +07:00

1395 lines
46 KiB
Vue

<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref, watch, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
// NOTE: Import stores
import useCustomerStore from 'stores/customer';
import { useQuotationStore } from 'src/stores/quotations';
import { dialog, isRoleInclude, notify, setPrefixName } from 'stores/utils';
import { useNavigator } from 'src/stores/navigator';
import useFlowStore from 'src/stores/flow';
import useMyBranch from 'stores/my-branch';
import { useQuotationForm } from './form';
import { hslaColors } from './constants';
import { pageTabs, columnQuotation } from './constants';
import { toCamelCase, canAccess } from 'stores/utils';
import { getUserId } from 'src/services/keycloak';
// NOTE Import Types
import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
// NOTE: Import Components
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
import PaginationComponent from 'src/components/PaginationComponent.vue';
import StatCardComponent from 'src/components/StatCardComponent.vue';
import FloatingActionButton from 'components/FloatingActionButton.vue';
import FormAbout from 'src/components/05_quotation/FormAbout.vue';
import CreateButton from 'src/components/AddButton.vue';
import ItemCard from 'src/components/ItemCard.vue';
import DialogForm from 'components/DialogForm.vue';
import SideMenu from 'components/SideMenu.vue';
import NoData from 'src/components/NoData.vue';
import { dialogCreateCustomerItem } from 'src/pages/03_customer-management/constant';
import { SaveButton } from 'components/button';
import ProfileBanner from 'components/ProfileBanner.vue';
import { AddressForm } from 'components/form';
import {
EmployerFormBusiness,
EmployerFormAbout,
EmployerFormBasicInfo,
EmployerFormBranch,
} from 'src/pages/03_customer-management/components';
import { useCustomerForm } from 'src/pages/03_customer-management/form';
import { Quotation } from 'src/stores/quotations/types';
import TableQuotation from 'src/components/05_quotation/TableQuotation.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import { DialogContainer, DialogHeader } from 'src/components/dialog';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t, locale } = useI18n();
const $q = useQuasar();
const quotationFormStore = useQuotationForm();
const customerFormStore = useCustomerForm();
const flowStore = useFlowStore();
const userBranch = useMyBranch();
const navigatorStore = useNavigator();
const customerStore = useCustomerStore();
const {
fetchListOfOptionBranch,
customerFormUndo,
deleteCustomerById,
validateTabField,
deleteCustomerBranchById,
} = customerFormStore;
const {
currentFormData: quotationFormData,
currentFormState: quotationFormState,
} = storeToRefs(quotationFormStore);
const {
state: customerFormState,
currentFormData: customerFormData,
currentBranchRootId,
registerAbleBranchOption,
tabFieldRequired,
} = storeToRefs(customerFormStore);
const { currentMyBranch } = storeToRefs(userBranch);
const fieldSelectedOption = computed(() => {
return columnQuotation
.filter((v) => v.name !== 'action')
.map((v) => ({
label: v.label,
value: v.name,
}));
});
const keyAddDialog = ref<number>(0);
const special = ref(false);
const branchId = ref('');
const agentPrice = ref<boolean>(false);
const emptyCreateDialog = ref(false);
const onCreateImageList = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
const currentQuotationPayment = ref<Quotation>();
const pageState = reactive({
hideStat: false,
inputSearch: '',
fieldSelected: [''],
gridView: false,
total: 0,
sellerId: '',
currentTab: 'Issued',
addModal: false,
quotationModal: false,
employeeModal: false,
receiptModal: false,
searchDate: [],
});
pageState.fieldSelected = [...columnQuotation.map((v) => v.name)];
const CUSTOMER_BRANCH_DEFAULT: CustomerBranchCreate & {
id?: string;
branchCode?: string;
codeCustomer?: string;
} = {
customerCode: '',
customerId: '',
legalPersonNo: '',
citizenId: '',
namePrefix: '',
firstName: '',
lastName: '',
firstNameEN: '',
lastNameEN: '',
telephoneNo: '',
gender: '',
birthDate: new Date().toString(),
businessType: '',
jobPosition: '',
jobDescription: '',
payDate: '',
payDateEN: '',
wageRate: 0,
wageRateText: '',
homeCode: '',
employmentOffice: '',
employmentOfficeEN: '',
address: '',
addressEN: '',
street: '',
streetEN: '',
moo: '',
mooEN: '',
soi: '',
soiEN: '',
provinceId: '',
districtId: '',
subDistrictId: '',
contactName: '',
email: '',
contactTel: '',
officeTel: '',
agentUserId: undefined,
status: 'CREATED',
customerName: '',
registerName: '',
registerNameEN: '',
registerDate: new Date(),
authorizedCapital: '',
authorizedName: '',
authorizedNameEN: '',
code: '',
};
const formDataCustomerBranch = ref<
CustomerBranchCreate & {
id?: string;
branchCode?: string;
codeCustomer?: string;
}
>(structuredClone(CUSTOMER_BRANCH_DEFAULT));
function setDefaultCustomer() {
formDataCustomerBranch.value = structuredClone(CUSTOMER_BRANCH_DEFAULT);
}
async function submitCustomer() {
if (currentMyBranch.value === undefined) return;
const { code, customerCode, birthDate, ...payload } =
formDataCustomerBranch.value;
customerFormData.value.customerBranch = [{ ...payload }];
customerFormData.value.registeredBranchId = isRoleInclude(['system'])
? branchId.value
: currentMyBranch.value.id;
await customerFormStore.addCurrentCustomerBranch();
customerFormState.value.dialogModal = false;
// customerFormState.value.dialogType = 'info';
}
async function triggerDialogDeleteQuottaion(id: string) {
quotationFormStore.dialogDelete(async () => {
await quotationStore.deleteQuotation(id);
await fetchQuotationList();
});
}
function triggerCreateCustomerd(opts: { type: CustomerType }) {
setDefaultCustomer();
customerFormState.value.dialogType = 'create';
customerFormData.value.customerType = opts?.type;
customerFormState.value.dialogModal = true;
}
function triggerSelectTypeCustomerd() {
emptyCreateDialog.value = true;
}
function triggerAddQuotationDialog() {
pageState.addModal = true;
quotationFormStore.resetForm(true);
// TODO: form and state controll
}
function triggerQuotationDialog(opts: {
statusDialog: 'info' | 'edit' | 'create';
quotationId?: string;
branchId?: string;
}) {
const url = new URL(
`/quotation/${opts.statusDialog === 'create' ? 'add' : 'view'}`,
window.location.origin,
);
localStorage.setItem(
'new-quotation',
JSON.stringify({
customerBranchId: quotationFormData.value.customerBranchId,
branchId: opts.branchId ?? branchId.value,
agentPrice: agentPrice.value,
special: special.value,
statusDialog: opts.statusDialog,
quotationId: opts.quotationId,
}),
);
pageState.addModal = false;
window.open(url.toString(), '_blank');
}
function triggerReceiptDialog(data: Quotation) {
currentQuotationPayment.value = data;
pageState.receiptModal = true;
}
const quotationStore = useQuotationStore();
const {
data: quotationData,
page: quotationPage,
pageSize: quotationPageSize,
pageMax: quotationPageMax,
stats: quotationStats,
} = storeToRefs(quotationStore);
const customerNameInfo = computed(() => {
if (customerFormData.value.customerBranch === undefined) return;
const name =
locale.value === 'eng'
? `${customerFormData.value.customerBranch[0]?.firstNameEN} ${customerFormData.value.customerBranch[0]?.lastNameEN}`
: `${customerFormData.value.customerBranch[0]?.firstName} ${customerFormData.value.customerBranch[0]?.lastName}`;
return name || '-';
});
function handleWindowFocus() {
fetchQuotationList();
}
onMounted(async () => {
pageState.gridView = $q.screen.lt.md ? true : false;
navigatorStore.current.title = 'quotation.title';
navigatorStore.current.path = [
{
text: 'quotation.caption',
i18n: true,
handler: () => {
pageState.currentTab = 'Issued';
},
},
];
{
const ret = await quotationStore.getQuotationStats();
if (ret) {
quotationStats.value = Object.assign(quotationStats.value, ret);
}
}
{
const ret = await quotationStore.getQuotationList({
page: quotationPage.value,
pageSize: quotationPageSize.value,
status: 'Issued',
urgentFirst: true,
sellerId: pageState.sellerId || undefined,
});
if (ret) {
quotationData.value = ret.result;
quotationPageMax.value = Math.ceil(ret.total / quotationPageSize.value);
pageState.total = ret.total;
}
}
flowStore.rotate();
window.addEventListener('focus', handleWindowFocus);
});
onUnmounted(() => {
window.removeEventListener('focus', handleWindowFocus);
});
async function fetchQuotationList(mobileFetch?: boolean) {
{
const ret = await quotationStore.getQuotationList({
page: mobileFetch ? 1 : quotationPage.value,
pageSize: quotationPageSize.value,
status:
pageState.currentTab !== 'Issued'
? (
{
Accepted: 'Accepted',
Expired: 'Expired',
Invoice: 'PaymentInProcess',
PaymentSuccess: 'PaymentSuccess',
ProcessComplete: 'ProcessComplete',
Canceled: 'Canceled',
} as const
)[pageState.currentTab]
: 'Issued',
query: pageState.inputSearch,
urgentFirst: true,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
sellerId: pageState.sellerId || undefined,
});
if (ret) {
quotationData.value =
$q.screen.xs && !mobileFetch
? [...quotationData.value, ...ret.result]
: ret.result;
quotationPageMax.value = Math.ceil(ret.total / quotationPageSize.value);
pageState.total = ret.total;
}
}
{
const ret = await quotationStore.getQuotationStats();
if (ret) {
quotationStats.value = Object.assign(quotationStats.value, ret);
}
}
flowStore.rotate();
}
watch(
[
() => pageState.currentTab,
() => pageState.inputSearch,
() => pageState.searchDate,
quotationPageSize,
],
() => {
quotationPage.value = 1;
quotationData.value = [];
fetchQuotationList();
},
);
async function storeDataLocal(id: string) {
await quotationFormStore.assignFormData(id, 'assign');
localStorage.setItem(
'quotation-preview',
JSON.stringify({
data: {
...quotationFormData.value,
},
meta: {
source: {
...quotationFormState.value.source,
code:
quotationFormState.value.mode === 'create'
? '-'
: quotationFormState.value?.source?.code,
createAt:
quotationFormState.value.mode === 'create'
? Date.now()
: quotationFormState.value?.source?.createdAt,
createBy: quotationFormState.value?.source?.createdBy,
payCondition: quotationFormData.value.payCondition,
contactName: quotationFormData.value.contactName,
contactTel: quotationFormData.value.contactTel,
workName: quotationFormData.value.workName,
dueDate: quotationFormData.value.dueDate,
},
selectedWorker: quotationFormData.value.worker,
createdBy: quotationFormState.value.createdBy('tha'),
},
}),
);
const url = new URL('/quotation/document-view', window.location.origin);
url.searchParams.append('type', pageState.currentTab.toLowerCase());
window.open(url, '_blank');
}
async function filterBySellerId() {
pageState.sellerId = pageState.sellerId ? '' : getUserId();
await fetchQuotationList();
}
</script>
<template>
<FloatingActionButton
hide-icon
style="z-index: 999"
@click.stop="triggerAddQuotationDialog"
v-if="canAccess('quotation', 'create')"
/>
<div class="column full-height no-wrap">
<!-- SEC: stat -->
<section class="text-body-2 q-mb-xs flex items-center">
{{ $t('general.dataSum') }}
<q-badge
rounded
class="q-ml-sm"
style="
background-color: hsla(var(--info-bg) / 0.15);
color: hsl(var(--info-bg));
"
>
{{ Object.values(quotationStats).reduce((a, c) => a + c, 0) }}
</q-badge>
<q-btn
class="q-ml-sm"
icon="mdi-pin-outline"
color="primary"
size="sm"
flat
dense
rounded
@click="pageState.hideStat = !pageState.hideStat"
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
style="transition: 0.1s ease-in-out"
/>
</section>
<transition name="slide">
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
<div style="display: inline-block">
<StatCardComponent
labelI18n
:branch="[
{
icon: 'material-symbols-light:receipt-long',
count: quotationStats.issued,
label: 'quotation.status.Issued',
color: 'orange',
hidden: pageState.currentTab !== 'Issued',
},
{
icon: 'mdi-hand-coin-outline',
count: quotationStats.accepted,
label: 'quotation.status.Accepted',
color: 'pink',
hidden: pageState.currentTab !== 'Accepted',
},
{
icon: 'pajamas:expire',
count: quotationStats.expired,
label: 'quotation.status.Expired',
color: 'cyan',
hidden: pageState.currentTab !== 'Expired',
},
{
icon: 'material-symbols-light:receipt-long',
count: quotationStats.paymentInProcess,
label: 'quotation.status.Invoice',
color: 'purple',
hidden: pageState.currentTab !== 'Invoice',
},
{
icon: 'fluent:receipt-money-16-regular',
count: quotationStats.paymentSuccess,
label: 'quotation.status.PaymentSuccess',
color: 'light-green',
hidden: pageState.currentTab !== 'PaymentSuccess',
},
{
icon: 'mdi-check-decagram-outline',
count: quotationStats.processComplete,
label: 'quotation.status.ProcessComplete',
color: 'blue',
hidden: pageState.currentTab !== 'ProcessComplete',
},
{
icon: 'mdi-cancel',
count: quotationStats.canceled,
label: 'general.cancel',
color: 'red',
hidden: pageState.currentTab !== 'Canceled',
},
]"
:dark="$q.dark.isActive"
/>
</div>
</div>
</transition>
<!-- SEC: header content -->
<header class="col surface-1 rounded bordered overflow-hidden">
<div class="column full-height">
<section
class="row surface-3 justify-between full-width bordered-b"
style="z-index: 1"
>
<div class="row q-py-sm q-px-md justify-between full-width">
<q-input
for="input-search"
outlined
dense
:label="$t('general.search')"
class="col col-md-3"
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
v-model="pageState.inputSearch"
debounce="200"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate">
<template #prepend>
<div class="text-weight-medium q-mb-sm">
<q-checkbox
size="xs"
:model-value="!!pageState.sellerId"
@click="filterBySellerId"
/>
{{ $t('quotation.ownOnly') }}
</div>
</template>
</AdvanceSearch>
</template>
</q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap">
<q-select
v-if="!pageState.gridView"
id="select-field"
for="select-field"
class="col-md-5 q-ml-sm"
:options="
fieldSelectedOption.map((v) => ({
...v,
label: v.label && $t(v.label),
}))
"
:display-value="$t('general.displayField')"
:hide-dropdown-icon="$q.screen.lt.sm"
v-model="pageState.fieldSelected"
option-label="label"
option-value="value"
map-options
emit-value
outlined
multiple
dense
/>
<q-btn-toggle
id="btn-mode"
v-model="pageState.gridView"
dense
class="no-shadow bordered rounded surface-1 q-ml-sm"
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
size="xs"
:options="[
{ value: true, slot: 'folder' },
{ value: false, slot: 'list' },
]"
>
<template v-slot:folder>
<q-icon
name="mdi-view-grid-outline"
size="16px"
class="q-px-sm q-py-xs rounded"
:style="{
color: $q.dark.isActive
? pageState.gridView
? '#C9D3DB '
: '#787B7C'
: pageState.gridView
? '#787B7C'
: '#C9D3DB',
}"
/>
</template>
<template v-slot:list>
<q-icon
name="mdi-format-list-bulleted"
class="q-px-sm q-py-xs rounded"
size="16px"
:style="{
color: $q.dark.isActive
? pageState.gridView === false
? '#C9D3DB'
: '#787B7C'
: pageState.gridView === false
? '#787B7C'
: '#C9D3DB',
}"
/>
</template>
</q-btn-toggle>
</div>
</div>
</section>
<nav class="surface-2 bordered-b q-px-md full-width">
<q-tabs
inline-label
mobile-arrows
dense
v-model="pageState.currentTab"
align="left"
class="full-width"
active-color="info"
>
<q-tab
v-for="tab in pageTabs"
:name="tab"
:key="tab"
@click="
() => {
quotationPage = 1;
pageState.currentTab = tab;
pageState.inputSearch = '';
flowStore.rotate();
}
"
>
<div
class="row text-capitalize"
:class="
pageState.currentTab === tab ? 'text-bold' : 'app-text-muted'
"
>
{{ $t(`quotation.status.${tab}`) }}
</div>
</q-tab>
</q-tabs>
</nav>
<!-- SEC: body content -->
<article
v-if="!quotationData || quotationData.length === 0"
class="col surface-2 flex items-center justify-center"
>
<NoData
v-if="
pageState.inputSearch ||
!canAccess('quotation', 'create') ||
pageState.currentTab !== 'Issued'
"
:not-found="!!pageState.inputSearch"
/>
<CreateButton
v-if="
!pageState.inputSearch &&
pageState.currentTab === 'Issued' &&
canAccess('quotation', 'create')
"
@click="triggerAddQuotationDialog"
label="general.add"
:i18n-args="{ text: $t('quotation.title') }"
/>
</article>
<article v-else class="col surface-2 full-width scroll">
<div class="q-pa-md">
<q-infinite-scroll
:key="pageState.currentTab"
:offset="100"
@load="
(_, done) => {
if ($q.screen.gt.xs) return;
quotationPage = quotationPage + 1;
fetchQuotationList().then(() => {
done(quotationPage >= quotationPageMax);
});
}
"
>
<TableQuotation
:page="quotationPage"
:page-size="quotationPageSize"
:columns="columnQuotation"
:rows="quotationData"
:visible-columns="pageState.fieldSelected"
:grid="pageState.gridView"
:hide-edit="pageState.currentTab !== 'Issued'"
:hide-action="!canAccess('quotation', 'edit')"
:hide-delete="!canAccess('quotation', 'delete')"
:hide-btn-preview="pageState.currentTab === 'PaymentSuccess'"
@preview="(id: any) => storeDataLocal(id)"
@view="
(item) => {
triggerQuotationDialog({
statusDialog: 'info',
quotationId: item.id,
branchId: item.customerBranch.customer.registeredBranchId,
});
}
"
@edit="
(item) =>
triggerQuotationDialog({
statusDialog: 'edit',
quotationId: item.id,
branchId: item.customerBranch.customer.registeredBranchId,
})
"
@delete="(id) => triggerDialogDeleteQuottaion(id)"
>
<template #grid="{ item }">
<div class="col-md-4 col-sm-6 col-12 column">
<QuotationCard
class="col"
:hide-action="!canAccess('quotation', 'edit')"
:hide-kebab-delete="!canAccess('quotation', 'delete')"
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
:hide-preview="pageState.currentTab === 'PaymentSuccess'"
:urgent="item.row.urgent"
:code="item.row.code"
:title="item.row.workName"
:created-at="
new Date(item.row.createdAt).toLocaleString('th-TH', {
hour12: false,
})
"
:valid-until="
(() => {
const date = new Date(item.row.dueDate);
date.setHours(23, 59, 59, 0);
return date.toLocaleString('th-TH', {
hour12: false,
});
})()
"
:status="
$t(`quotation.status.${item.row.quotationStatus}`)
"
:worker-count="item.row._count.worker"
:worker-max="item.row.workerMax || item.row._count.worker"
:customer-name="
item.row.customerBranch.customer.customerType === 'CORP'
? $i18n.locale === 'tha'
? item.row.customerBranch.registerName
: item.row.customerBranch.registerNameEN
: $i18n.locale === 'tha'
? `${item.row.customerBranch.firstName || '-'} ${item.row.customerBranch.lastName || ''}`
: `${item.row.customerBranch.firstNameEN || '-'} ${item.row.customerBranch.lastNameEN || ''}`
"
:reporter="
$i18n.locale === 'eng'
? item.row.createdBy.firstNameEN +
' ' +
item.row.createdBy.lastNameEN
: item.row.createdBy.firstName +
' ' +
item.row.createdBy.lastName
"
:total-price="item.row.finalPrice"
:badge-color="hslaColors[item.row.quotationStatus] || ''"
@preview="storeDataLocal(item.row.id)"
@view="
() => {
triggerQuotationDialog({
statusDialog: 'info',
quotationId: item.row.id,
branchId:
item.row.customerBranch.customer
.registeredBranchId,
});
}
"
@edit="
triggerQuotationDialog({
statusDialog: 'edit',
quotationId: item.row.id,
branchId:
item.row.customerBranch.customer.registeredBranchId,
})
"
@link="triggerReceiptDialog(item.row)"
@upload="console.log('upload')"
@delete="triggerDialogDeleteQuottaion(item.row.id)"
/>
</div>
</template>
</TableQuotation>
<template v-slot:loading>
<div
v-if="$q.screen.lt.sm && quotationPage !== quotationPageMax"
class="row justify-center"
>
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</div>
</article>
<!-- SEC: footer content -->
<footer
class="row justify-between items-center q-px-md q-py-sm surface-2"
v-if="quotationPageMax > 0 && $q.screen.gt.xs"
>
<div class="col-4">
<div class="row items-center no-wrap">
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
{{ $t('general.recordPerPage') }}
</div>
<div><PaginationPageSize v-model="quotationPageSize" /></div>
</div>
</div>
<div class="col-4 row justify-center app-text-muted">
{{
$q.screen.gt.sm
? $t('general.recordsPage', {
resultcurrentPage: quotationData.length,
total: pageState.inputSearch
? quotationData.length
: pageState.total,
})
: $t('general.ofPage', {
current: quotationData.length,
total: pageState.inputSearch
? quotationData.length
: pageState.total,
})
}}
</div>
<nav class="col-4 row justify-end">
<PaginationComponent
v-model:current-page="quotationPage"
v-model:max-page="quotationPageMax"
:fetch-data="() => fetchQuotationList()"
/>
</nav>
</footer>
</div>
</header>
</div>
<!-- NOTE: SEC START - Dialog -->
<!-- NOTE: START - Quotation Form, Add Quotation -->
<DialogForm
:key="keyAddDialog"
:title="$t('general.add', { text: $t('quotation.title') })"
v-model:modal="pageState.addModal"
:submit-label="$t('general.add', { text: $t('quotation.title') })"
submit-icon="mdi-check"
height="auto"
width="60vw"
:submit="
() => {
quotationFormState.mode = 'create';
quotationFormData.customerBranchId = currentBranchRootId;
triggerQuotationDialog({ statusDialog: 'create' });
}
"
:close="
() => {
branchId = '';
currentBranchRootId = '';
}
"
:beforeClose="
() => {
agentPrice = false;
special = false;
return false;
}
"
>
<header
:class="{
'q-mx-lg q-mt-md': $q.screen.gt.sm,
'q-mx-md q-mt-sm': $q.screen.lt.md,
}"
>
<ProfileBanner
prefix="dialog"
img="/images/quotation-bg-avatar.png"
fallback-cover="/images/quotation-banner.png"
bg-color="var(--surface-1)"
:title="$t('general.itemNo', { msg: $t('quotation.title') })"
hideActive
readonly
noImageAction
hideFade
/>
</header>
<section
class="col surface-1 rounded bordered row scroll"
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
'q-mx-md q-my-sm': $q.screen.lt.md,
}"
>
<div
class="col-12"
:class="{
'q-px-md q-py-lg': $q.screen.gt.sm,
'q-pa-sm': $q.screen.lt.md,
}"
id="customer-form-content"
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<FormAbout
on-create
v-model:agent-price="agentPrice"
v-model:branch-id="branchId"
v-model:special="special"
v-model:customer-branch-id="currentBranchRootId"
@add-customer="triggerSelectTypeCustomerd()"
/>
</div>
</section>
</DialogForm>
<!-- NOTE: END - Quotation Form -->
<!-- NOTE: START - Customer / Employer Type Select Form -->
<DialogForm
v-model:modal="emptyCreateDialog"
:title="$t('customer.employerType')"
hide-footer
no-app-box
width="40vw"
height="250px"
:close="
() => {
emptyCreateDialog = false;
onCreateImageList = { selectedImage: '', list: [] };
}
"
>
<div class="full-height row q-pa-md">
<ItemCard
class="col q-mx-sm full-height cursor-pointer"
v-for="value in dialogCreateCustomerItem"
:key="value.text"
:icon="value.icon"
:text="value.text"
:icon-color="value.iconColor"
:bg-color="value.color"
:id="`btn-add-new-${value.text}`"
@trigger="
() => {
triggerCreateCustomerd({
type:
value.text === 'customer.employerLegalEntity'
? CustomerType.Corporate
: CustomerType.Person,
});
emptyCreateDialog = false;
}
"
/>
</div>
</DialogForm>
<!-- NOTE: END - Customer / Employer Type Form -->
<!-- NOTE: START - Customer / Employer Form -->
<DialogContainer
:model-value="customerFormState.dialogModal"
:on-open="
async () => {
customerFormStore.resetForm(customerFormState.dialogType === 'create');
onCreateImageList = { selectedImage: '', list: [] };
customerFormState.customerImageUrl = '';
await fetchListOfOptionBranch();
await customerFormStore.addCurrentCustomerBranch();
}
"
:on-close="
() => {
customerFormState.dialogModal = false;
onCreateImageList = { selectedImage: '', list: [] };
keyAddDialog++;
}
"
>
<template #header>
<DialogHeader
:title="
customerFormState.dialogType === 'create'
? $t(`general.add`, {
text: `${$t('customer.employer')} `,
})
: `${$t('customer.employer')} `
"
>
<template #title-after>
<span
:style="`color: hsla(var(--${customerFormData.customerType === 'PERS' ? 'teal-10-hsl' : 'violet-11-hsl'})/1)`"
>
:
{{
customerFormData.customerType === 'CORP'
? $t('customer.employerLegalEntity')
: $t('customer.employerNaturalPerson')
}}
</span>
</template>
</DialogHeader>
</template>
<div
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
'q-mx-md q-my-sm': !$q.screen.gt.sm,
}"
>
<ProfileBanner
v-if="customerFormData.customerBranch !== undefined"
active
hide-fade
prefix="dialog"
:fallback-cover="`/images/customer-${customerFormData.customerType}-banner-bg.jpg`"
:img="
customerFormState.customerImageUrl ||
`/images/customer-${customerFormData.customerType}-avartar-${customerFormData.customerType === 'PERS' ? customerFormData.customerBranch[0]?.gender : 'male'}.png`
"
:fallbackImg="`/images/customer-${customerFormData.customerType}-avartar-${customerFormData.customerType === 'PERS' ? customerFormData.customerBranch[0]?.gender : 'male'}.png`"
:color="`hsla(var(--${customerFormData.customerType === 'PERS' ? 'teal-10-hsl' : 'violet-11-hsl'})/1)`"
:bg-color="`hsla(var(--${customerFormData.customerType === 'PERS' ? 'teal-10-hsl' : 'violet-11-hsl'})/0.1)`"
:icon="
customerFormData.customerType === 'PERS'
? 'mdi-account-plus-outline'
: 'mdi-office-building-outline'
"
:title="
customerFormData.customerType === 'PERS'
? setPrefixName(
{
namePrefix: customerFormData.customerBranch[0]?.namePrefix,
firstName: customerFormData.customerBranch[0]?.firstName,
lastName: customerFormData.customerBranch[0]?.lastName,
firstNameEN: customerFormData.customerBranch[0]?.firstNameEN,
lastNameEN: customerFormData.customerBranch[0]?.lastNameEN,
},
{ locale },
)
: customerFormData.customerBranch[0]?.registerName
"
:caption="
customerFormData.customerType === 'PERS'
? `${customerFormData.customerBranch[0]?.firstNameEN} ${customerFormData.customerBranch[0]?.lastNameEN}`
: customerFormData.customerBranch[0]?.registerNameEN
"
@view="
() => {
customerFormState.imageDialog = true;
customerFormState.isImageEdit = false;
}
"
@edit="
customerFormState.imageDialog = customerFormState.isImageEdit = true
"
/>
</div>
<div
style="flex: 1; width: 100%; overflow-y: auto"
id="customer-form"
:class="{
'q-px-lg q-pb-lg': $q.screen.gt.sm,
'q-px-md q-pb-sm': !$q.screen.gt.sm,
}"
>
<div class="col surface-1 full-height rounded bordered scroll row">
<div
class="col"
style="height: 100%; max-height: 100; overflow-y: auto"
v-if="$q.screen.gt.sm"
>
<div class="q-py-md q-pl-md q-pr-sm">
<SideMenu
:menu="[
{
name: $t('form.field.basicInformation'),
anchor: 'form-basic-info-customer',
},
{
name: $t('customer.form.group.branch'),
anchor: 'form-branch-customer-branch',
useBtn: true,
},
...(customerFormData.customerBranch?.map((v, i) => ({
name:
i === 0
? $t('customer.form.headQuarters.title')
: $t('customer.form.branch.title', {
name: i,
}),
anchor: `form-branch-customer-no-${i}`,
sub: true,
})) || []),
]"
background="transparent"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#customer-form-content"
>
<template v-slot:btn-form-branch-customer-branch>
<q-btn
dense
flat
icon="mdi-plus"
size="sm"
rounded
padding="0px 0px"
style="color: var(--stone-9)"
@click.stop="submitCustomer()"
v-if="
customerFormState.branchIndex === -1 &&
!!customerFormState.editCustomerId &&
customerFormState.dialogType !== 'create'
"
:disabled="!customerFormState.readonly"
/>
</template>
</SideMenu>
</div>
</div>
<div
class="col-12 col-md-10"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
id="customer-form-content"
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<EmployerFormBasicInfo
prefixId="form"
v-if="
customerFormData.customerBranch !== undefined &&
customerFormData.customerBranch.length > 0
"
class="q-mb-xl"
:readonly="
(customerFormState.dialogType === 'edit' &&
customerFormState.readonly === true) ||
customerFormState.dialogType === 'info'
"
:action-disabled="customerFormState.branchIndex !== -1"
id="form-basic-info-customer"
:onCreate="customerFormState.dialogType === 'create'"
@edit="
((customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false))
"
@cancel="() => customerFormUndo(false)"
@delete="
customerFormState.editCustomerId &&
deleteCustomerById(customerFormState.editCustomerId)
"
:customer-type="customerFormData.customerType"
v-model:registered-branch-id="customerFormData.registeredBranchId"
v-model:customer-name="customerNameInfo"
v-model:register-name="
customerFormData.customerBranch[0].registerName
"
v-model:citizen-id="customerFormData.customerBranch[0].citizenId"
v-model:legal-person-no="
customerFormData.customerBranch[0].legalPersonNo
"
v-model:business-type="
customerFormData.customerBranch[0].businessTypeId
"
v-model:job-position="
customerFormData.customerBranch[0].jobPosition
"
v-model:telephone-no="
customerFormData.customerBranch[0].telephoneNo
"
v-model:branch-options="registerAbleBranchOption"
/>
<div class="row q-col-gutter-sm" id="form-branch-customer-branch">
<div class="col-12 text-weight-bold text-body1 row items-center">
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-xs"
color="info"
name="mdi-briefcase-outline"
style="background-color: var(--surface-3)"
/>
<span>{{ $t('customer.form.group.branch') }}</span>
</div>
<template
v-for="(_, idx) in customerFormData.customerBranch"
:key="idx"
>
<!-- v-if="customerFormData.customerBranch" -->
<q-form
class="full-width"
greedy
@submit.prevent="
async () => {
if (!customerFormData.customerBranch) return;
if (customerFormData.customerType === 'PERS') {
tabFieldRequired.main = [
'citizenId',
'namePrefix',
'firstName',
'firstNameEN',
'lastName',
'lastNameEN',
'gender',
'birthDate',
];
}
if (customerFormData.customerType === 'CORP') {
tabFieldRequired.main = ['legalPersonNo', 'registerName'];
}
let tapIsUndefined = validateTabField(
customerFormData.customerBranch?.[idx],
tabFieldRequired,
);
if (tapIsUndefined.length > 0) {
return dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('dialog.title.incompleteDataEntry'),
actionText: t('general.ok'),
persistent: true,
message: t('dialog.message.incompleteDataEntry', {
tap: `${tapIsUndefined.map((v: string) => t(`customerBranch.tab.${v}`)).join(', ')}`,
}),
action: async () => {
return;
},
});
}
if (!customerFormData.customerBranch[idx].id) {
let res: any;
if (idx === 0) {
res =
await customerFormStore.submitFormCustomer(
onCreateImageList,
);
} else {
res = await customerStore.createBranch({
...customerFormData.customerBranch[idx],
customerId: customerFormState.editCustomerId || '',
});
}
if (res) {
customerFormState.readonly = true;
notify('create', $t('general.success'));
}
} else {
if (!customerFormState.editCustomerId) return;
await customerStore.editBranchById(
customerFormData.customerBranch[idx].id || '',
{
...customerFormData.customerBranch[idx],
id: undefined,
},
);
}
const uploadResult =
customerFormData.customerBranch[idx].file?.map(
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,
);
}
},
) || [];
for (const r of uploadResult) await r;
await customerFormStore.assignFormData(
customerFormState.editCustomerId,
);
customerFormStore.resetForm();
}
"
>
<!-- v-if="!!customerFormState.editCustomerId" -->
<EmployerFormBranch
:index="idx"
prefixId="form"
v-if="customerFormData.customerBranch"
v-model:customer="customerFormData"
v-model:customer-branch="customerFormData.customerBranch[idx]"
:onCreate="customerFormState.dialogType === 'create'"
:customer-type="customerFormData.customerType"
:readonly="customerFormState.branchIndex !== idx"
:action-disabled="
!customerFormState.readonly ||
(customerFormState.branchIndex !== -1 &&
customerFormState.branchIndex !== idx)
"
@edit="() => (customerFormState.branchIndex = idx)"
@cancel="() => customerFormUndo(false)"
@delete="
async () => {
if (!customerFormState.editCustomerId) return;
if (idx === 0) {
deleteCustomerById(customerFormState.editCustomerId);
return;
}
if (!!customerFormData.customerBranch?.[idx].id) {
const action = await deleteCustomerBranchById(
customerFormData.customerBranch[idx].id || '',
);
if (action) {
await customerFormStore.assignFormData(
customerFormState.editCustomerId,
);
}
customerFormStore.resetForm();
}
}
"
@save="() => {}"
/>
</q-form>
</template>
</div>
</div>
</div>
</div>
</DialogContainer>
</template>
<style scoped></style>