feat: add notification dialog component and integrate with main layout

This commit is contained in:
puriphatt 2025-03-06 10:47:57 +07:00
parent ede8e80181
commit 65bf510386
3 changed files with 370 additions and 22 deletions

View file

@ -1,10 +1,11 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed, reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
import { Icon } from '@iconify/vue';
import { useI18n } from 'vue-i18n';
import moment from 'moment';
import useLoader from 'stores/loader';
import ProfileMenu from './ProfileMenu.vue';
@ -18,8 +19,7 @@ import { useNavigator } from 'src/stores/navigator';
import { initLang, initTheme, Lang, setLang } from 'src/utils/ui';
import { baseUrl } from 'stores/utils';
import { useNotification } from 'src/stores/notification';
import moment from 'moment';
import { computed } from 'vue';
import NotiDialog from 'src/pages/00_notification/NotiDialog.vue';
const useMyBranch = useMyBranchStore();
const { fetchListMyBranch } = useMyBranch;
@ -47,7 +47,6 @@ const canvasModal = ref(false);
const leftDrawerOpen = ref<boolean>(false);
const leftDrawerMini = ref(false);
const filterUnread = ref(false);
const unread = computed<number>(
() => notificationData.value.filter((v) => !v.read).length || 0,
);
@ -66,15 +65,20 @@ const language: {
{ value: Lang.English, label: 'English', icon: 'us', date: 'en-gb' },
];
const notiOpen = ref(false);
const state = reactive({
filterUnread: false,
notiOpen: false,
notiDialog: false,
notiId: '',
});
const notiMenu = ref<NotificationButton[]>([
{
item: 'ทั้งหมด',
item: 'all',
color: 'noti-switch-on',
active: true,
},
{
item: 'ยังไม่ได้อ่าน',
item: 'unread',
color: 'noti-switch-off',
active: false,
},
@ -86,12 +90,12 @@ function setActive(button: NotificationButton) {
color: current.item !== button.item ? 'noti-switch-off' : 'noti-switch-on',
active: current.item === button.item,
}));
if (button.item === 'ยังไม่ได้อ่าน') {
if (button.item === 'unread') {
// noti.value?.result &&
filterUnread.value = true;
state.filterUnread = true;
}
if (button.item === 'ทั้งหมด') {
filterUnread.value = false;
if (button.item === 'all') {
state.filterUnread = false;
}
}
@ -110,6 +114,11 @@ function doLogout() {
});
}
function readNoti(id: string) {
state.notiDialog = true;
state.notiId = id;
}
onMounted(async () => {
initTheme();
initLang();
@ -281,7 +290,7 @@ onMounted(async () => {
:size="$q.screen.lt.sm ? 'sm' : ''"
:class="{ bordered: $q.dark.isActive, dark: $q.dark.isActive }"
style="color: var(--surface-1)"
@click="notiOpen = !notiOpen"
@click="state.notiOpen = !state.notiOpen"
>
<q-icon name="mdi-bell" />
<q-badge v-if="unread !== 0" rounded floating color="negative">
@ -292,10 +301,16 @@ onMounted(async () => {
:offset="[0, 10]"
anchor="bottom middle"
self="top middle"
@before-hide="notiOpen = false"
@before-hide="
() => {
state.notiOpen = false;
}
"
>
<div class="q-px-md q-py-sm row col-12 items-center">
<div class="text-subtitle1 text-weight-bold">แจงเตอน</div>
<div class="text-subtitle1 text-weight-bold">
{{ $t('noti.title') }}
</div>
<q-space />
</div>
<div class="q-px-md q-pb-md q-gutter-x-md">
@ -307,22 +322,29 @@ onMounted(async () => {
:flat="!btn.active"
:unelevated="btn.active"
:key="index"
:label="btn.item"
:label="$t('noti.' + btn.item)"
:class="btn.color"
@click="setActive(btn)"
/>
</div>
<div style="max-height: 30vh; overflow-y: auto">
<div class="caption cursor-pointer">
<div style="max-height: 30vh; width: 400px; overflow-y: auto">
<section
v-if="
state.filterUnread
? notificationData.filter((v) => !v.read).length
: notificationData.length
"
class="caption cursor-pointer"
>
<q-item
v-for="(item, i) in state.filterUnread
? notificationData.filter((v) => !v.read)
: notificationData"
dense
clickable
class="q-py-sm"
v-ripple
v-for="(item, i) in filterUnread
? notificationData.filter((v) => !v.read)
: notificationData"
@click="notificationStore.getNotification(item.id)"
@click="readNoti(item.id)"
:key="i"
>
<q-avatar
@ -354,7 +376,23 @@ onMounted(async () => {
{{ item.detail }}
</q-tooltip>
</q-item>
</div>
</section>
<section v-else class="text-center q-py-sm">
<span class="app-text-muted">
{{ $t('general.noData') }}
</span>
</section>
</div>
<div class="col bordered-t">
<q-btn
flat
dense
color="info"
class="full-width"
@click="() => $router.push('/notification')"
>
{{ $t('noti.viewAll') }}
</q-btn>
</div>
</q-menu>
</q-btn>
@ -455,6 +493,8 @@ onMounted(async () => {
</template>
</DialogForm>
<NotiDialog v-model="state.notiDialog" v-model:id="state.notiId" />
<global-loading :visibility="visible" />
</q-layout>
</template>

View file

@ -0,0 +1,243 @@
<script lang="ts" setup>
// NOTE: Library
import { onMounted, reactive, ref } from 'vue';
// NOTE: Components
import NoData from 'src/components/NoData.vue';
import NotiDialog from './NotiDialog.vue';
// NOTE: Stores & Type
import { useNavigator } from 'src/stores/navigator';
import { storeToRefs } from 'pinia';
import { useNotification } from 'src/stores/notification';
import { dateFormatJS } from 'src/utils/datetime';
// NOTE: Variable
const navigatorStore = useNavigator();
const notificationStore = useNotification();
const { data: noti } = storeToRefs(notificationStore);
const state = reactive({
currentTab: 'all',
inputSearch: '',
notiDialog: false,
notiId: '',
});
const pageTabs = [
{ label: 'all', value: 'all' },
{ label: 'unread', value: 'unread' },
];
const selectedNoti = ref<string[]>([]);
function toggleSelection(id: string, checkAll: boolean = false) {
const index = selectedNoti.value.indexOf(id);
if (checkAll) {
if (selectedNoti.value.length === noti.value.length) {
selectedNoti.value = [];
} else {
selectedNoti.value = [...noti.value.map((v) => v.id)];
}
return;
}
if (index > -1) {
selectedNoti.value.splice(index, 1);
} else {
selectedNoti.value.push(id);
}
}
async function fetchNoti() {
const res = await notificationStore.getNotificationList();
if (res) {
noti.value = res.result;
}
}
async function markAsRead() {
const res = await notificationStore.markReadNotification(selectedNoti.value);
if (res) {
await fetchNoti();
selectedNoti.value = [];
}
}
async function deleteNoti() {
const res = await notificationStore.deleteMultiNotification(
selectedNoti.value,
);
if (res) {
await fetchNoti();
selectedNoti.value = [];
}
}
function readNoti(id: string) {
state.notiId = id;
state.notiDialog = true;
}
onMounted(async () => {
navigatorStore.current.title = 'noti.title';
navigatorStore.current.path = [{ text: 'noti.caption', i18n: true }];
await fetchNoti();
});
</script>
<template>
<main class="column full-height no-wrap">
<div class="surface-1 col bordered rounded column">
<div class="q-px-lg q-py-xs row items-center">
<q-checkbox
size="xs"
:model-value="selectedNoti.length === noti.length"
class="q-px-sm"
@click="toggleSelection('', true)"
/>
<q-btn
v-if="selectedNoti.length === 0"
icon="mdi-refresh"
rounded
flat
dense
size="xs"
class="app-text-muted-2 q-ml-sm q-mt-xs"
@click="async () => await fetchNoti()"
>
<q-tooltip>Refresh</q-tooltip>
</q-btn>
<q-btn
v-if="selectedNoti.length > 0"
icon="mdi-trash-can-outline"
rounded
flat
dense
size="xs"
class="app-text-muted-2 q-ml-sm q-mt-xs"
@click="async () => await deleteNoti()"
>
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
</q-btn>
<q-btn
v-if="selectedNoti.length > 0"
icon="mdi-email-open-outline"
rounded
flat
dense
size="xs"
class="app-text-muted-2 q-mx-sm q-mt-xs"
@click="async () => await markAsRead()"
>
<q-tooltip>{{ $t('noti.markAsRead') }}</q-tooltip>
</q-btn>
<span v-if="selectedNoti.length" class="text-caption app-text-muted-2">
{{
$t('general.selected', {
number: selectedNoti.length,
msg: $t('noti.title'),
})
}}
</span>
</div>
<q-tabs
inline-label
mobile-arrows
dense
v-model="state.currentTab"
align="left"
class="full-width bordered-b"
active-color="info"
>
<q-tab
v-for="tab in pageTabs"
:name="tab.value"
:key="tab.value"
@click="
() => {
state.currentTab = tab.value;
state.inputSearch = '';
}
"
>
<div
class="row text-capitalize"
:class="
state.currentTab === tab.value ? 'text-bold' : 'app-text-muted'
"
>
{{ $t(`noti.${tab.label}`) }}
<q-badge
rounded
class="q-ml-md"
style="background: hsl(var(--info-bg))"
>
{{
tab.value === 'all'
? noti.length
: noti.filter((v) => !v.read).length
}}
</q-badge>
</div>
</q-tab>
</q-tabs>
<section class="scroll full-height col">
<article
v-if="
state.currentTab === 'unread'
? noti.filter((v) => !v.read).length > 0
: noti.length > 0
"
class="q-pa-md q-gutter-sm"
>
<q-item
v-for="(n, i) in state.currentTab === 'unread'
? noti.filter((v) => !v.read)
: noti"
:key="i"
clickable
:class="{ 'surface-3': !n.read }"
class="q-py-sm q-px-md rounded row no-wrap items-center"
@click.stop="readNoti(n.id)"
>
<!-- <div
class="rounded"
:style="`background: hsl(var(--info-bg)/${n.read ? 0 : 1}); width: 6px; height: 6px`"
/> -->
<q-checkbox
:model-value="selectedNoti.includes(n.id)"
size="xs"
class="q-pr-lg"
@click.stop="toggleSelection(n.id)"
/>
<div class="column">
{{ n.title }}
<span class="text-caption app-text-muted">
{{ n.detail }}
</span>
</div>
<div class="q-ml-auto">
{{
dateFormatJS({
date: n.createdAt,
dayStyle: 'numeric',
monthStyle: '2-digit',
withTime: true,
})
}}
</div>
</q-item>
</article>
<div v-else class="full-height flex items-center justify-center">
<NoData />
</div>
</section>
</div>
</main>
<NotiDialog v-model="state.notiDialog" v-model:id="state.notiId" />
</template>
<style scoped></style>

View file

@ -0,0 +1,65 @@
<script lang="ts" setup>
import DialogFormContainer from 'src/components/dialog/DialogFormContainer.vue';
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
import { CancelButton } from 'src/components/button';
import { ref } from 'vue';
import { Notification, useNotification } from 'src/stores/notification';
import { dateFormatJS } from 'src/utils/datetime';
const notificationStore = useNotification();
const open = defineModel<boolean>({ default: false, required: true });
const notiId = defineModel<string>('id', { default: '', required: true });
const noti = ref<Notification>();
function close() {
open.value = false;
}
async function fetchNoti() {
const res = await notificationStore.getNotification(notiId.value);
if (res) {
noti.value = res;
}
}
</script>
<template>
<DialogFormContainer
width="60vw"
height="350px"
v-model="open"
v-on:open="fetchNoti"
>
<template #header>
<DialogHeader :title="$t('noti.title')" />
</template>
<section v-if="noti" class="q-pa-md col full-width">
<div class="surface-1 rounded bordered q-pa-md full-height full-width">
{{ noti.title }}
<div class="text-caption app-text-muted">
{{
dateFormatJS({
date: noti.createdAt,
monthStyle: 'long',
withTime: true,
})
}}
</div>
<q-separator spaced="md" />
<div class="text-caption">
{{ noti.detail }}
</div>
</div>
</section>
<template #footer>
<CancelButton
class="q-ml-auto"
:label="$t('general.close')"
outlined
@click="close"
/>
</template>
</DialogFormContainer>
</template>