feat: add crud business type
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s

This commit is contained in:
Thanaphon Frappet 2025-07-11 09:12:17 +07:00
parent e19d3f05f1
commit c3989768ed
6 changed files with 1746 additions and 0 deletions

View file

@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getRole } from 'src/services/keycloak';
import { createSelect, SelectProps } from './select';
import SelectInput from '../SelectInput.vue';
import { User as BusinessType } from 'src/stores/business-type/types';
import useStore from 'src/stores/business-type';
type SelectOption = BusinessType;
const value = defineModel<string | null | undefined>('value', {
required: true,
});
const valueOption = defineModel<SelectOption>('valueOption', {
required: false,
});
const selectOptions = ref<SelectOption[]>([]);
const { fetchList: getList, fetchById: getById } = useStore();
defineEmits<{
(e: 'create'): void;
}>();
type ExclusiveProps = {
codeOnly?: boolean;
selectFirstValue?: boolean;
branchVirtual?: boolean;
checkRole?: string[];
};
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
const { getOptions, setFirstValue, getSelectedOption, filter } =
createSelect<SelectOption>(
{
value,
valueOption,
selectOptions,
getList: async (query) => {
const ret = await getList({
query,
...props.params,
pageSize: 99999,
});
if (ret) return ret.result;
},
getByValue: async (id) => {
const ret = await getById(id);
if (ret) return ret;
},
},
{ valueField: 'id' },
);
onMounted(async () => {
await getOptions();
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
setFirstValue();
}
if (props.selectFirstValue) {
setDefaultValue();
} else await getSelectedOption();
});
function setDefaultValue() {
setFirstValue();
}
</script>
<template>
<SelectInput
v-model="value"
incremental
option-value="id"
:label
:placeholder
:readonly
:disable="disabled"
:option="selectOptions"
:hide-selected="false"
:fill-input="false"
:rules="[
(v: string) => !props.required || !!v || $t('form.error.required'),
]"
@filter="filter"
>
<template #selected-item="{ opt }">
{{
$i18n.locale === 'tha'
? opt.name + ' ' + opt.name
: opt.nameEN + ' ' + opt.nameEN
}}
</template>
<template #option="{ opt, scope }">
<q-item v-bind="scope.itemProps">
<span class="row items-center">
{{
$i18n.locale === 'tha'
? opt.name + ' ' + opt.name
: opt.nameEN + ' ' + opt.nameEN
}}
</span>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #append v-if="clearable">
<q-icon
v-if="!readonly && value"
name="mdi-close-circle"
@click.stop="value = ''"
class="cursor-pointer clear-btn"
/>
</template>
</SelectInput>
</template>

View file

