Merge pull request #188 from Frappet/feat/notification
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
feat: notification
This commit is contained in:
commit
cd231513ee
8 changed files with 457 additions and 61 deletions
|
|
@ -241,6 +241,16 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noti: {
|
||||||
|
title: 'Notification',
|
||||||
|
caption: 'All Notification',
|
||||||
|
unread: 'Unread',
|
||||||
|
all: 'All',
|
||||||
|
read: 'Read',
|
||||||
|
viewALL: 'View All',
|
||||||
|
markAsRead: 'Mark as Read',
|
||||||
|
},
|
||||||
|
|
||||||
form: {
|
form: {
|
||||||
tm6: {
|
tm6: {
|
||||||
transportation: 'Flight/Vehicle',
|
transportation: 'Flight/Vehicle',
|
||||||
|
|
@ -1141,6 +1151,7 @@ export default {
|
||||||
taskListNotFound: 'Task list cannot be found.',
|
taskListNotFound: 'Task list cannot be found.',
|
||||||
creditNoteNotFound: 'Credit note cannot be found.',
|
creditNoteNotFound: 'Credit note cannot be found.',
|
||||||
debitNoteNotFound: 'Debit note cannot be found.',
|
debitNoteNotFound: 'Debit note cannot be found.',
|
||||||
|
notificationNotFound: 'Notification cannot be found.',
|
||||||
|
|
||||||
productGroupIsUsed: 'Product group is in used.',
|
productGroupIsUsed: 'Product group is in used.',
|
||||||
productIsUsed: 'Product is in used.',
|
productIsUsed: 'Product is in used.',
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,16 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noti: {
|
||||||
|
title: 'การแจ้งเตือน',
|
||||||
|
caption: 'การแจ้งเตือนทั้งหมด',
|
||||||
|
unread: 'ยังไม่ได้อ่าน',
|
||||||
|
all: 'ทั้งหมด',
|
||||||
|
read: 'อ่าน',
|
||||||
|
viewAll: 'ดูทั้งหมด',
|
||||||
|
markAsRead: 'ทำเครื่องหมายว่าอ่านแล้ว',
|
||||||
|
},
|
||||||
|
|
||||||
form: {
|
form: {
|
||||||
tm6: {
|
tm6: {
|
||||||
transportation: 'เที่ยวบิน/พาหนะ',
|
transportation: 'เที่ยวบิน/พาหนะ',
|
||||||
|
|
@ -1119,6 +1129,7 @@ export default {
|
||||||
taskListNotFound: 'ไม่พบใบสั่งงาน',
|
taskListNotFound: 'ไม่พบใบสั่งงาน',
|
||||||
creditNoteNotFound: 'ไม่พบใบลดหนี้',
|
creditNoteNotFound: 'ไม่พบใบลดหนี้',
|
||||||
debitNoteNotFound: 'ไม่พบใบเพิ่มหนี้',
|
debitNoteNotFound: 'ไม่พบใบเพิ่มหนี้',
|
||||||
|
notificationNotFound: 'ไม่พบการแจ้งเตือน',
|
||||||
|
|
||||||
productGroupIsUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
|
productGroupIsUsed: 'กลุ่มสินค้าและบริการที่ใช้งานอยู่',
|
||||||
productIsUsed: 'สินค้าและบริการใช้งานอยู่',
|
productIsUsed: 'สินค้าและบริการใช้งานอยู่',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed, reactive } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
|
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import useLoader from 'stores/loader';
|
import useLoader from 'stores/loader';
|
||||||
import ProfileMenu from './ProfileMenu.vue';
|
import ProfileMenu from './ProfileMenu.vue';
|
||||||
|
|
@ -17,6 +18,8 @@ import { useConfigStore } from 'src/stores/config';
|
||||||
import { useNavigator } from 'src/stores/navigator';
|
import { useNavigator } from 'src/stores/navigator';
|
||||||
import { initLang, initTheme, Lang, setLang } from 'src/utils/ui';
|
import { initLang, initTheme, Lang, setLang } from 'src/utils/ui';
|
||||||
import { baseUrl } from 'stores/utils';
|
import { baseUrl } from 'stores/utils';
|
||||||
|
import { useNotification } from 'src/stores/notification';
|
||||||
|
import NotiDialog from 'src/pages/00_notification/NotiDialog.vue';
|
||||||
|
|
||||||
const useMyBranch = useMyBranchStore();
|
const useMyBranch = useMyBranchStore();
|
||||||
const { fetchListMyBranch } = useMyBranch;
|
const { fetchListMyBranch } = useMyBranch;
|
||||||
|
|
@ -27,18 +30,14 @@ interface NotificationButton {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
read: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const loaderStore = useLoader();
|
const loaderStore = useLoader();
|
||||||
const navigatorStore = useNavigator();
|
const navigatorStore = useNavigator();
|
||||||
|
const notificationStore = useNotification();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
const { data: notificationData } = storeToRefs(notificationStore);
|
||||||
|
|
||||||
const { visible } = storeToRefs(loaderStore);
|
const { visible } = storeToRefs(loaderStore);
|
||||||
const { t } = useI18n({ useScope: 'global' });
|
const { t } = useI18n({ useScope: 'global' });
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
@ -48,8 +47,9 @@ const canvasModal = ref(false);
|
||||||
const leftDrawerOpen = ref<boolean>(false);
|
const leftDrawerOpen = ref<boolean>(false);
|
||||||
const leftDrawerMini = ref(false);
|
const leftDrawerMini = ref(false);
|
||||||
|
|
||||||
const filterUnread = ref(false);
|
const unread = computed<number>(
|
||||||
const unread = ref<number>(1);
|
() => notificationData.value.filter((v) => !v.read).length || 0,
|
||||||
|
);
|
||||||
// const filterRole = ref<string[]>();
|
// const filterRole = ref<string[]>();
|
||||||
const userImage = ref<string>();
|
const userImage = ref<string>();
|
||||||
const userGender = ref('');
|
const userGender = ref('');
|
||||||
|
|
@ -65,33 +65,24 @@ const language: {
|
||||||
{ value: Lang.English, label: 'English', icon: 'us', date: 'en-gb' },
|
{ 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[]>([
|
const notiMenu = ref<NotificationButton[]>([
|
||||||
{
|
{
|
||||||
item: 'ทั้งหมด',
|
item: 'all',
|
||||||
color: 'noti-switch-on',
|
color: 'noti-switch-on',
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
item: 'ยังไม่ได้อ่าน',
|
item: 'unread',
|
||||||
color: 'noti-switch-off',
|
color: 'noti-switch-off',
|
||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const notification = ref<Notification[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Unread',
|
|
||||||
content: 'Unread',
|
|
||||||
read: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'Read',
|
|
||||||
content: 'Already read',
|
|
||||||
read: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
function setActive(button: NotificationButton) {
|
function setActive(button: NotificationButton) {
|
||||||
notiMenu.value = notiMenu.value.map((current) => ({
|
notiMenu.value = notiMenu.value.map((current) => ({
|
||||||
|
|
@ -99,12 +90,12 @@ function setActive(button: NotificationButton) {
|
||||||
color: current.item !== button.item ? 'noti-switch-off' : 'noti-switch-on',
|
color: current.item !== button.item ? 'noti-switch-off' : 'noti-switch-on',
|
||||||
active: current.item === button.item,
|
active: current.item === button.item,
|
||||||
}));
|
}));
|
||||||
if (button.item === 'ยังไม่ได้อ่าน') {
|
if (button.item === 'unread') {
|
||||||
// noti.value?.result &&
|
// noti.value?.result &&
|
||||||
filterUnread.value = true;
|
state.filterUnread = true;
|
||||||
}
|
}
|
||||||
if (button.item === 'ทั้งหมด') {
|
if (button.item === 'all') {
|
||||||
filterUnread.value = false;
|
state.filterUnread = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,12 +114,25 @@ function doLogout() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readNoti(id: string) {
|
||||||
|
state.notiDialog = true;
|
||||||
|
state.notiId = id;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initTheme();
|
initTheme();
|
||||||
initLang();
|
initLang();
|
||||||
|
|
||||||
await configStore.getConfig();
|
await configStore.getConfig();
|
||||||
|
|
||||||
|
{
|
||||||
|
const noti = await notificationStore.getNotificationList();
|
||||||
|
|
||||||
|
if (noti) {
|
||||||
|
notificationData.value = noti.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fetchListMyBranch(getUserId() ?? '');
|
await fetchListMyBranch(getUserId() ?? '');
|
||||||
leftDrawerOpen.value = $q.screen.gt.xs ? true : false;
|
leftDrawerOpen.value = $q.screen.gt.xs ? true : false;
|
||||||
|
|
||||||
|
|
@ -278,7 +282,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
<div class="row q-gutter-x-md items-center" style="margin-left: auto">
|
<div class="row q-gutter-x-md items-center" style="margin-left: auto">
|
||||||
<!-- notification -->
|
<!-- notification -->
|
||||||
<!-- <q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
|
|
@ -286,7 +290,7 @@ onMounted(async () => {
|
||||||
:size="$q.screen.lt.sm ? 'sm' : ''"
|
:size="$q.screen.lt.sm ? 'sm' : ''"
|
||||||
:class="{ bordered: $q.dark.isActive, dark: $q.dark.isActive }"
|
:class="{ bordered: $q.dark.isActive, dark: $q.dark.isActive }"
|
||||||
style="color: var(--surface-1)"
|
style="color: var(--surface-1)"
|
||||||
@click="notiOpen = !notiOpen"
|
@click="state.notiOpen = !state.notiOpen"
|
||||||
>
|
>
|
||||||
<q-icon name="mdi-bell" />
|
<q-icon name="mdi-bell" />
|
||||||
<q-badge v-if="unread !== 0" rounded floating color="negative">
|
<q-badge v-if="unread !== 0" rounded floating color="negative">
|
||||||
|
|
@ -297,10 +301,16 @@ onMounted(async () => {
|
||||||
:offset="[0, 10]"
|
:offset="[0, 10]"
|
||||||
anchor="bottom middle"
|
anchor="bottom middle"
|
||||||
self="top 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="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 />
|
<q-space />
|
||||||
</div>
|
</div>
|
||||||
<div class="q-px-md q-pb-md q-gutter-x-md">
|
<div class="q-px-md q-pb-md q-gutter-x-md">
|
||||||
|
|
@ -312,22 +322,30 @@ onMounted(async () => {
|
||||||
:flat="!btn.active"
|
:flat="!btn.active"
|
||||||
:unelevated="btn.active"
|
:unelevated="btn.active"
|
||||||
:key="index"
|
:key="index"
|
||||||
:label="btn.item"
|
:label="$t('noti.' + btn.item)"
|
||||||
:class="btn.color"
|
:class="btn.color"
|
||||||
@click="setActive(btn)"
|
@click="setActive(btn)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-infinite-scroll :offset="250">
|
<div style="max-height: 30vh; width: 400px; overflow-y: auto">
|
||||||
<div class="caption cursor-pointer">
|
<section
|
||||||
|
v-if="
|
||||||
|
state.filterUnread
|
||||||
|
? notificationData.filter((v) => !v.read).length
|
||||||
|
: notificationData.length
|
||||||
|
"
|
||||||
|
class="caption cursor-pointer"
|
||||||
|
>
|
||||||
<q-item
|
<q-item
|
||||||
|
v-for="(item, i) in state.filterUnread
|
||||||
|
? notificationData.filter((v) => !v.read)
|
||||||
|
: notificationData"
|
||||||
dense
|
dense
|
||||||
clickable
|
clickable
|
||||||
class="q-py-sm"
|
class="q-py-sm"
|
||||||
v-ripple
|
v-ripple
|
||||||
v-for="item in !filterUnread
|
@click="readNoti(item.id)"
|
||||||
? notification
|
:key="i"
|
||||||
: notification.filter((v) => !v.read)"
|
|
||||||
:key="item.id"
|
|
||||||
>
|
>
|
||||||
<q-avatar
|
<q-avatar
|
||||||
color="positive"
|
color="positive"
|
||||||
|
|
@ -343,12 +361,11 @@ onMounted(async () => {
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</span>
|
</span>
|
||||||
<span class="block ellipsis full-width text-stone">
|
<span class="block ellipsis full-width text-stone">
|
||||||
{{ item.content }}
|
{{ item.detail }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span align="right" class="col text-caption text-stone">
|
<span align="right" class="col text-caption text-stone">
|
||||||
{{ moment(item.createdAt).fromNow() }}
|
{{ moment(item.createdAt).fromNow() }}
|
||||||
5 s
|
|
||||||
</span>
|
</span>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
anchor="top middle"
|
anchor="top middle"
|
||||||
|
|
@ -356,21 +373,29 @@ onMounted(async () => {
|
||||||
:delay="1000"
|
:delay="1000"
|
||||||
:offset="[10, 10]"
|
:offset="[10, 10]"
|
||||||
>
|
>
|
||||||
{{ item.content }}
|
{{ item.detail }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</section>
|
||||||
<template v-slot:loading>
|
<section v-else class="text-center q-py-sm">
|
||||||
<div
|
<span class="app-text-muted">
|
||||||
class="text-center q-my-md"
|
{{ $t('general.noData') }}
|
||||||
v-if="noti && noti?.result.length < noti?.total"
|
</span>
|
||||||
>
|
</section>
|
||||||
<q-spinner-dots color="primary" size="40px" />
|
</div>
|
||||||
</div>
|
<div class="col bordered-t">
|
||||||
</template>
|
<q-btn
|
||||||
</q-infinite-scroll>
|
flat
|
||||||
|
dense
|
||||||
|
color="info"
|
||||||
|
class="full-width"
|
||||||
|
@click="() => $router.push('/notification')"
|
||||||
|
>
|
||||||
|
{{ $t('noti.viewAll') }}
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn> -->
|
</q-btn>
|
||||||
|
|
||||||
<!-- เปลี่นนภาษา -->
|
<!-- เปลี่นนภาษา -->
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -468,6 +493,8 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
</DialogForm>
|
</DialogForm>
|
||||||
|
|
||||||
|
<NotiDialog v-model="state.notiDialog" v-model:id="state.notiId" />
|
||||||
|
|
||||||
<global-loading :visibility="visible" />
|
<global-loading :visibility="visible" />
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
243
src/pages/00_notification/MainPage.vue
Normal file
243
src/pages/00_notification/MainPage.vue
Normal 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"
|
||||||
|
@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"
|
||||||
|
@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>
|
||||||
69
src/pages/00_notification/NotiDialog.vue
Normal file
69
src/pages/00_notification/NotiDialog.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<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">
|
||||||
|
<article
|
||||||
|
class="surface-1 rounded bordered q-pa-md full-height full-width"
|
||||||
|
>
|
||||||
|
<div class="text-bold">
|
||||||
|
{{ noti.title }}
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<CancelButton
|
||||||
|
class="q-ml-auto"
|
||||||
|
:label="$t('general.close')"
|
||||||
|
outlined
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DialogFormContainer>
|
||||||
|
</template>
|
||||||
|
|
@ -135,6 +135,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'dashBoard',
|
name: 'dashBoard',
|
||||||
component: () => import('pages/15_dash-board/MainPage.vue'),
|
component: () => import('pages/15_dash-board/MainPage.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/notification',
|
||||||
|
name: 'Notification',
|
||||||
|
component: () => import('pages/00_notification/MainPage.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -208,7 +213,6 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'DebitNoteView',
|
name: 'DebitNoteView',
|
||||||
component: () => import('pages/12_debit-note/FormPage.vue'),
|
component: () => import('pages/12_debit-note/FormPage.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/debit-note/document-view',
|
path: '/debit-note/document-view',
|
||||||
name: 'DebitNoteDocumentView',
|
name: 'DebitNoteDocumentView',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@ import { api } from 'src/boot/axios';
|
||||||
import { PaginationResult } from 'src/types';
|
import { PaginationResult } from 'src/types';
|
||||||
import { createDataRefBase } from '../utils';
|
import { createDataRefBase } from '../utils';
|
||||||
|
|
||||||
export type Notification = {};
|
export type Notification = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
read?: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const useNotification = defineStore('noti-store', () => {
|
export const useNotification = defineStore('noti-store', () => {
|
||||||
const state = createDataRefBase<Notification>();
|
const state = createDataRefBase<Notification>();
|
||||||
|
|
@ -28,9 +34,12 @@ export const useNotification = defineStore('noti-store', () => {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNotification(paymentId: string, payload: Notification) {
|
async function updateNotification(
|
||||||
|
notificationId: string,
|
||||||
|
payload: Notification,
|
||||||
|
) {
|
||||||
const res = await api.put<Notification & { id: string }>(
|
const res = await api.put<Notification & { id: string }>(
|
||||||
`/notification/${paymentId}`,
|
`/notification/${notificationId}`,
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
if (res.status >= 400) return null;
|
if (res.status >= 400) return null;
|
||||||
|
|
@ -45,6 +54,26 @@ export const useNotification = defineStore('noti-store', () => {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMultiNotification(notificationId: string[]) {
|
||||||
|
const res = await api.delete<Notification & { id: string }>(
|
||||||
|
'/notification',
|
||||||
|
{
|
||||||
|
data: notificationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.status >= 400) return null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markReadNotification(notificationId: string[]) {
|
||||||
|
const res = await api.post<Notification & { id: string }>(
|
||||||
|
'/notification/mark-read',
|
||||||
|
{ id: notificationId },
|
||||||
|
);
|
||||||
|
if (res.status >= 400) return null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
||||||
|
|
@ -52,5 +81,7 @@ export const useNotification = defineStore('noti-store', () => {
|
||||||
getNotificationList,
|
getNotificationList,
|
||||||
updateNotification,
|
updateNotification,
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
|
deleteMultiNotification,
|
||||||
|
markReadNotification,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -532,7 +532,7 @@ export function createDataRefBase<T>(
|
||||||
defaultPageSize = 30,
|
defaultPageSize = 30,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
data: ref<T[]>(),
|
data: ref<T[]>([]),
|
||||||
page: ref<number>(defaultPage),
|
page: ref<number>(defaultPage),
|
||||||
pageMax: ref<number>(defaultPageMax),
|
pageMax: ref<number>(defaultPageMax),
|
||||||
pageSize: ref<number>(defaultPageSize),
|
pageSize: ref<number>(defaultPageSize),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue