sozsoft-platform/ui/src/views/admin/activityLog/ActivityLog.tsx
2026-09-20 23:23:39 +03:00

177 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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