@ -0,0 +1,658 @@
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import { InstitutionPayload } from 'src/stores/institution/types';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { useInstitution } from 'src/stores/institution';
import { baseUrl } from 'src/stores/utils';
import DrawerInfo from 'src/components/DrawerInfo.vue';
import DialogForm from 'src/components/DialogForm.vue';
import ProfileBanner from 'src/components/ProfileBanner.vue';
import SideMenu from 'src/components/SideMenu.vue';
import FormBank from 'src/components/01_branch-management/FormBank.vue';
import FormBasicInfoAgencies from 'src/components/07_agencies-management/FormBasicInfoAgencies.vue';
import {
UndoButton,
SaveButton,
EditButton,
DeleteButton,
} from 'src/components/button';
import AddressForm from 'src/components/form/AddressForm.vue';
import ImageUploadDialog from 'src/components/ImageUploadDialog.vue';
import { BankBook } from 'src/stores/branch/types';
import QrCodeUploadDialog from 'src/components/QrCodeUploadDialog.vue';
const institutionStore = useInstitution();
const model = defineModel<boolean>({ required: true, default: false });
const drawerModel = defineModel<boolean>('drawerModel', {
required: true,
default: false,
});
const imageListOnCreate = defineModel<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>('imageListOnCreate', { default: { selectedImage: '', list: [] } });
const deletesStatusQrCodeBankImag = defineModel<number[]>(
'deletesStatusQrCodeBankImag',
{ default: [] },
);
const imageState = reactive({
imageDialog: false,
isImageEdit: false,
refreshImageState: false,
imageUrl: '',
});
const imageFile = ref<File | null>(null);
const imageList = ref<{ selectedImage: string; list: string[] }>();
const qrCodeDialog = ref(false);
const qrCodeImageUrl = ref<string>('');
const currentIndexQrCodeBank = ref<number>(-1);
const statusQrCodeFile = ref<File | undefined>(undefined);
const refQrCodeUpload = ref();
const statusQrCodeUrl = ref<string>('');
const statusDeletesQrCode = ref<boolean>(false);
const props = withDefaults(
defineProps<{
readonly?: boolean;
isEdit?: boolean;
hideAction?: boolean;
hideDelete?: boolean;
dataId?: string;
}>(),
{ readonly: false, isEdit: false, dataId: '' },
);
defineExpose({ clearImageState });
const emit = defineEmits<{
(e: 'submit'): void;
(e: 'close'): void;
(e: 'changeStatus'): void;
(e: 'drawerUndo'): void;
(e: 'drawerEdit'): void;
(e: 'drawerDelete'): void;
(e: 'addImage'): void;
(e: 'removeImage'): void;
(e: 'submitImage', name: string): void;
}>();
const data = defineModel<InstitutionPayload>('data', {
required: true,
default: {
group: '',
name: '',
nameEN: '',
contactName: '',
contactEmail: '',
contactTel: '',
code: '',
addressEN: '',
address: '',
soi: '',
soiEN: '',
moo: '',
mooEN: '',
street: '',
streetEN: '',
subDistrictId: '',
districtId: '',
provinceId: '',
selectedImage: '',
},
});
const formBankBook = defineModel<BankBook[]>('formBankBook', {
default: [
{
bankName: '',
accountNumber: '',
bankBranch: '',
accountName: '',
accountType: '',
currentlyUse: true,
bankUrl: '',
},
],
});
function viewImage() {
imageState.imageDialog = true;
imageState.isImageEdit = false;
}
function editImage() {
imageState.imageDialog = imageState.isImageEdit = true;
}
async function fetchImageList(id: string, selectedName: string) {
const res = await institutionStore.fetchImageListById(id);
imageList.value = {
selectedImage: selectedName,
list: res.map((n: string) => `institution/${id}/image/${n}`),
};
return res;
}
async function addImage(file: File | null) {
if (!file) return;
if (!props.dataId) return;
await institutionStore.addImageList(
file,
props.dataId,
Date.now().toString(),
);
await fetchImageList(props.dataId, data.value.selectedImage || '');
}
async function removeImage(name: string) {
if (!name) return;
if (!props.dataId) return;
const targetName = name.split('/').pop() || '';
await institutionStore.deleteImageByName(props.dataId, targetName);
await fetchImageList(props.dataId, data.value.selectedImage || '');
}
async function submitImage(name: string) {
if (model.value) {
imageState.imageUrl = name;
imageState.imageDialog = false;
} else {
imageState.refreshImageState = true;
emit('submitImage', name);
}
}
function clearImageState() {
imageState.imageDialog = false;
imageFile.value = null;
imageListOnCreate.value = { selectedImage: '', list: [] };
imageState.refreshImageState = false;
}
function triggerEditQrCodeBank(opts?: { save?: boolean }) {
if (opts?.save === undefined) {
qrCodeDialog.value = true;
statusDeletesQrCode.value = false;
statusQrCodeUrl.value =
formBankBook.value[currentIndexQrCodeBank.value].bankUrl || '';
statusDeletesQrCode.value = false;
} else {
formBankBook.value[currentIndexQrCodeBank.value].bankUrl =
statusQrCodeUrl.value;
formBankBook.value[currentIndexQrCodeBank.value].bankQr =
statusQrCodeFile.value;
if (statusDeletesQrCode.value === true) {
deletesStatusQrCodeBankImag.value.push(currentIndexQrCodeBank.value);
}
if (statusDeletesQrCode.value === false) {
deletesStatusQrCodeBankImag.value =
deletesStatusQrCodeBankImag.value.filter(
(item) => item !== currentIndexQrCodeBank.value,
);
}
currentIndexQrCodeBank.value = -1;
statusDeletesQrCode.value = false;
}
}
watch(
() => imageFile.value,
() => {
if (imageFile.value !== null) imageState.isImageEdit = true;
},
);
watch(
() => data.value.selectedImage,
() => {
imageState.imageUrl = `${baseUrl}/institution/${props.dataId}/image/${data.value.selectedImage}`;
imageList.value
? (imageList.value.selectedImage = data.value.selectedImage || '')
: '';
imageState.refreshImageState = false;
},
);
watch(
() => drawerModel.value,
async () => {
if (drawerModel.value) {
await fetchImageList(props.dataId, data.value.selectedImage || '');
imageState.imageUrl = `${baseUrl}/institution/${props.dataId}/image/${data.value.selectedImage}`;
} else {
imageList.value = { selectedImage: '', list: [] };
}
},
);
</script>
<template>
<DialogForm
hide-footer
:title="$t('general.add', { text: $t('agencies.title') })"
v-model:modal="model"
:submit="() => $emit('submit')"
:close="
() => {
imageState.imageUrl = '';
clearImageState();
$emit('close');
}
"
>
<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
use-toggle
hide-fade
:toggle-title="$t('status.title')"
:icon="'ph-building-office'"
:img="imageState.imageUrl || null"
:title="data.name"
:caption="data.code"
:color="`hsla(var(--green-8-hsl)/1)`"
:bg-color="`hsla(var(--green-8-hsl)/0.1)`"
v-model:toggle-status="data.status"
@view="viewImage"
@edit="editImage"
@update:toggle-status="
() => {
data.status = data.status === 'CREATED' ? 'INACTIVE' : 'CREATED';
}
"
/>
</div>
<div
class="col surface-1 rounded bordered scroll row relative-position"
:class="{
'q-mb-lg q-mx-lg ': $q.screen.gt.sm,
'q-mb-sm q-mx-md': !$q.screen.gt.sm,
}"
id="group-form"
>
<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: 'agencies-form-basic-info',
},
{
name: $t('general.address'),
anchor: 'agencies-form-address-info',
},
{
name: $t('agencies.bankInfo'),
anchor: 'agencies-form-bank-info',
},
]"
background="transparent"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#agencies-form-content"
/>
</div>
</div>
<div
class="col-12 col-md-10"
id="agencies-form-content"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<div
class="rounded"
:class="{
'q-py-md q-px-lg': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
style="position: absolute; z-index: 99999; top: 0; right: 0"
>
<div class="surface-1 row rounded">
<SaveButton id="btn-info-basic-save" icon-only type="submit" />
</div>
</div>
<FormBasicInfoAgencies
id="agencies-form-basic-info"
class="q-mb-xl"
v-model:group="data.group"
v-model:name="data.name"
v-model:name-en="data.nameEN"
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
/>
<AddressForm
id="agencies-form-address-info"
dense
:prefix-id="''"
v-model:address="data.address"
v-model:address-en="data.addressEN"
v-model:moo="data.moo"
v-model:moo-en="data.mooEN"
v-model:soi="data.soi"
v-model:soi-en="data.soiEN"
v-model:street="data.street"
v-model:street-en="data.streetEN"
v-model:province-id="data.provinceId"
v-model:district-id="data.districtId"
v-model:sub-district-id="data.subDistrictId"
/>
<FormBank
id="agencies-form-bank-info"
title="agencies.bankInfo"
class="q-pt-xl"
dense
v-model:bank-book-list="formBankBook"
@view-qr="
(i) => {
currentIndexQrCodeBank = i;
triggerEditQrCodeBank();
}
"
@edit-qr="
(i) => {
currentIndexQrCodeBank = i;
refQrCodeUpload && refQrCodeUpload.browse();
}
"
/>
</div>
</div>
</DialogForm>
<DrawerInfo
bg-on
hide-action
:is-edit="isEdit"
:title="data.name"
v-model:drawerOpen="drawerModel"
:submit="() => $emit('submit')"
:close="
() => {
clearImageState();
$emit('close');
}
"
>
<div class="col column full-height">
<div
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
'q-mx-md q-my-sm': !$q.screen.gt.sm,
}"
>
<ProfileBanner
:prefix="data.name"
hide-fade
use-toggle
:readonly="hideAction"
:active="data.status !== 'INACTIVE'"
:toggle-title="$t('status.title')"
:icon="'ph-building-office'"
:title="data.name"
:caption="data.code"
:color="`hsla(var(${'--green-8'}-hsl)/1)`"
:bg-color="`hsla(var(${'--green-8'}-hsl)/0.1)`"
:img="
`${baseUrl}/institution/${dataId}/image/${data.selectedImage}`.concat(
imageState.refreshImageState ? `?ts=${Date.now()}` : '',
) || null
"
v-model:toggle-status="data.status"
@view="viewImage"
@edit="editImage"
@update:toggle-status="$emit('changeStatus')"
/>
</div>
<div
style="flex: 1; width: 100%; overflow-y: auto"
id="drawer-user-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 relative-position"
>
<div
class="rounded row"
:class="{
'q-py-md q-px-lg': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
style="position: absolute; z-index: 999; top: 0; right: 0"
>
<div
v-if="data.status !== 'INACTIVE' && !hideAction"
class="surface-1 row rounded"
>
<UndoButton
v-if="isEdit"
id="btn-info-basic-undo"
icon-only
@click="
() => {
$emit('drawerUndo');
}
"
type="button"
/>
<SaveButton
v-if="isEdit"
id="btn-info-basic-save"
icon-only
type="submit"
/>
<EditButton
v-if="!isEdit"
id="btn-info-basic-edit"
icon-only
@click="
() => {
$emit('drawerEdit');
// infoDrawerEdit = true;
// isImageEdit = true;
}
"
type="button"
/>
<DeleteButton
v-if="!isEdit && !hideDelete"
id="btn-info-basic-delete"
icon-only
@click="
() => {
$emit('drawerDelete');
}
"
type="button"
/>
</div>
</div>
<div
class="col"
style="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: 'agencies-basic-info',
},
{
name: $t('general.address'),
anchor: 'agencies-address-info',
},
{
name: $t('agencies.bankInfo'),
anchor: 'agencies-bank-info',
},
]"
background="transparent"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#agencies-view-content"
/>
</div>
</div>
<div
class="col-12 col-md-10 relative-position"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
id="agencies-view-content"
style="height: 100%; max-height: 100; overflow-y: auto"
>
<FormBasicInfoAgencies
id="agencies-basic-info"
:readonly
onDrawer
class="q-mb-xl"
v-model:group="data.group"
v-model:name="data.name"
v-model:name-en="data.nameEN"
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
/>
<AddressForm
id="agencies-address-info"
dense
:readonly
:prefix-id="data.code ? data.code : ''"
v-model:address="data.address"
v-model:address-en="data.addressEN"
v-model:moo="data.moo"
v-model:moo-en="data.mooEN"
v-model:soi="data.soi"
v-model:soi-en="data.soiEN"
v-model:street="data.street"
v-model:street-en="data.streetEN"
v-model:province-id="data.provinceId"
v-model:district-id="data.districtId"
v-model:sub-district-id="data.subDistrictId"
/>
<FormBank
id="agencies-bank-info"
title="agencies.bankInfo"
class="q-pt-xl"
dense
:readonly
v-model:bank-book-list="formBankBook"
@view-qr="
(i) => {
currentIndexQrCodeBank = i;
triggerEditQrCodeBank();
}
"
@edit-qr="
(i) => {
currentIndexQrCodeBank = i;
refQrCodeUpload && refQrCodeUpload.browse();
}
"
/>
</div>
</div>
</div>
</div>
</DrawerInfo>
<ImageUploadDialog
v-model:dialog-state="imageState.imageDialog"
v-model:file="imageFile"
v-model:on-create-data-list="imageListOnCreate"
v-model:image-url="imageState.imageUrl"
v-model:data-list="imageList"
:changeDisabled="hideAction"
:on-create="model"
:hiddenFooter="!imageState.isImageEdit"
@add-image="addImage"
@remove-image="removeImage"
@submit="submitImage"
>
<template #title>
<span v-if="!model" class="justify-center flex text-bold">
{{ $t('general.image') }}
{{ $i18n.locale === 'eng' ? data.nameEN : data.name }}
</span>
</template>
<template #error>
<div class="full-height full-width" style="background: white">
<div
class="full-height full-width flex justify-center items-center"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon width="3rem" icon="ph-building-office" />
</div>
</div>
</template>
</ImageUploadDialog>
<QrCodeUploadDialog
ref="refQrCodeUpload"
v-model:dialog-state="qrCodeDialog"
v-model:file="statusQrCodeFile as File"
v-model:image-url="statusQrCodeUrl"
@save="
(_file) => {
qrCodeDialog = false;
if (currentIndexQrCodeBank !== -1) {
triggerEditQrCodeBank({ save: true });
}
}
"
@clear="statusDeletesQrCode = true"
clearButton
>
<template #error>
<div
class="full-width full-height flex items-center justify-center"
style="color: gray"
>
<q-icon size="15rem" name="mdi-qrcode" />
</div>
</template>
</QrCodeUploadDialog>
</template>
<style scoped></style>

