jws-frontend/src/pages/07_agencies-management/MainPage.vue

804 lines
26 KiB
Vue
Raw Normal View History

<script lang="ts" setup>
import { QTableProps } from 'quasar';
import { dialog } from 'src/stores/utils';
import { onMounted, reactive, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { useI18n } from 'vue-i18n';
2024-11-08 17:21:25 +07:00
import { baseUrl } from 'src/stores/utils';
import { useNavigator } from 'src/stores/navigator';
import { useInstitution } from 'src/stores/institution';
import { Institution, InstitutionPayload } from 'src/stores/institution/types';
2024-11-11 12:11:54 +07:00
import { formatAddress } from 'src/utils/address';
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';
2024-11-08 17:09:02 +07:00
import { computed } from 'vue';
import { watch } from 'vue';
const { t } = useI18n();
const navigatorStore = useNavigator();
const institutionStore = useInstitution();
const { data, page, pageMax, pageSize } = storeToRefs(institutionStore);
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const fieldSelected = ref<('orderNumber' | 'name' | 'address')[]>([
'orderNumber',
'name',
'address',
]);
const fieldSelectedOption = ref<{ label: string; value: string }[]>([
{
label: 'general.order',
value: 'orderNumber',
},
{
label: 'general.name',
value: 'name',
},
{
label: 'general.address',
value: 'address',
},
]);
const columns = [
{
name: 'orderNumber',
align: 'center',
label: 'general.order',
field: 'branchNo',
},
{
name: 'name',
align: 'left',
label: 'general.name',
field: 'name',
},
{
name: 'address',
align: 'center',
label: 'general.address',
field: 'address',
},
] satisfies QTableProps['columns'];
const pageState = reactive({
hideStat: false,
inputSearch: '',
fieldSelected: [],
gridView: false,
total: 0,
addModal: false,
viewDrawer: false,
isDrawerEdit: true,
});
const blankFormData: InstitutionPayload = {
group: '',
code: '',
name: '',
nameEN: '',
addressEN: '',
address: '',
soi: '',
soiEN: '',
moo: '',
mooEN: '',
street: '',
streetEN: '',
subDistrictId: '',
districtId: '',
provinceId: '',
2024-11-08 17:21:25 +07:00
selectedImage: '',
};
2024-11-08 17:21:25 +07:00
const refAgenciesDialog = ref();
const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
const currAgenciesData = ref<Institution>();
2024-11-08 17:21:25 +07:00
const onCreateImageList = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') {
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;
currAgenciesData.value = undefined;
formData.value = structuredClone(blankFormData);
}
function undo() {
if (!currAgenciesData.value) return;
assignFormData(currAgenciesData.value);
pageState.isDrawerEdit = false;
}
function assignFormData(data: Institution) {
currAgenciesData.value = data;
formData.value = {
group: data.group,
code: data.code,
name: data.name,
nameEN: data.nameEN,
addressEN: data.addressEN,
address: data.address,
soi: data.soi,
soiEN: data.soiEN,
moo: data.moo,
mooEN: data.mooEN,
street: data.street,
streetEN: data.streetEN,
subDistrictId: data.subDistrictId,
districtId: data.districtId,
provinceId: data.provinceId,
2024-11-08 17:21:25 +07:00
selectedImage: data.selectedImage,
};
}
2024-11-08 17:21:25 +07:00
async function submit(opt?: { selectedImage: string }) {
const payload = {
group: undefined,
code: formData.value.code,
name: formData.value.name,
nameEN: formData.value.nameEN,
addressEN: formData.value.addressEN,
address: formData.value.address,
soi: formData.value.soi,
soiEN: formData.value.soiEN,
moo: formData.value.moo,
mooEN: formData.value.mooEN,
street: formData.value.street,
streetEN: formData.value.streetEN,
subDistrictId: formData.value.subDistrictId,
districtId: formData.value.districtId,
provinceId: formData.value.provinceId,
};
2024-11-08 17:21:25 +07:00
if (
(pageState.isDrawerEdit && currAgenciesData.value?.id) ||
(opt?.selectedImage && currAgenciesData.value?.id)
) {
const ret = await institutionStore.editInstitution(
2024-11-08 17:21:25 +07:00
Object.assign(payload, {
id: currAgenciesData.value.id,
selectedImage: opt?.selectedImage || undefined,
}),
);
2024-11-08 17:21:25 +07:00
if (ret) {
pageState.isDrawerEdit = false;
currAgenciesData.value = ret;
2024-11-08 17:21:25 +07:00
formData.value.selectedImage = ret.selectedImage;
await fetchData();
2024-11-08 17:21:25 +07:00
if (refAgenciesDialog.value && opt?.selectedImage) {
refAgenciesDialog.value.clearImageState();
}
return;
}
2024-11-08 17:21:25 +07:00
} else {
await institutionStore.createInstitution(
{
...payload,
code: formData.value.group || '',
},
onCreateImageList.value,
);
2024-11-08 17:21:25 +07:00
await fetchData();
pageState.addModal = false;
return;
}
}
async function triggerChangeStatus(data?: Institution) {}
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 institutionStore.deleteInstitution(id);
resetForm();
await fetchData();
},
cancel: () => {},
});
}
async function fetchData() {
const ret = await institutionStore.getInstitutionList({
page: page.value,
pageSize: pageSize.value,
2024-11-08 17:09:02 +07:00
query: pageState.inputSearch,
});
if (ret) {
data.value = 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 }];
await fetchData();
});
2024-11-08 17:09:02 +07:00
watch(
() => pageState.inputSearch,
() => fetchData(),
);
</script>
<template>
<FloatingActionButton
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" style="overflow: hidden">
<div class="column full-height">
<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="q-mr-md col-12 col-md-3"
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
v-model="pageState.inputSearch"
debounce="200"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
<div
2024-11-08 17:28:49 +07:00
class="row col-12 col-md-4 justify-end"
:class="{ 'q-pt-xs': $q.screen.lt.md }"
style="white-space: nowrap"
>
2024-11-08 17:28:49 +07:00
<!-- <q-select
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' },
]"
2024-11-08 17:28:49 +07:00
/> -->
<q-select
v-if="!pageState.gridView"
id="select-field"
for="select-field"
2024-11-08 17:28:49 +07:00
class="col"
: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
2024-11-08 17:21:25 +07:00
v-if="data.length === 0"
class="col surface-2 flex items-center justify-center"
>
<NoData
v-if="pageState.total !== 0"
:not-found="!!pageState.inputSearch"
/>
<CreateButton
v-if="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-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')"
>
{{ props.rowIndex + 1 }}
<!-- {{ (currentPage - 1) * pageSize + props.rowIndex + 1 }} -->
</q-td>
<q-td v-if="fieldSelected.includes('name')">
<section class="row items-center no-wrap">
2024-11-08 17:21:25 +07:00
<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-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')">
{{
2024-11-11 12:11:54 +07:00
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>
{{
2024-11-11 12:11:54 +07:00
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"
size="sm"
dense
round
flat
@click.stop="
() => {
assignFormData(props.row);
triggerDialog('view');
}
"
/>
<KebabAction
hide-toggle
: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)"
/>
</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">
2024-11-08 17:21:25 +07:00
<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-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
hide-toggle
: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">
{{
$i18n.locale === 'eng'
? `${props.row.addressEN}, ${props.row.mooEN && `${$t('form.moo')} ${props.row.mooEN},`} ${props.row.soiEN && `${$t('form.soi')} ${props.row.soiEN},`} ${props.row.moo && `${props.row.streetEN} Rd,`} ${props.row.subDistrict?.nameEN}, ${props.row.district?.nameEN}, ${props.row.province?.nameEN} ${props.row.subDistrict?.zipCode}` ||
'-'
: `${props.row.address}, ${props.row.moo && `${$t('form.moo')} ${props.row.moo},`} ${props.row.soi && `${$t('form.soi')} ${props.row.soi},`} ${props.row.street && `${$t('form.road')} ${props.row.street},`} ${props.row.subDistrict?.name}, ${props.row.district?.name}, ${props.row.province?.name} ${props.row.subDistrict?.zipCode}` ||
'-'
}}
<q-tooltip>
{{
$i18n.locale === 'eng'
? `${props.row.addressEN}, ${props.row.mooEN && `${$t('form.moo')} ${props.row.mooEN},`} ${props.row.soiEN && `${$t('form.soi')} ${props.row.soiEN},`} ${props.row.moo && `${props.row.streetEN} Rd,`} ${props.row.subDistrict?.nameEN}, ${props.row.district?.nameEN}, ${props.row.province?.nameEN} ${props.row.subDistrict?.zipCode}` ||
'-'
: `${props.row.address}, ${props.row.moo && `${$t('form.moo')} ${props.row.moo},`} ${props.row.soi && `${$t('form.soi')} ${props.row.soi},`} ${props.row.street && `${$t('form.road')} ${props.row.street},`} ${props.row.subDistrict?.name}, ${props.row.district?.name}, ${props.row.province?.name} ${props.row.subDistrict?.zipCode}` ||
'-'
}}
</q-tooltip>
</span>
</div>
</div>
</section>
</template>
</q-table>
</article>
<!-- SEC: footer content -->
<footer
class="row justify-between items-center q-px-md q-py-sm surface-2"
v-if="pageMax > 0"
>
<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>
<q-btn-dropdown
dense
unelevated
:label="pageSize"
class="bordered q-pl-md"
>
<q-list>
<q-item
v-for="v in [10, 30, 50, 100, 500, 1000]"
:key="v"
clickable
v-close-popup
@click="pageSize = v"
>
<q-item-section>
<q-item-label>{{ v }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</div>
</div>
<div class="col-4 row justify-center app-text-muted">
{{
$t('general.recordsPage', {
resultcurrentPage: 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
2024-11-08 17:21:25 +07:00
ref="refAgenciesDialog"
:data-id="currAgenciesData && currAgenciesData.id"
@change-status="triggerChangeStatus"
@drawer-delete="
() => {
if (currAgenciesData) triggerDelete(currAgenciesData.id);
}
"
@drawer-edit="pageState.isDrawerEdit = true"
@drawer-undo="undo"
@close="resetForm"
@submit="submit"
2024-11-08 17:21:25 +07:00
@submit-image="
async (v) => {
if (v) await submit({ selectedImage: v });
}
"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal"
v-model:drawer-model="pageState.viewDrawer"
v-model:data="formData"
2024-11-08 17:21:25 +07:00
v-model:on-create-image-list="onCreateImageList"
/>
</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>