feat: filter customer by business type, address

This commit is contained in:
puriphatt 2025-09-17 13:07:55 +07:00 committed by Methapon2001
parent 56f0a86845
commit 5becbae369
4 changed files with 196 additions and 54 deletions

View file

@ -198,3 +198,10 @@ i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-i
.q-focus-helper {
visibility: hidden;
}
.clear-btn {
opacity: 0.6;
&:hover {
opacity: 1;
}
}

View file

@ -1,15 +1,17 @@
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue';
import { ref, watch, onMounted, nextTick, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRoute, useRouter } from 'vue-router';
import { canAccess } from 'stores/utils';
import type { District, Province, SubDistrict } from 'src/stores/address';
import useCustomerStore from 'stores/customer';
import useEmployeeStore from 'stores/employee';
import useAddressStore from 'src/stores/address';
import useFlowStore from 'stores/flow';
import { dialog } from 'stores/utils';
import { useNavigator } from 'src/stores/navigator';
import useFlowStore from 'stores/flow';
import { Status } from 'stores/types';
import {
CustomerStats,
@ -17,28 +19,28 @@ import {
CustomerBranch,
CustomerType,
} from 'stores/customer/types';
import { Employee, EmployeeHistory } from 'stores/employee/types';
import { SaveButton } from 'components/button';
import { SaveButton } from 'components/button';
import TabCustomer from './TabCustomer.vue';
import FloatingActionButton from 'components/FloatingActionButton.vue';
import StatCardComponent from 'components/StatCardComponent.vue';
import BranchPage from './BranchPage.vue';
import SelectBusinessType from 'src/components/shared/select/SelectBusinessType.vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
import TabEmployee from './TabEmployee.vue';
import SelectInput from 'src/components/shared/SelectInput.vue';
import { columnsCustomer, columnsEmployee } from './constant';
import { useCustomerForm, useEmployeeForm } from './form';
import { storeToRefs } from 'pinia';
import { nextTick } from 'vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
import TabEmployee from './TabEmployee.vue';
const { t, locale } = useI18n();
const $q = useQuasar();
const route = useRoute();
const router = useRouter();
const flowStore = useFlowStore();
const navigatorStore = useNavigator();
const addressStore = useAddressStore();
const customerStore = useCustomerStore();
const employeeStore = useEmployeeStore();
const customerFormStore = useCustomerForm();
@ -49,21 +51,21 @@ const { state: customerFormState, currentFormData: customerFormData } =
const { state: employeeFormState, statusEmployeeCreate } =
storeToRefs(employeeFormStore);
// NOTE: Component ref
const refTabCustomer = ref<InstanceType<typeof TabCustomer>>();
const refTabEmployee = ref<InstanceType<typeof TabEmployee>>();
// NOTE: Page Data
const currentCustomer = ref<Customer>();
const hideStats = ref(false);
const statsCustomerType = ref<CustomerStats>({
CORP: 0,
PERS: 0,
});
// NOTE: Page State
const hideStats = ref(false);
const gridView = ref(false);
const employeeStats = ref(0);
const inputSearch = ref('');
const filterBusinessType = ref('');
const searchDate = ref<string[]>([]);
const currentTab = ref<'employer' | 'employee'>('employer');
const inputSearch = ref('');
const currentStatus = ref<Status | 'All'>('All');
const customerTypeSelected = ref<{
label: string;
@ -72,14 +74,10 @@ const customerTypeSelected = ref<{
label: t('general.all'),
value: 'all',
});
const employeeStats = ref(0);
const gridView = ref(false);
// image
const imageList = ref<{ selectedImage: string; list: string[] }>();
const branch = ref<CustomerBranch[]>();
const statsCustomerType = ref<CustomerStats>({
CORP: 0,
PERS: 0,
});
const fieldDisplayCustomer = ref<
{
label: string;
@ -106,17 +104,30 @@ const fieldSelected = ref<string[]>(
...columnsCustomer.map((v) => v.name),
].filter((v, index, self) => self.indexOf(v) === index),
);
const customerStats = [
{ id: 1, count: 2, name: 'CORP' },
{ id: 2, count: 3, name: 'PERS' },
];
const filterAddress = reactive({
provinceId: '',
districtId: '',
subDistrictId: '',
});
const addressOpt = reactive<{
provinceOpt: Province[];
districtOpt: District[];
subDistrictOpt: SubDistrict[];
}>({
provinceOpt: [],
districtOpt: [],
subDistrictOpt: [],
});
const fieldCustomer = [
'all',
'customerLegalEntity',
'customerNaturalPerson',
] as const;
// image
const imageList = ref<{ selectedImage: string; list: string[] }>();
const branch = ref<CustomerBranch[]>();
async function triggerChangeStatus(
id: string,
status: string,
@ -190,16 +201,75 @@ async function fetchImageList(
async function triggerExport() {
switch (currentTab.value) {
case 'employer':
customerStore.customerExport({ pageSize: 10000 });
customerStore.customerExport({
pageSize: 10000,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
businessType: filterBusinessType.value,
province: filterAddress.provinceId || undefined,
district: filterAddress.districtId || undefined,
subDistrict: filterAddress.subDistrictId || undefined,
customerType:
customerTypeSelected.value.value === 'customerLegalEntity'
? CustomerType.Corporate
: customerTypeSelected.value.value === 'customerNaturalPerson'
? CustomerType.Person
: undefined,
});
break;
case 'employee':
employeeStore.employeeExport({ pageSize: 10000 });
employeeStore.employeeExport({
pageSize: 10000,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
});
break;
}
}
async function fetchAddressData(type: 'province' | 'district' | 'subDistrict') {
let result;
if (type === 'province') {
result = await addressStore.fetchProvince();
if (result) addressOpt.provinceOpt = result;
}
if (type === 'district') {
if (!filterAddress.provinceId) return;
result = await addressStore.fetchDistrictByProvinceId(
filterAddress.provinceId,
);
if (result) addressOpt.districtOpt = result;
}
if (type === 'subDistrict') {
if (!filterAddress.districtId) return;
result = await addressStore.fetchSubDistrictByProvinceId(
filterAddress.districtId,
);
if (result) addressOpt.subDistrictOpt = result;
}
return result;
}
async function handleSelectAddress(type: 'province' | 'district') {
if (type === 'province') {
filterAddress.districtId = filterAddress.subDistrictId = '';
addressOpt.districtOpt = addressOpt.subDistrictOpt = [];
await fetchAddressData('district');
}
if (type === 'district') {
filterAddress.subDistrictId = '';
await fetchAddressData('subDistrict');
}
}
async function init() {
navigatorStore.current.title = 'menu.customer';
navigatorStore.current.path = [
@ -245,6 +315,8 @@ async function init() {
}
}
await fetchAddressData('province');
flowStore.rotate();
}
@ -344,26 +416,24 @@ onMounted(async () => await init());
<div v-if="!hideStats" class="col-12 q-mb-md">
<div class="scroll">
<StatCardComponent
v-if="customerStats"
v-if="statsCustomerType"
label-i18n
:branch="
customerStats &&
(currentTab === 'employer'
? customerStats.map((v) => ({
count:
v.name === 'CORP'
? (statsCustomerType?.CORP ?? 0)
: (statsCustomerType?.PERS ?? 0),
label:
v.name === 'CORP'
? 'customer.employerLegalEntity'
: 'customer.employerNaturalPerson',
icon:
v.name === 'CORP'
? 'mdi-office-building-outline'
: 'mdi-account-outline',
color: v.name === 'CORP' ? 'purple' : 'green',
}))
currentTab === 'employer'
? [
{
count: statsCustomerType.CORP ?? 0,
label: 'customer.employerLegalEntity',
icon: 'mdi-office-building-outline',
color: 'purple',
},
{
count: statsCustomerType.PERS ?? 0,
label: 'customer.employerNaturalPerson',
icon: 'mdi-account-outline',
color: 'green',
},
]
: [
{
label: 'customer.employee',
@ -371,7 +441,7 @@ onMounted(async () => await init());
icon: 'mdi-account-outline',
color: 'pink',
},
])
]
"
:dark="$q.dark.isActive"
/>
@ -406,7 +476,11 @@ onMounted(async () => await init());
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="searchDate"
:active="$q.screen.lt.md && currentStatus !== 'All'"
:active="
($q.screen.lt.md && currentStatus !== 'All') ||
!!filterBusinessType ||
!!filterAddress.provinceId
"
>
<div
v-if="$q.screen.lt.md"
@ -432,6 +506,46 @@ onMounted(async () => await init());
{ label: $t('status.INACTIVE'), value: 'INACTIVE' },
]"
/>
<template v-if="currentTab === 'employer'">
<div class="q-mt-sm text-weight-medium">
{{ $t('customer.form.businessType') }}
</div>
<SelectBusinessType
v-model:value="filterBusinessType"
clearable
/>
<div class="q-mt-sm text-weight-medium">
{{ $t('customer.form.address') }}
</div>
<SelectInput
clearable
v-model="filterAddress.provinceId"
option-value="id"
:label="$t('form.province')"
:option="addressOpt.provinceOpt"
:option-label="locale === 'eng' ? 'nameEN' : 'name'"
@update:model-value="handleSelectAddress('province')"
/>
<SelectInput
clearable
class="q-mt-sm"
option-value="id"
v-model="filterAddress.districtId"
:option="addressOpt.districtOpt"
:label="$t('form.district')"
:option-label="locale === 'eng' ? 'nameEN' : 'name'"
@update:model-value="handleSelectAddress('district')"
/>
<SelectInput
clearable
class="q-mt-sm"
option-value="id"
v-model="filterAddress.subDistrictId"
:option="addressOpt.subDistrictOpt"
:label="$t('form.subDistrict')"
:option-label="locale === 'eng' ? 'nameEN' : 'name'"
/>
</template>
</AdvanceSearch>
</template>
</q-input>
@ -632,8 +746,11 @@ onMounted(async () => await init());
<TabCustomer
v-if="currentTab === 'employer'"
ref="refTabCustomer"
v-model:customer-type-selected="customerTypeSelected"
v-model:stats-customer-type="statsCustomerType"
v-model:img-list="imageList"
:filter-address="filterAddress"
:filter-business-type="filterBusinessType"
:gridView
:searchDate
:currentTab

View file

@ -110,6 +110,12 @@ const props = defineProps<{
inputSearch: string;
searchDate: string[];
fieldSelected: string[];
filterBusinessType: string;
filterAddress: {
provinceId: string;
districtId: string;
subDistrictId: string;
};
fetchImageList: (
id: string,
selectedName: string,
@ -143,13 +149,10 @@ const fieldCustomer = [
'customerLegalEntity',
'customerNaturalPerson',
] as const;
const customerTypeSelected = ref<{
const customerTypeSelected = defineModel<{
label: string;
value: 'all' | 'customerLegalEntity' | 'customerNaturalPerson';
}>({
label: t('general.all'),
value: 'all',
});
}>('customerTypeSelected');
const splitPercent = computed(() => ($q.screen.lt.md ? 0 : 15));
const customerNameInfo = computed(() => {
@ -186,6 +189,10 @@ async function fetchListCustomer(fetchStats = false, mobileFetch?: boolean) {
? 'ACTIVE'
: 'INACTIVE',
query: props.inputSearch,
businessType: props.filterBusinessType,
province: props.filterAddress.provinceId || undefined,
district: props.filterAddress.districtId || undefined,
subDistrict: props.filterAddress.subDistrictId || undefined,
startDate: props.searchDate[0],
endDate: props.searchDate[1],
customerType: (
@ -288,6 +295,8 @@ watch(
props.inputSearch,
props.searchDate,
props.currentStatus,
props.filterBusinessType,
props.filterAddress,
customerTypeSelected.value,
pageSize.value,
],
@ -302,6 +311,7 @@ watch(
customerFormState.value.currentCustomerId = undefined;
flowStore.rotate();
},
{ deep: true },
);
watch(

View file

@ -115,6 +115,10 @@ const useCustomerStore = defineStore('api-customer', () => {
customerType?: CustomerType;
startDate?: string;
endDate?: string;
businessType?: string;
province?: string;
district?: string;
subDistrict?: string;
},
Data extends Pagination<
(Customer &
@ -484,6 +488,10 @@ const useCustomerStore = defineStore('api-customer', () => {
activeBranchOnly?: boolean;
startDate?: string | Date;
endDate?: string | Date;
businessType?: string;
province?: string;
district?: string;
subDistrict?: string;
}) {
let url = baseUrl + '/' + 'customer-export';