View file

@ -0,0 +1,858 @@
<script lang="ts" setup>
import { QTableProps } from 'quasar';
import { dialog } from 'src/stores/utils';
import { onMounted, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { baseUrl, canAccess } from 'src/stores/utils';
import useBusinessTypeStore from 'src/stores/business-type';
import { useNavigator } from 'src/stores/navigator';
import { useInstitution } from 'src/stores/institution';
import {
BusinessType,
BusinessTypePayLoad,
} from 'src/stores/business-type/types';
import { formatAddress } from 'src/utils/address';
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import PaginationComponent from 'src/components/PaginationComponent.vue';
import KebabAction from 'src/components/shared/KebabAction.vue';
import StatCardComponent from 'src/components/StatCardComponent.vue';
import FloatingActionButton from 'src/components/FloatingActionButton.vue';
import CreateButton from 'src/components/AddButton.vue';
import NoData from 'src/components/NoData.vue';
import AgenciesDialog from './AgenciesDialog.vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t } = useI18n();
const $q = useQuasar();
const useBusinessType = useBusinessTypeStore();
const navigatorStore = useNavigator();
const institutionStore = useInstitution();
const { data, page, pageMax, pageSize } = storeToRefs(institutionStore);
const fieldSelected = ref<('no' | 'name' | 'nameEn')[]>([
'no',
'name',
'nameEn',
]);
const fieldSelectedOption = ref<{ label: string; value: string }[]>([
{
label: 'general.order',
value: 'orderNumber',
},
{
label: 'general.name',
value: 'name',
},
{
label: 'general.nameEn',
value: 'nameEn',
},
]);
const columns = [
{
name: 'no',
align: 'center',
label: 'general.order',
field: 'no',
},
{
name: 'name',
align: 'left',
label: 'general.name',
field: 'name',
},
{
name: 'nameEn',
align: 'center',
label: 'general.nameEn',
field: 'nameEn',
},
] satisfies QTableProps['columns'];
const pageState = reactive({
hideStat: false,
inputSearch: '',
gridView: false,
total: 0,
addModal: false,
viewDrawer: false,
isDrawerEdit: true,
searchDate: [],
});
const deletesStatusQrCodeBankImag = ref<number[]>([]);
const blankFormData: BusinessTypePayLoad = {
name: '',
nameEN: '',
};
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const refAgenciesDialog = ref();
const formData = ref<BusinessTypePayLoad | undefined>(
structuredClone(blankFormData),
);
const currBusinessType = ref<BusinessType>();
const imageListOnCreate = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') {
formData.value = structuredClone(blankFormData);
pageState.addModal = true;
pageState.isDrawerEdit = true;
}
if (type === 'view') {
pageState.viewDrawer = true;
pageState.isDrawerEdit = false;
}
if (type === 'edit') {
pageState.viewDrawer = true;
pageState.isDrawerEdit = true;
}
}
function resetForm() {
pageState.isDrawerEdit = true;
pageState.addModal = false;
pageState.viewDrawer = false;
currBusinessType.value = undefined;
formData.value = structuredClone(blankFormData);
}
function undo() {
if (!currBusinessType.value) return;
assignFormData(currBusinessType.value);
pageState.isDrawerEdit = false;
}
function assignFormData(data: BusinessType) {
currBusinessType.value = data;
formData.value = { id: data.id, name: data.name, nameEN: data.nameEN };
}
async function submit(opt?: { selectedImage: string }) {
const payload = {
id: formData.value.id,
name: formData.value.name,
nameEN: formData.value.nameEN,
};
if (pageState.isDrawerEdit && currBusinessType.value?.id) {
const ret = await useBusinessType.editById(payload);
if (ret) {
pageState.isDrawerEdit = false;
await fetchData($q.screen.xs);
return;
}
} else {
await useBusinessType.create(payload);
await fetchData($q.screen.xs);
pageState.addModal = false;
return;
}
}
async function triggerDelete(id?: string) {
if (!id) return;
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 () => {
await useBusinessType.deleteById(id);
resetForm();
await fetchData($q.screen.xs);
},
cancel: () => {},
});
}
async function fetchData(mobileFetch?: boolean) {
const ret = await useBusinessType.fetchList({
page: mobileFetch ? 1 : page.value,
pageSize: mobileFetch
? data.value.length + (pageState.total === data.value.length ? 1 : 0)
: pageSize.value,
query: pageState.inputSearch,
});
if (ret) {
data.value =
$q.screen.xs && !mobileFetch
? [...data.value, ...ret.result]
: ret.result;
pageMax.value = Math.ceil(ret.total / pageSize.value);
pageState.total = ret.total;
}
}
onMounted(async () => {
navigatorStore.current.title = 'agencies.title';
navigatorStore.current.path = [{ text: 'agencies.caption', i18n: true }];
pageState.gridView = $q.screen.lt.md ? true : false;
await fetchData();
});
watch(
() => [pageState.inputSearch, statusFilter.value, pageState.searchDate],
() => {
page.value = 1;
data.value = [];
fetchData();
},
);
</script>
<template>
<FloatingActionButton
v-if="canAccess('agencies', 'edit')"
style="z-index: 999"
hide-icon
@click="triggerDialog('add')"
></FloatingActionButton>
<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));
"
>
{{ pageState.total }}
</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: 'ph-building-office',
count: pageState.total,
label: 'agencies.title',
color: 'light-green',
},
]"
:dark="$q.dark.isActive"
/>
</div>
</div>
</transition>
<!-- SEC: header content -->
<section class="col surface-1 rounded bordered overflow-hidden">
<div class="column full-height no-wrap">
<header
class="row surface-3 justify-between full-width items-center 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"
:active="$q.screen.lt.md && statusFilter !== 'all'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="statusFilter"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:for="'field-select-status'"
:options="[
{ label: $t('general.all'), value: 'all' },
{ label: $t('general.active'), value: 'statusACTIVE' },
{
label: $t('general.inactive'),
value: 'statusINACTIVE',
},
]"
/>
</AdvanceSearch>
</template>
</q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap">
<q-select
v-if="$q.screen.gt.sm"
v-model="statusFilter"
outlined
dense
option-value="value"
option-label="label"
class="col"
:class="{ 'offset-md-5': pageState.gridView }"
map-options
emit-value
:for="'field-select-status'"
:hide-dropdown-icon="$q.screen.lt.sm"
:options="[
{ label: $t('general.all'), value: 'all' },
{ label: $t('general.active'), value: 'statusACTIVE' },
{ label: $t('general.inactive'), value: 'statusINACTIVE' },
]"
/>
<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: $t(v.label),
}))
"
:display-value="$t('general.displayField')"
:hide-dropdown-icon="$q.screen.lt.sm"
v-model="fieldSelected"
option-label="label"
option-value="value"
autocomplete="off"
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>
</header>
<!-- SEC: body content -->
<article
v-if="data.length === 0"
class="col surface-2 flex items-center justify-center"
>
<NoData
v-if="pageState.inputSearch"
:not-found="!!pageState.inputSearch"
/>
<CreateButton
v-if="!pageState.inputSearch && pageState.total === 0"
@click="triggerDialog('add')"
label="general.add"
:i18n-args="{ text: $t('agencies.title') }"
/>
</article>
<article v-else class="col q-pa-md surface-2 scroll full-width">
<q-infinite-scroll
:offset="100"
@load="
(_, done) => {
if ($q.screen.gt.xs || page === pageMax) return;
page = page + 1;
fetchData().then(() => done(page >= pageMax));
}
"
>
<q-table
flat
bordered
:grid="pageState.gridView"
:rows="data"
:columns="columns"
class="full-width"
card-container-class="q-col-gutter-md"
row-key="name"
:rows-per-page-options="[0]"
hide-pagination
:visible-columns="fieldSelected"
>
<template v-slot:header="props">
<q-tr
style="background-color: hsla(var(--info-bg) / 0.07)"
:props="props"
>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ $t(col.label) }}
</q-th>
<q-th auto-width />
</q-tr>
</template>
<template v-slot:body="props">
<q-tr
:class="{
'app-text-muted': props.row.status === 'INACTIVE',
'status-active': props.row.status !== 'INACTIVE',
'status-inactive': props.row.status === 'INACTIVE',
}"
:props="props"
:style="
props.rowIndex % 2 !== 0
? $q.dark.isActive
? 'background: hsl(var(--gray-11-hsl)/0.2)'
: `background: #f9fafc`
: ''
"
>
<q-td
class="text-center"
v-if="fieldSelected.includes('orderNumber')"
>
{{
$q.screen.xs
? props.rowIndex + 1
: (page - 1) * pageSize + props.rowIndex + 1
}}
</q-td>
<q-td v-if="fieldSelected.includes('name')">
<section class="row items-center no-wrap">
<q-avatar size="md">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon icon="ph-building-office" />
</div>
</template>
</q-img>
<q-badge
class="absolute-bottom-right no-padding"
style="
border-radius: 50%;
min-width: 8px;
min-height: 8px;
"
:style="{
background: `var(--${props.row.status === 'INACTIVE' ? 'stone-5' : 'green-6'})`,
}"
></q-badge>
</q-avatar>
<span class="col q-pl-md">
<div>
{{
$i18n.locale === 'eng'
? props.row.nameEN
: props.row.name
}}
</div>
<div class="app-text-muted">
{{ props.row.code }}
</div>
</span>
</section>
</q-td>
<q-td v-if="fieldSelected.includes('address')">
{{
formatAddress({
address: props.row.address,
addressEN: props.row.addressEN,
moo: props.row.moo,
mooEN: props.row.mooEN,
soi: props.row.soi,
soiEN: props.row.soiEN,
street: props.row.street,
streetEN: props.row.streetEN,
province: props.row.province,
district: props.row.district,
subDistrict: props.row.subDistrict,
en: $i18n.locale === 'eng',
})
}}
<q-tooltip>
{{
formatAddress({
address: props.row.address,
addressEN: props.row.addressEN,
moo: props.row.moo,
mooEN: props.row.mooEN,
soi: props.row.soi,
soiEN: props.row.soiEN,
street: props.row.street,
streetEN: props.row.streetEN,
province: props.row.province,
district: props.row.district,
subDistrict: props.row.subDistrict,
en: $i18n.locale === 'eng',
})
}}
</q-tooltip>
</q-td>
<q-td>
<q-btn
icon="mdi-eye-outline"
:id="`btn-eye-${props.row.name}`"
size="sm"
dense
round
flat
@click.stop="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
/>
<KebabAction
v-if="canAccess('agencies', 'edit')"
:id-name="props.row.name"
:status="props.row.status"
@view="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
@edit="
() => {
assignFormData(props.row);
triggerDialog('edit');
}
"
@delete="() => triggerDelete(props.row.id)"
@change-status="() => triggerChangeStatus(props.row)"
/>
</q-td>
</q-tr>
</template>
<template v-slot:item="props">
<section class="column col-12 col-md-6">
<div class="bordered col surface-1 rounded q-pa-md">
<header class="row items-center">
<q-avatar size="xl">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon icon="ph-building-office" />
</div>
</template>
</q-img>
<q-badge
class="absolute-bottom-right no-padding"
style="
border-radius: 50%;
min-width: 10px;
min-height: 10px;
"
:style="{
background: `var(--${props.row.status === 'INACTIVE' ? 'stone-5' : 'green-6'})`,
}"
></q-badge>
</q-avatar>
<span class="text-weight-bold column q-pl-md">
{{
$i18n.locale === 'eng'
? props.row.nameEN
: props.row.name
}}
<span class="text-caption app-text-muted-2">
{{ props.row.code }}
</span>
</span>
<nav
class="row q-ml-auto items-center justify-end no-wrap"
>
<q-btn
icon="mdi-eye-outline"
size="sm"
dense
round
flat
@click.stop="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
/>
<KebabAction
v-if="canAccess('agencies', 'edit')"
:id-name="props.row.id"
:status="props.row.status"
@view="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
@edit="
() => {
assignFormData(props.row);
triggerDialog('edit');
}
"
@delete="() => triggerDelete(props.row.id)"
@change-status="() => triggerChangeStatus(props.row)"
/>
</nav>
</header>
<q-separator spaced="lg" />
<div class="row full-width">
<span class="col-2 app-text-muted">
{{ $t('general.address') }}
</span>
<span class="col">
{{
formatAddress({
address: props.row.address,
addressEN: props.row.addressEN,
moo: props.row.moo,
mooEN: props.row.mooEN,
soi: props.row.soi,
soiEN: props.row.soiEN,
street: props.row.street,
streetEN: props.row.streetEN,
province: props.row.province,
district: props.row.district,
subDistrict: props.row.subDistrict,
en: $i18n.locale === 'eng',
})
}}
<q-tooltip>
{{
formatAddress({
address: props.row.address,
addressEN: props.row.addressEN,
moo: props.row.moo,
mooEN: props.row.mooEN,
soi: props.row.soi,
soiEN: props.row.soiEN,
street: props.row.street,
streetEN: props.row.streetEN,
province: props.row.province,
district: props.row.district,
subDistrict: props.row.subDistrict,
en: $i18n.locale === 'eng',
})
}}
</q-tooltip>
</span>
</div>
</div>
</section>
</template>
</q-table>
<template v-slot:loading>
<div
v-if="$q.screen.lt.sm && page !== pageMax"
class="row justify-center"
>
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</article>
<!-- SEC: footer content -->
<footer
class="row justify-between items-center q-px-md q-py-sm surface-2"
v-if="pageMax > 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="pageSize" />
</div>
</div>
</div>
<div class="col-4 row justify-center app-text-muted">
{{
$q.screen.gt.sm
? $t('general.recordsPage', {
resultcurrentPage: data.length,
total: pageState.inputSearch
? data.length
: pageState.total,
})
: $t('general.ofPage', {
current: data.length,
total: pageState.inputSearch
? data.length
: pageState.total,
})
}}
</div>
<nav class="col-4 row justify-end">
<PaginationComponent
v-model:current-page="page"
v-model:max-page="pageMax"
:fetch-data="() => fetchData()"
/>
</nav>
</footer>
</div>
</section>
</div>
<AgenciesDialog
ref="refAgenciesDialog"
:data-id="currBusinessType && currBusinessType.id"
@drawer-delete="
() => {
if (currBusinessType) triggerDelete(currBusinessType.id);
}
"
@drawer-edit="pageState.isDrawerEdit = true"
@drawer-undo="undo"
@close="resetForm"
@submit="submit"
@submit-image="
async (v) => {
if (v) await submit({ selectedImage: v });
}
"
@change-status="triggerChangeStatus"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
:hide-action="!canAccess('agencies', 'edit')"
v-model="pageState.addModal"
v-model:drawer-model="pageState.viewDrawer"
v-model:data="formData"
v-model:form-bank-book="formData.bank"
v-model:image-list-on-create="imageListOnCreate"
v-model:deletes-status-qr-code-bank-imag="deletesStatusQrCodeBankImag"
/>
</template>
<style scoped>
.slide-enter-active {
transition: all 0.1s ease-out;
}
.slide-leave-active {
transition: all 0.1s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-enter-from,
.slide-leave-to {
transform: translateY(-20px);
opacity: 0;
}
</style>

View file

@ -157,6 +157,12 @@ const routes: RouteRecordRaw[] = [
name: 'Notification',
component: () => import('pages/00_notification/MainPage.vue'),
},
{
path: '/business-type',
name: 'businessType',
component: () =>
import('pages/16_ิbusiness-type-management/MainPage.vue'),
},
{
path: '/manual',
name: 'Manual',

View file

@ -0,0 +1,85 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { Pagination, Status } from '../types';
import { api } from 'src/boot/axios';
import { BusinessType, BusinessTypePayLoad } from './types';
import axios from 'axios';
const useBusinessTypeStore = defineStore('business-type-store', () => {
const data = ref<Pagination<BusinessType[]>>();
async function fetchList(opts?: {
page?: number;
pageSize?: number;
query?: string;
}) {
const res = await api.get<Pagination<BusinessType[]>>('/business-type', {
params: opts,
});
if (res && res.status === 200) {
return res.data;
}
return false;
}
async function fetchById(id: string) {
const res = await api.get<BusinessType>(`/business-type/${id}`);
if (!res) return false;
if (res.status === 200) return res.data;
if (res.status === 204) return null;
return false;
}
async function create(data: BusinessTypePayLoad) {
const res = await api.post('/business-type', {
...data,
});
if (res.status < 400) {
return res.data;
}
return null;
}
async function editById(data: BusinessTypePayLoad) {
const { id, ...payload } = data;
const res = await api.put<BusinessTypePayLoad>(
`/business-type/${id}`,
payload,
);
if (!res) return false;
return res.data;
}
async function deleteById(id: string) {
const res = await api.delete<BusinessType>(`/business-type/${id}`);
if (!res) return false;
if (res.status === 200) return res.data;
return false;
}
return {
data,
fetchList,
fetchById,
create,
editById,
deleteById,
};
});
export * from './types';
export default useBusinessTypeStore;

View file

@ -0,0 +1,15 @@
export type BusinessType = {
updatedByUserId: string;
updatedAt: Date;
createdByUserId: string;
createdAt: Date;
nameEN: string;
name: string;
id: string;
};
export type BusinessTypePayLoad = {
nameEN: string;
name: string;
id?: string;
};