feat: menu agencies (#65)

* feat: agencies => add agencies menu

---------

Co-authored-by: Methapon Metanipat <methapon@frappet.com>
This commit is contained in:
puriphatt 2024-11-08 13:11:42 +07:00 committed by GitHub
parent f6479cc72d
commit b87e15301f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1305 additions and 18 deletions

View file

@ -0,0 +1,73 @@
<script lang="ts" setup>
import SelectInput from '../shared/SelectInput.vue';
import useOptionStore from 'src/stores/options';
const optionStore = useOptionStore();
defineProps<{
dense?: boolean;
outlined?: boolean;
readonly?: boolean;
onDrawer?: boolean;
}>();
const group = defineModel('group', { default: '' });
const name = defineModel('name', { default: '' });
const nameEn = defineModel('nameEn', { default: '' });
</script>
<template>
<div class="row col-12">
<div class="col-12 q-pb-sm row items-center">
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-sm"
color="info"
name="mdi-office-building-outline"
style="background-color: var(--surface-3)"
/>
<span class="text-body1 text-weight-bold">
{{ $t(`form.field.basicInformation`) }}
</span>
</div>
<div class="col-12 row q-col-gutter-sm">
<SelectInput
:disable="!readonly && onDrawer"
:readonly="readonly"
for="input-agencies-code"
:label="$t('agencies.group')"
option-label="value"
:option="optionStore.globalOption?.agenciesType"
v-model="group"
/>
<q-input
for="input-agencies-name"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="$t('agencies.name')"
v-model="name"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
for="input-agencies-name-en"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col"
:label="'Agencies Name'"
v-model="nameEn"
:rules="[
(val: string) => !!val || $t('form.error.required'),
(val: string) =>
/^[A-Za-z0-9.,' -]+$/.test(val) || $t('form.error.letterOnly'),
]"
/>
</div>
</div>
</template>
<style scoped></style>

View file

@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import ToggleButton from './button/ToggleButton.vue';
import { Icon } from '@iconify/vue/dist/iconify.js';
defineProps<{
img?: string | null;
@ -120,7 +121,7 @@ const smallBanner = ref(false);
color: `${color || 'white'}`,
}"
>
<q-icon :name="icon || 'mdi-account'" />
<Icon :icon="icon || 'mdi-account'" />
</div>
</template>
</q-img>
@ -132,7 +133,7 @@ const smallBanner = ref(false);
color: `${color || 'white'}`,
}"
>
<q-icon :name="icon || 'mdi-account'" />
<Icon :icon="icon || 'mdi-account'" />
</div>
</template>
</q-img>
@ -145,7 +146,7 @@ const smallBanner = ref(false);
color: `${color || 'white'}`,
}"
>
<q-icon :name="icon || 'mdi-account'" />
<Icon :icon="icon || 'mdi-account'" />
</div>
<q-badge
v-if="!hideActive"
@ -332,9 +333,9 @@ const smallBanner = ref(false);
color: `${color || 'white'}`,
}"
>
<q-icon
<Icon
class="full-width full-height flex items-center justify-center"
:name="icon || 'mdi-account'"
:icon="icon || 'mdi-account'"
/>
</div>
</template>
@ -347,9 +348,9 @@ const smallBanner = ref(false);
color: `${color || 'white'}`,
}"
>
<q-icon
<Icon
class="full-width full-height flex items-center justify-center"
:name="icon || 'mdi-account'"
:icon="icon || 'mdi-account'"
/>
</div>
</template>
@ -363,9 +364,9 @@ const smallBanner = ref(false);
color: `${color || 'white'}`,
}"
>
<q-icon
<Icon
class="full-width full-height flex items-center justify-center"
:name="icon || 'mdi-account'"
:icon="icon || 'mdi-account'"
/>
</div>

View file

