177 lines
6.2 KiB
TypeScript
177 lines
6.2 KiB
TypeScript
import AdaptableCard from '@/components/shared/AdaptableCard'
|
||
import Container from '@/components/shared/Container'
|
||
import Drawer from '@/components/ui/Drawer'
|
||
import Button from '@/components/ui/Button'
|
||
import NotificationChannels from '@/constants/notification-channel.enum'
|
||
import { NotificationDto } from '@/proxy/notification/models'
|
||
import { getList, updateReadAll } from '@/services/notification.service'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import dayjs from 'dayjs'
|
||
import { groupBy } from '@/utils/collection'
|
||
import { useCallback, useEffect, useState } from 'react'
|
||
import Log from './components/Log'
|
||
import LogFilter from './components/LogFilter'
|
||
import PageTitle from '@/components/shared/PageTitle'
|
||
import { DIR_RTL } from '@/constants/theme.constant'
|
||
import { useStoreState } from '@/store'
|
||
|
||
const itemsPerPage = 10
|
||
type NotificationGroups = Record<string, NotificationDto[]>
|
||
|
||
const ActivityLog = () => {
|
||
const { translate } = useLocalization()
|
||
|
||
const [loading, setLoading] = useState(false)
|
||
const [notifications, setNotifications] = useState<NotificationGroups>({})
|
||
const [page, setPage] = useState(0)
|
||
const [hasMore, setHasMore] = useState(false)
|
||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false)
|
||
const [markingRead, setMarkingRead] = useState(false)
|
||
const direction = useStoreState((state) => state.theme.direction)
|
||
const [filter, setFilter] = useState<string[]>([
|
||
NotificationChannels.Desktop,
|
||
NotificationChannels.Mail,
|
||
NotificationChannels.Rocket,
|
||
NotificationChannels.Sms,
|
||
NotificationChannels.UiActivity,
|
||
NotificationChannels.UiToast,
|
||
NotificationChannels.WhatsApp,
|
||
NotificationChannels.Telegram,
|
||
])
|
||
|
||
const fetchData = useCallback(async (isLoadmore: boolean = false) => {
|
||
setLoading(true)
|
||
try {
|
||
const response = await getList({
|
||
maxResultCount: itemsPerPage,
|
||
skipCount: page * itemsPerPage,
|
||
sorting: 'CreationTime DESC',
|
||
channels: filter,
|
||
isListRequest: true,
|
||
})
|
||
const items = response.data?.items ?? []
|
||
|
||
const currentItemCount = page * itemsPerPage + items.length
|
||
setHasMore((response.data?.totalCount ?? 0) > currentItemCount)
|
||
const logs = groupBy(items, (a) => dayjs(a.creationTime).format('YYYY-MM-DD'))
|
||
|
||
if (isLoadmore) {
|
||
setNotifications((prev) => {
|
||
const next: NotificationGroups = { ...prev }
|
||
|
||
for (const [key, value] of Object.entries(logs)) {
|
||
// Sayfalama canli veri uzerinde calisir; araya yeni bildirim girdiginde ayni
|
||
// kayit ikinci sayfada tekrar gelebilir. Id bazli eleme olmadan liste kaydi iki
|
||
// kez cizer ve React ayni anahtari iki kez gorur.
|
||
const existingIds = new Set((next[key] ?? []).map((item) => item.id))
|
||
next[key] = [
|
||
...(next[key] ?? []),
|
||
...value.filter((item) => !existingIds.has(item.id)),
|
||
]
|
||
}
|
||
|
||
return next
|
||
})
|
||
} else {
|
||
setNotifications(logs)
|
||
}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [filter, page])
|
||
|
||
useEffect(() => {
|
||
fetchData(page > 0)
|
||
}, [fetchData, page])
|
||
|
||
/**
|
||
* Ekranda görülen kayıtlar okundu işaretlenmeden rozetteki sayı düşmüyordu. İşaretleme
|
||
* süzülen kanallarla sınırlıdır; sunucu iletilmemiş kayıtları zaten atlar.
|
||
*/
|
||
const handleMarkAllAsRead = async () => {
|
||
setMarkingRead(true)
|
||
try {
|
||
await Promise.all(filter.map((channel) => updateReadAll(channel, true)))
|
||
setPage(0)
|
||
await fetchData()
|
||
} finally {
|
||
setMarkingRead(false)
|
||
}
|
||
}
|
||
|
||
const handleFilterChange = (value: string[]) => {
|
||
setPage(0)
|
||
setNotifications({})
|
||
setHasMore(false)
|
||
setFilter(value)
|
||
setIsFilterDrawerOpen(false)
|
||
}
|
||
|
||
return (
|
||
<Container>
|
||
<PageTitle title={translate('::' + 'Abp.Identity.ActivityLogs')} />
|
||
|
||
<AdaptableCard className="overflow-hidden bg-white dark:bg-gray-800">
|
||
<div className="w-full">
|
||
<div className="mb-5 flex items-center justify-between gap-3">
|
||
<h3 className="text-xl font-semibold md:text-2xl text-gray-900 dark:text-white">
|
||
{translate('::Abp.Identity.ActivityLogs')}
|
||
</h3>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
size="sm"
|
||
variant="twoTone"
|
||
loading={markingRead}
|
||
className="dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600"
|
||
onClick={handleMarkAllAsRead}
|
||
>
|
||
{translate('::Abp.Identity.ActivityLogs.MarkAllRead')}
|
||
</Button>
|
||
<Button
|
||
className="lg:hidden dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600"
|
||
size="sm"
|
||
variant="twoTone"
|
||
onClick={() => setIsFilterDrawerOpen(true)}
|
||
>
|
||
{translate('::Abp.Identity.ActivityLogs.Filters')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_340px]">
|
||
<div className="min-w-0 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 lg:p-6">
|
||
<Log
|
||
notifications={notifications}
|
||
isLoading={loading}
|
||
loadable={hasMore}
|
||
onLoadMore={() => setPage((prev) => prev + 1)}
|
||
></Log>
|
||
</div>
|
||
|
||
<div className="hidden lg:block">
|
||
<LogFilter useAffix filter={filter} className="dark:bg-gray-800" onFilterChange={handleFilterChange} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Drawer
|
||
title={translate('::Abp.Identity.ActivityLogs.Filters')}
|
||
isOpen={isFilterDrawerOpen}
|
||
width={340}
|
||
placement={direction === DIR_RTL ? 'right' : 'left'}
|
||
onClose={() => setIsFilterDrawerOpen(false)}
|
||
onRequestClose={() => setIsFilterDrawerOpen(false)}
|
||
>
|
||
<LogFilter
|
||
filter={filter}
|
||
useAffix={false}
|
||
className="border-none p-0 dark:bg-gray-800"
|
||
onFilterChange={handleFilterChange}
|
||
/>
|
||
</Drawer>
|
||
</AdaptableCard>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
export default ActivityLog
|