@ -1,4 +1,6 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
const props = withDefaults(
defineProps<{
branch: {
@ -18,7 +20,8 @@ const props = withDefaults(
| 'magenta'
| 'blue'
| 'lime'
| 'light-purple';
| 'light-purple'
| 'light-green';
}[];
dark?: boolean;
textSize?: string;
@ -45,8 +48,9 @@ const props = withDefaults(
size="lg"
style="background-color: hsla(0 0% 100% /0.2)"
text-color="white"
:icon="v.icon"
/>
>
<Icon :icon="v.icon" width="24px" />
</q-avatar>
</div>
<div class="col-6 justify-center column">
<div
@ -127,6 +131,10 @@ const props = withDefaults(
--_color: var(--jungle-8-hsl);
}
.stat-card__light-green {
--_color: var(--green-8-hsl);
}
.stat-card__light-purple {
--_color: var(--purple-7-hsl);
}

View file

@ -38,12 +38,16 @@ const workplace = defineModel<string>('workplace', { default: '' });
const workplaceEN = defineModel<string>('workplaceEn', { default: '' });
const address = defineModel('address', { default: '' });
const addressEN = defineModel('addressEn', { default: '' });
const street = defineModel('street', { default: '' });
const streetEN = defineModel('streetEn', { default: '' });
const moo = defineModel('moo', { default: '' });
const mooEN = defineModel('mooEn', { default: '' });
const soi = defineModel('soi', { default: '' });
const soiEN = defineModel('soiEn', { default: '' });
const street = defineModel<string | null | undefined>('street', {
default: '',
});
const streetEN = defineModel<string | null | undefined>('streetEn', {
default: '',
});
const moo = defineModel<string | null | undefined>('moo', { default: '' });
const mooEN = defineModel<string | null | undefined>('mooEn', { default: '' });
const soi = defineModel<string | null | undefined>('soi', { default: '' });
const soiEN = defineModel<string | null | undefined>('soiEn', { default: '' });
const provinceId = defineModel<string | null | undefined>('provinceId');
const districtId = defineModel<string | null | undefined>('districtId');
const subDistrictId = defineModel<string | null | undefined>('subDistrictId');

View file

@ -170,6 +170,7 @@ export default {
workflow: 'Workflow',
customer: 'Customer',
mainData: 'Main Data',
agencies: 'Agencies',
},
sales: {
@ -800,6 +801,14 @@ export default {
responsiblePerson: 'Responsible Person',
},
agencies: {
title: 'Agencies',
caption: 'Manage Agencies',
code: 'Agencies Code',
group: 'Agencies Group',
name: 'Agencies Name',
},
dialog: {
title: {
incompleteDataEntry: 'Incomplete Data Entry',

View file

@ -170,6 +170,7 @@ export default {
workflow: 'ขั้นตอนการทำงาน',
customer: 'ลูกค้า',
mainData: 'ข้อมูลหลัก',
agencies: 'หน่วยงาน',
},
sales: {
@ -796,6 +797,14 @@ export default {
responsiblePerson: 'ผู้รับผิดชอบ',
},
agencies: {
title: 'หน่วยงาน',
caption: 'จัดการหน่วยงานทั้งหมด',
code: 'รหัสหน่วยงาน',
group: 'กลุ่มหน่วยงาน',
name: 'ชื่อหน่วยงาน',
},
dialog: {
title: {
incompleteDataEntry: 'กรอกข้อมูลไม่ครบ',

View file

@ -132,6 +132,7 @@ onMounted(async () => {
{ label: 'productService', route: '/product-service' },
{ label: 'document', route: '/document-management' },
{ label: 'customer', route: '/customer-management' },
{ label: 'agencies', route: '/agencies-management' },
],
},
{

View file

@ -0,0 +1,334 @@
<script setup lang="ts">
import DialogForm from 'src/components/DialogForm.vue';
import DrawerInfo from 'src/components/DrawerInfo.vue';
import ProfileBanner from 'src/components/ProfileBanner.vue';
import SideMenu from 'src/components/SideMenu.vue';
import FormBasicInfoAgencies from 'src/components/07_agencies-management/FormBasicInfoAgencies.vue';
import AddressForm from 'src/components/form/AddressForm.vue';
import {
UndoButton,
SaveButton,
EditButton,
DeleteButton,
} from 'src/components/button';
import { InstitutionPayload } from 'src/stores/institution/types';
const model = defineModel<boolean>({ required: true, default: false });
const drawerModel = defineModel<boolean>('drawerModel', {
required: true,
default: false,
});
withDefaults(
defineProps<{
readonly?: boolean;
isEdit?: boolean;
}>(),
{ readonly: false, isEdit: false },
);
defineEmits<{
(e: 'submit'): void;
(e: 'close'): void;
(e: 'changeStatus'): void;
(e: 'drawerUndo'): void;
(e: 'drawerEdit'): void;
(e: 'drawerDelete'): void;
}>();
const data = defineModel<InstitutionPayload>('data', {
required: true,
default: {
group: '',
name: '',
nameEN: '',
code: '',
addressEN: '',
address: '',
soi: '',
soiEN: '',
moo: '',
mooEN: '',
street: '',
streetEN: '',
subDistrictId: '',
districtId: '',
provinceId: '',
},
});
</script>
<template>
<DialogForm
hide-footer
:title="$t('general.add', { text: $t('agencies.title') })"
v-model:modal="model"
:submit="() => $emit('submit')"
:close="() => $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
no-image-action
readonly
active
hide-fade
hide-active
: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)`"
/>
<!-- v-model:toggle-status="currentStatusGroupType"
@update:toggle-status="
() => {
currentStatusGroupType =
currentStatusGroupType === '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-basic-info',
},
{
name: $t('general.address'),
anchor: 'agencies-address-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-py-md q-px-lg': !$q.screen.gt.sm,
}"
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<div
class="q-py-md q-px-lg"
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-basic-info"
class="q-mb-xl"
v-model:group="data.group"
v-model:name="data.name"
v-model:name-en="data.nameEN"
/>
<AddressForm
id="agencies-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"
/>
</div>
</div>
</DialogForm>
<DrawerInfo
bg-on
hide-action
:is-edit="isEdit"
:title="data.name"
v-model:drawerOpen="drawerModel"
:submit="() => $emit('submit')"
:close="() => $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
no-image-action
readonly
active
hide-fade
hide-active
: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)`"
/>
</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-py-sm q-px-lg': !$q.screen.gt.sm,
}"
style="position: absolute; z-index: 999; top: 0; right: 0"
>
<div v-if="true" 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"
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',
},
]"
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 relative-position"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-py-md q-px-lg': !$q.screen.gt.sm,
}"
id="user-form-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"
/>
<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"
/>
</div>
</div>
</div>
</div>
</DrawerInfo>
</template>
<style scoped></style>

View file

@ -0,0 +1,723 @@
<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';
import { useNavigator } from 'src/stores/navigator';
import { useInstitution } from 'src/stores/institution';
import { Institution, InstitutionPayload } from 'src/stores/institution/types';
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';
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: '',
};
const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
const currAgenciesData = ref<Institution>();
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,
};
}
async function submit() {
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,
};
if (pageState.isDrawerEdit && currAgenciesData.value?.id) {
const ret = await institutionStore.editInstitution(
Object.assign(payload, { id: currAgenciesData.value.id }),
);
if (ret) {
pageState.isDrawerEdit = false;
currAgenciesData.value = ret;
await fetchData();
return;
}
}
await institutionStore.createInstitution({
...payload,
code: formData.value.group || '',
});
await fetchData();
pageState.addModal = false;
}
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,
});
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();
});
</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
class="row col-12 col-md-5 justify-end"
:class="{ 'q-pt-xs': $q.screen.lt.md }"
style="white-space: nowrap"
>
<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' },
]"
/>
<q-select
v-if="!pageState.gridView"
id="select-field"
for="select-field"
class="col 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"
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="false"
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">
<q-avatar
size="md"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon icon="ph-building-office" />
</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')">
{{
$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>
</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">
<q-avatar
size="xl"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon icon="ph-building-office" />
</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
@change-status="triggerChangeStatus"
@drawer-delete="
() => {
if (currAgenciesData) triggerDelete(currAgenciesData.id);
}
"
@drawer-edit="pageState.isDrawerEdit = true"
@drawer-undo="undo"
@close="resetForm"
@submit="submit"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal"
v-model:drawer-model="pageState.viewDrawer"
v-model:data="formData"
/>
</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

@ -89,6 +89,11 @@ const routes: RouteRecordRaw[] = [
name: 'document-management',
component: () => import('pages/06_edm/MainPage.vue'),
},
{
path: '/agencies-management',
name: 'agencies-management',
component: () => import('pages/07_agencies-management/MainPage.vue'),
},
],
},

View file

@ -0,0 +1,75 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { Institution, InstitutionPayload } from './types';
import { api } from 'src/boot/axios';
import { PaginationResult } from 'src/types';
export const useInstitution = defineStore('institution-store', () => {
const data = ref<Institution[]>([]);
const page = ref<number>(1);
const pageMax = ref<number>(1);
const pageSize = ref<number>(30);
async function getInstitution(id: string) {
const res = await api.get<Institution>(`/institution/${id}`);
if (res.status < 400) {
return res.data;
}
return null;
}
async function getInstitutionList(params?: {
page?: number;
pageSize?: number;
query?: string;
group?: string;
}) {
const res = await api.get<PaginationResult<Institution>>('/institution', {
params,
});
if (res.status < 400) {
return res.data;
}
return null;
}
async function createInstitution(data: InstitutionPayload) {
const res = await api.post('/institution', data);
if (res.status < 400) {
return res.data;
}
return null;
}
async function editInstitution(data: InstitutionPayload & { id: string }) {
const res = await api.put(`/institution/${data.id}`, {
...data,
id: undefined,
});
if (res.status < 400) {
return res.data;
}
return null;
}
async function deleteInstitution(id: string) {
const res = await api.delete(`/institution/${id}`);
if (res.status < 400) {
return res.data;
}
return null;
}
return {
data,
page,
pageMax,
pageSize,
getInstitution,
getInstitutionList,
createInstitution,
editInstitution,
deleteInstitution,
};
});

View file

@ -0,0 +1,45 @@
import { District, Province, SubDistrict } from '../address';
export type Institution = {
id: string;
code: string;
group: string;
name: string;
nameEN: string;
addressEN: string;
address: string;
soi: string | null;
soiEN: string | null;
moo: string | null;
mooEN: string | null;
street: string | null;
streetEN: string | null;
province?: Province;
district?: District;
subDistrict?: SubDistrict;
subDistrictId: string;
districtId: string;
provinceId: string;
};
export type InstitutionPayload = {
code: string;
name: string;
nameEN: string;
group?: string;
addressEN: string;
address: string;
soi?: string | null;
soiEN?: string | null;
moo?: string | null;
mooEN?: string | null;
street?: string | null;
streetEN?: string | null;
subDistrictId: string;
districtId: string;
provinceId: string;
};