Notification Setting düzenlemesi
This commit is contained in:
parent
368756f0ac
commit
c79cee0df7
7 changed files with 307 additions and 166 deletions
|
|
@ -1929,20 +1929,20 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.NotificationChannel.Sms",
|
"key": "App.NotificationChannel.Sms",
|
||||||
"en": "SMS",
|
"en": "Sms",
|
||||||
"tr": "SMS"
|
"tr": "Sms"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.NotificationChannel.Mail",
|
"key": "App.NotificationChannel.Mail",
|
||||||
"en": "E-mail",
|
"en": "Mail",
|
||||||
"tr": "E-posta"
|
"tr": "E-posta"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.NotificationChannel.Rocket",
|
"key": "App.NotificationChannel.Rocket",
|
||||||
"en": "Rocket.Chat",
|
"en": "Rocket",
|
||||||
"tr": "Rocket.Chat"
|
"tr": "Rocket"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -1953,14 +1953,14 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.NotificationChannel.UiActivity",
|
"key": "App.NotificationChannel.UiActivity",
|
||||||
"en": "Activity",
|
"en": "UI Activity",
|
||||||
"tr": "Etkinlik"
|
"tr": "Arayüz Etkinliği"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.NotificationChannel.UiToast",
|
"key": "App.NotificationChannel.UiToast",
|
||||||
"en": "In-app",
|
"en": "UI Toast",
|
||||||
"tr": "Uygulama içi"
|
"tr": "Arayüz Bildirimi"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -1980,6 +1980,30 @@
|
||||||
"en": "Reset to default",
|
"en": "Reset to default",
|
||||||
"tr": "Varsayılana döndür"
|
"tr": "Varsayılana döndür"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "Abp.Identity.NotificationSettings.Description",
|
||||||
|
"en": "Choose how you want to be notified for each notification type.",
|
||||||
|
"tr": "Her bildirim türü için nasıl haberdar olmak istediğinizi seçin."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "Abp.Identity.NotificationSettings.Type",
|
||||||
|
"en": "Notification type",
|
||||||
|
"tr": "Bildirim türü"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "Abp.Identity.NotificationSettings.Customized",
|
||||||
|
"en": "Customized by you",
|
||||||
|
"tr": "Sizin tarafınızdan değiştirildi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "Abp.Identity.NotificationSettings.Fixed",
|
||||||
|
"en": "Fixed by the administrator",
|
||||||
|
"tr": "Yönetici tarafından sabitlenmiş"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "Abp.Identity.NotificationSettings.NoRule",
|
"key": "Abp.Identity.NotificationSettings.NoRule",
|
||||||
|
|
@ -19578,12 +19602,6 @@
|
||||||
"en": "DELETE",
|
"en": "DELETE",
|
||||||
"tr": "DELETE"
|
"tr": "DELETE"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.StaticLookup.Desktop",
|
|
||||||
"en": "Desktop",
|
|
||||||
"tr": "Masaüstü"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.StaticLookup.Double",
|
"key": "App.StaticLookup.Double",
|
||||||
|
|
@ -19674,12 +19692,6 @@
|
||||||
"en": "List",
|
"en": "List",
|
||||||
"tr": "Liste"
|
"tr": "Liste"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.StaticLookup.Mail",
|
|
||||||
"en": "Mail",
|
|
||||||
"tr": "E-posta"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.StaticLookup.MailQueueWorker",
|
"key": "App.StaticLookup.MailQueueWorker",
|
||||||
|
|
@ -19914,12 +19926,6 @@
|
||||||
"en": "Reference",
|
"en": "Reference",
|
||||||
"tr": "Referans"
|
"tr": "Referans"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.StaticLookup.Rocket",
|
|
||||||
"en": "Rocket",
|
|
||||||
"tr": "Rocket"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.StaticLookup.Role",
|
"key": "App.StaticLookup.Role",
|
||||||
|
|
@ -20028,12 +20034,6 @@
|
||||||
"en": "Time",
|
"en": "Time",
|
||||||
"tr": "Saat"
|
"tr": "Saat"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.StaticLookup.UiActivity",
|
|
||||||
"en": "UI Activity",
|
|
||||||
"tr": "Arayüz Etkinliği"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.StaticLookup.UInt16",
|
"key": "App.StaticLookup.UInt16",
|
||||||
|
|
@ -20052,12 +20052,6 @@
|
||||||
"en": "UInt64",
|
"en": "UInt64",
|
||||||
"tr": "UInt64"
|
"tr": "UInt64"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.StaticLookup.UiToast",
|
|
||||||
"en": "UI Toast",
|
|
||||||
"tr": "Arayüz Bildirimi"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.User",
|
"key": "App.Platform.User",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
SeedSyncInsert = true,
|
SeedSyncInsert = true,
|
||||||
SeedSyncUpdate = true,
|
SeedSyncUpdate = true,
|
||||||
SeedSyncDelete = true,
|
SeedSyncDelete = true,
|
||||||
SortMode = GridOptions.SortModeSingle,
|
SortMode = GridOptions.SortModeMultiple,
|
||||||
FilterRowJson = DefaultFilterRowJson(),
|
FilterRowJson = DefaultFilterRowJson(),
|
||||||
HeaderFilterJson = DefaultHeaderFilterJson(),
|
HeaderFilterJson = DefaultHeaderFilterJson(),
|
||||||
SearchPanelJson = DefaultSearchPanelJson(),
|
SearchPanelJson = DefaultSearchPanelJson(),
|
||||||
|
|
@ -454,6 +454,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
Visible = true,
|
Visible = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
|
||||||
|
SortIndex = 2,
|
||||||
|
SortDirection = GridColumnOptions.SortOrderAsc,
|
||||||
LookupJson = JsonSerializer.Serialize(new LookupDto
|
LookupJson = JsonSerializer.Serialize(new LookupDto
|
||||||
{
|
{
|
||||||
DataSourceType = UiLookupDataSourceTypeEnum.Query,
|
DataSourceType = UiLookupDataSourceTypeEnum.Query,
|
||||||
|
|
@ -7238,8 +7240,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
}),
|
}),
|
||||||
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
|
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
|
||||||
new CommandColumnDto() {
|
new CommandColumnDto() {
|
||||||
Hint = "Create",
|
Hint = "App.Platform.Create",
|
||||||
Text = "Create",
|
Text = "App.Platform.Create",
|
||||||
AuthName = listFormName,
|
AuthName = listFormName,
|
||||||
DialogName = "CreateNotification",
|
DialogName = "CreateNotification",
|
||||||
DialogParameters = JsonSerializer.Serialize(new { name = "id", id = "@Id" }),
|
DialogParameters = JsonSerializer.Serialize(new { name = "id", id = "@Id" }),
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,14 @@ public static class ListFormSeeder_DefaultJsons
|
||||||
ValueExpr = "key",
|
ValueExpr = "key",
|
||||||
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[]
|
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[]
|
||||||
{
|
{
|
||||||
new() { Key = "Sms", Name = "App.Sender.Sms" },
|
new() { Key = "Sms", Name = "App.NotificationChannel.Sms" },
|
||||||
new() { Key = "Mail", Name = "App.StaticLookup.Mail" },
|
new() { Key = "Mail", Name = "App.NotificationChannel.Mail" },
|
||||||
new() { Key = "Rocket", Name = "App.StaticLookup.Rocket" },
|
new() { Key = "Rocket", Name = "App.NotificationChannel.Rocket" },
|
||||||
new() { Key = "Desktop", Name = "App.StaticLookup.Desktop" },
|
new() { Key = "Desktop", Name = "App.NotificationChannel.Desktop" },
|
||||||
new() { Key = "UiActivity", Name = "App.StaticLookup.UiActivity" },
|
new() { Key = "UiActivity", Name = "App.NotificationChannel.UiActivity" },
|
||||||
new() { Key = "UiToast", Name = "App.StaticLookup.UiToast" },
|
new() { Key = "UiToast", Name = "App.NotificationChannel.UiToast" },
|
||||||
new() { Key = "WhatsApp", Name = "App.Sender.WhatsApp" },
|
new() { Key = "WhatsApp", Name = "App.NotificationChannel.WhatsApp" },
|
||||||
new() { Key = "Telegram", Name = "App.Sender.Telegram" },
|
new() { Key = "Telegram", Name = "App.NotificationChannel.Telegram" },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
|
import type { IconType } from 'react-icons'
|
||||||
|
import {
|
||||||
|
FaBell,
|
||||||
|
FaDesktop,
|
||||||
|
FaEnvelope,
|
||||||
|
FaListUl,
|
||||||
|
FaRocketchat,
|
||||||
|
FaSms,
|
||||||
|
FaTelegramPlane,
|
||||||
|
FaWhatsapp,
|
||||||
|
} from 'react-icons/fa'
|
||||||
|
|
||||||
const NotificationChannels = {
|
const NotificationChannels = {
|
||||||
Sms: 'Sms',
|
Sms: 'Sms',
|
||||||
Mail: 'Mail',
|
Mail: 'Mail',
|
||||||
|
|
@ -20,4 +32,16 @@ export const NotificationChannelColors: Record<string, string> = {
|
||||||
Telegram: 'bg-purple-900',
|
Telegram: 'bg-purple-900',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Kanal kayıt defterinin görsel yüzü; yeni kanal eklendiğinde ikon da burada tanımlanır. */
|
||||||
|
export const NotificationChannelIcons: Record<string, IconType> = {
|
||||||
|
Sms: FaSms,
|
||||||
|
Mail: FaEnvelope,
|
||||||
|
Rocket: FaRocketchat,
|
||||||
|
Desktop: FaDesktop,
|
||||||
|
UiActivity: FaListUl,
|
||||||
|
UiToast: FaBell,
|
||||||
|
WhatsApp: FaWhatsapp,
|
||||||
|
Telegram: FaTelegramPlane,
|
||||||
|
}
|
||||||
|
|
||||||
export default NotificationChannels
|
export default NotificationChannels
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,5 @@ export const postMyNotificationRule = (data: {
|
||||||
export const deleteMyNotificationRules = (notificationTypeId: string) =>
|
export const deleteMyNotificationRules = (notificationTypeId: string) =>
|
||||||
apiService.fetchData<NotificationRuleDto[]>({
|
apiService.fetchData<NotificationRuleDto[]>({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: `/api/app/notification-rule/my-notification-rules`,
|
url: `/api/app/notification-rule/my-notification-rules/${notificationTypeId}`,
|
||||||
params: { notificationTypeId },
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,8 @@ import classNames from 'classnames'
|
||||||
|
|
||||||
type CategoryTitleProps = CommonProps
|
type CategoryTitleProps = CommonProps
|
||||||
|
|
||||||
const ticketCheckboxes = [
|
/** Kanal listesi kayit defterinden gelir; etiket her ekranda ayni dil anahtarindan okunur. */
|
||||||
{ label: NotificationChannels.Sms, value: NotificationChannels.Sms },
|
const channels = Object.values(NotificationChannels)
|
||||||
{ label: NotificationChannels.Mail, value: NotificationChannels.Mail },
|
|
||||||
{ label: NotificationChannels.Rocket, value: NotificationChannels.Rocket },
|
|
||||||
{ label: NotificationChannels.Desktop, value: NotificationChannels.Desktop },
|
|
||||||
{ label: NotificationChannels.UiActivity, value: NotificationChannels.UiActivity },
|
|
||||||
{ label: NotificationChannels.UiToast, value: NotificationChannels.UiToast },
|
|
||||||
{ label: NotificationChannels.WhatsApp, value: NotificationChannels.WhatsApp },
|
|
||||||
{ label: NotificationChannels.Telegram, value: NotificationChannels.Telegram },
|
|
||||||
]
|
|
||||||
|
|
||||||
const CategoryTitle = ({ children, className }: CategoryTitleProps) => {
|
const CategoryTitle = ({ children, className }: CategoryTitleProps) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -64,9 +56,9 @@ const LogFilter = ({
|
||||||
<CategoryTitle className="mb-3 text-gray-500 dark:text-gray-400">
|
<CategoryTitle className="mb-3 text-gray-500 dark:text-gray-400">
|
||||||
{translate('::Abp.Identity.ActivityLogs.Channels')}
|
{translate('::Abp.Identity.ActivityLogs.Channels')}
|
||||||
</CategoryTitle>
|
</CategoryTitle>
|
||||||
{ticketCheckboxes.map((checkbox) => (
|
{channels.map((channel) => (
|
||||||
<Checkbox key={checkbox.value} className="mb-3 dark:text-white" value={checkbox.value}>
|
<Checkbox key={channel} className="mb-3 dark:text-white" value={channel}>
|
||||||
{checkbox.label}
|
{translate(`::App.NotificationChannel.${channel}`)}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
))}
|
))}
|
||||||
</Checkbox.Group>
|
</Checkbox.Group>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { Button, Checkbox, Spinner, Table } from '@/components/ui'
|
import { Button, Spinner, Switcher, Tooltip } from '@/components/ui'
|
||||||
import NotificationChannels from '@/constants/notification-channel.enum'
|
import NotificationChannels, {
|
||||||
|
NotificationChannelColors,
|
||||||
|
NotificationChannelIcons,
|
||||||
|
} from '@/constants/notification-channel.enum'
|
||||||
import { NotificationRuleDto, NotificationTypeDto } from '@/proxy/notification/models'
|
import { NotificationRuleDto, NotificationTypeDto } from '@/proxy/notification/models'
|
||||||
import {
|
import {
|
||||||
deleteMyNotificationRules,
|
deleteMyNotificationRules,
|
||||||
|
|
@ -8,49 +11,64 @@ import {
|
||||||
postMyNotificationRule,
|
postMyNotificationRule,
|
||||||
} from '@/services/notification-rule.service'
|
} from '@/services/notification-rule.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import classNames from 'classnames'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { FaHistory } from 'react-icons/fa'
|
import type { MouseEvent } from 'react'
|
||||||
|
import { FaHistory, FaLock } from 'react-icons/fa'
|
||||||
const { Tr, Th, Td, THead, TBody } = Table
|
|
||||||
|
|
||||||
/** Matris sütunları kanal kayıt defterinden gelir; yeni bir kanal eklendiğinde ekran kendiliğinden güncel olur. */
|
/** Matris sütunları kanal kayıt defterinden gelir; yeni bir kanal eklendiğinde ekran kendiliğinden güncel olur. */
|
||||||
const channels = Object.values(NotificationChannels)
|
const channels = Object.values(NotificationChannels)
|
||||||
|
|
||||||
|
const ruleKey = (notificationTypeId: string, channel: string) => `${notificationTypeId}|${channel}`
|
||||||
|
|
||||||
|
/** Sunucuda karşılığı olmayan tür/kanal kesişimi kapalı sayılır; matris boşluk bırakmaz. */
|
||||||
|
const emptyRule = (notificationTypeId: string, channel: string): NotificationRuleDto => ({
|
||||||
|
id: '',
|
||||||
|
notificationTypeId,
|
||||||
|
channel,
|
||||||
|
isActive: false,
|
||||||
|
isFixed: false,
|
||||||
|
isCustomized: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ChannelIcon = ({ channel, size = 'md' }: { channel: string; size?: 'sm' | 'md' }) => {
|
||||||
|
const Icon = NotificationChannelIcons[channel]
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center justify-center rounded-full text-white',
|
||||||
|
size === 'sm' ? 'h-6 w-6 text-xs' : 'h-7 w-7 text-sm',
|
||||||
|
NotificationChannelColors[channel] ?? 'bg-gray-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Icon && <Icon />}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const NotificationSettings = () => {
|
const NotificationSettings = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [rules, setRules] = useState<Record<string, NotificationRuleDto>>({})
|
||||||
const [list, setList] = useState<NotificationRuleDto[][]>([])
|
|
||||||
const [notificationTypes, setNotificationTypes] = useState<NotificationTypeDto[]>([])
|
const [notificationTypes, setNotificationTypes] = useState<NotificationTypeDto[]>([])
|
||||||
|
// Yalnızca kaydedilen hücre/satır kilitlenir; matrisin tamamını kilitlemek tek tıkta ekranı donduruyordu.
|
||||||
|
const [pending, setPending] = useState<string[]>([])
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setLoading(true)
|
|
||||||
try {
|
try {
|
||||||
const [typesResponse, rulesResponse] = await Promise.all([
|
const [typesResponse, rulesResponse] = await Promise.all([
|
||||||
getMyNotificationTypes(),
|
getMyNotificationTypes(),
|
||||||
getMyNotificationRules(),
|
getMyNotificationRules(),
|
||||||
])
|
])
|
||||||
|
|
||||||
const types = typesResponse.data ?? []
|
setNotificationTypes(typesResponse.data ?? [])
|
||||||
const rules = rulesResponse.data ?? []
|
setRules(
|
||||||
|
Object.fromEntries(
|
||||||
setNotificationTypes(types)
|
(rulesResponse.data ?? []).map((rule) => [
|
||||||
setList(
|
ruleKey(rule.notificationTypeId, rule.channel),
|
||||||
types.map((type) =>
|
rule,
|
||||||
channels.map(
|
]),
|
||||||
(channel) =>
|
|
||||||
rules.find(
|
|
||||||
(rule) => rule.channel === channel && rule.notificationTypeId === type.id,
|
|
||||||
) ?? {
|
|
||||||
id: '',
|
|
||||||
notificationTypeId: type.id,
|
|
||||||
channel,
|
|
||||||
isActive: false,
|
|
||||||
isFixed: false,
|
|
||||||
isCustomized: false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -63,94 +81,206 @@ const NotificationSettings = () => {
|
||||||
fetchData()
|
fetchData()
|
||||||
}, [fetchData])
|
}, [fetchData])
|
||||||
|
|
||||||
const onClickCheckbox = async (checked: boolean, rule: NotificationRuleDto) => {
|
const getRule = (notificationTypeId: string, channel: string) =>
|
||||||
// Kayıt sürerken kutular kilitlenir; art arda tıklamada son dönen yanıt kazanır ve
|
rules[ruleKey(notificationTypeId, channel)] ?? emptyRule(notificationTypeId, channel)
|
||||||
// ekran sunucudaki gerçeğin tersine düşerdi.
|
|
||||||
setSaving(true)
|
const isPending = (key: string) => pending.includes(key)
|
||||||
|
|
||||||
|
const withPending = async (key: string, action: () => Promise<unknown>) => {
|
||||||
|
setPending((current) => [...current, key])
|
||||||
try {
|
try {
|
||||||
await postMyNotificationRule({
|
await action()
|
||||||
channel: rule.channel,
|
|
||||||
notificationTypeId: rule.notificationTypeId,
|
|
||||||
isActive: checked,
|
|
||||||
})
|
|
||||||
await fetchData()
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setPending((current) => current.filter((item) => item !== key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetMyNotifications = async (notificationTypeId: string) => {
|
const onToggle = (checked: boolean, rule: NotificationRuleDto) => {
|
||||||
setSaving(true)
|
const key = ruleKey(rule.notificationTypeId, rule.channel)
|
||||||
try {
|
// Anahtar hemen döner, sunucu yanıtından sonra gerçek durum geri yazılır; hata olursa eski değere döner.
|
||||||
|
setRules((current) => ({
|
||||||
|
...current,
|
||||||
|
[key]: { ...rule, isActive: checked, isCustomized: true },
|
||||||
|
}))
|
||||||
|
|
||||||
|
return withPending(key, async () => {
|
||||||
|
try {
|
||||||
|
await postMyNotificationRule({
|
||||||
|
channel: rule.channel,
|
||||||
|
notificationTypeId: rule.notificationTypeId,
|
||||||
|
isActive: checked,
|
||||||
|
})
|
||||||
|
await fetchData()
|
||||||
|
} catch {
|
||||||
|
setRules((current) => ({ ...current, [key]: rule }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetMyNotifications = (notificationTypeId: string) =>
|
||||||
|
withPending(notificationTypeId, async () => {
|
||||||
await deleteMyNotificationRules(notificationTypeId)
|
await deleteMyNotificationRules(notificationTypeId)
|
||||||
await fetchData()
|
await fetchData()
|
||||||
} finally {
|
})
|
||||||
setSaving(false)
|
|
||||||
|
const isCustomized = (notificationTypeId: string) =>
|
||||||
|
channels.some((channel) => getRule(notificationTypeId, channel).isCustomized)
|
||||||
|
|
||||||
|
const renderSwitcher = (notificationTypeId: string, channel: string) => {
|
||||||
|
const rule = getRule(notificationTypeId, channel)
|
||||||
|
const busy = isPending(ruleKey(notificationTypeId, channel)) || isPending(notificationTypeId)
|
||||||
|
|
||||||
|
if (rule.isFixed) {
|
||||||
|
return (
|
||||||
|
<Tooltip title={translate('::Abp.Identity.NotificationSettings.Fixed')}>
|
||||||
|
<span className={rule.isActive ? 'text-emerald-600' : 'text-gray-400'}>
|
||||||
|
<FaLock />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switcher
|
||||||
|
checked={rule.isActive}
|
||||||
|
isLoading={busy}
|
||||||
|
disabled={busy}
|
||||||
|
// Switcher denetimli kipte eski degeri geri veriyor; yeni deger kuraldan turetilir.
|
||||||
|
onChange={() => onToggle(!rule.isActive, rule)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderResetButton = (notificationTypeId: string) => (
|
||||||
|
<Tooltip title={translate('::Abp.Identity.NotificationSettings.Reset')}>
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
variant="plain"
|
||||||
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
disabled={!isCustomized(notificationTypeId) || isPending(notificationTypeId)}
|
||||||
|
icon={<FaHistory />}
|
||||||
|
onClick={(e: MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
resetMyNotifications(notificationTypeId)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderTypeTitle = (type: NotificationTypeDto) => (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold">{type.name}</span>
|
||||||
|
{isCustomized(type.id) && <span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" />}
|
||||||
|
</div>
|
||||||
|
{type.description && <p className="mt-0.5 text-xs text-gray-500">{type.description}</p>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-10">
|
||||||
|
<Spinner size={40} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notificationTypes.length) {
|
||||||
|
return (
|
||||||
|
<p className="py-10 text-center text-gray-500">
|
||||||
|
{translate('::Abp.Identity.NotificationSettings.NoRule')}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-4">
|
||||||
{loading ? (
|
<div className="flex flex-wrap items-end justify-between gap-2">
|
||||||
<div className="flex items-center justify-center">
|
<p className="text-sm text-gray-500">
|
||||||
<Spinner size={40} />
|
{translate('::Abp.Identity.NotificationSettings.Description')}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-amber-500" />
|
||||||
|
{translate('::Abp.Identity.NotificationSettings.Customized')}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<FaLock />
|
||||||
|
{translate('::Abp.Identity.NotificationSettings.Fixed')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<>
|
|
||||||
{list.length ? (
|
{/* Geniş ekranda matris, dar ekranda kart listesi: sekiz kanal dar ekranda tek satıra sığmıyor. */}
|
||||||
<Table compact={true}>
|
<div className="hidden overflow-x-auto rounded-xl border border-gray-200 md:block dark:border-gray-700">
|
||||||
<THead>
|
<table className="w-full text-sm">
|
||||||
<Tr>
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
<Th></Th>
|
<tr>
|
||||||
{channels.map((channel) => (
|
<th className="sticky left-0 z-10 bg-gray-50 px-4 py-3 text-left font-semibold dark:bg-gray-700">
|
||||||
<Th key={channel} className="text-center">
|
{translate('::Abp.Identity.NotificationSettings.Type')}
|
||||||
|
</th>
|
||||||
|
{channels.map((channel) => (
|
||||||
|
<th key={channel} className="px-3 py-3 font-semibold">
|
||||||
|
<span className="flex flex-col items-center gap-1.5">
|
||||||
|
<ChannelIcon channel={channel} />
|
||||||
|
<span className="text-xs whitespace-nowrap">
|
||||||
{translate(`::App.NotificationChannel.${channel}`)}
|
{translate(`::App.NotificationChannel.${channel}`)}
|
||||||
</Th>
|
</span>
|
||||||
))}
|
</span>
|
||||||
</Tr>
|
</th>
|
||||||
</THead>
|
))}
|
||||||
<TBody>
|
<th className="w-12 px-2 py-3" />
|
||||||
{notificationTypes.map((type, m) => (
|
</tr>
|
||||||
<Tr key={type.id}>
|
</thead>
|
||||||
<Th>
|
<tbody>
|
||||||
<Button
|
{notificationTypes.map((type) => (
|
||||||
shape="circle"
|
<tr
|
||||||
variant="plain"
|
key={type.id}
|
||||||
type="button"
|
className="border-t border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700"
|
||||||
size="sm"
|
>
|
||||||
disabled={saving}
|
<td className="sticky left-0 z-10 bg-white px-4 py-3 dark:bg-gray-800">
|
||||||
title={translate('::Abp.Identity.NotificationSettings.Reset')}
|
{renderTypeTitle(type)}
|
||||||
icon={<FaHistory />}
|
</td>
|
||||||
onClick={async (e) => {
|
{channels.map((channel) => (
|
||||||
e.preventDefault()
|
<td key={channel} className="px-3 py-3">
|
||||||
resetMyNotifications(type.id)
|
<div className="flex justify-center">{renderSwitcher(type.id, channel)}</div>
|
||||||
}}
|
</td>
|
||||||
/>
|
|
||||||
<span title={type.description ?? undefined}>{type.name}</span>
|
|
||||||
</Th>
|
|
||||||
{channels.map((channel, n) => (
|
|
||||||
<Td key={type.id + channel} className="text-center">
|
|
||||||
<Checkbox
|
|
||||||
name={type.id + channel}
|
|
||||||
disabled={list[m][n].isFixed || saving}
|
|
||||||
checked={list[m][n].isActive}
|
|
||||||
color={list[m][n].isCustomized ? 'red-500' : undefined}
|
|
||||||
onChange={(e) => onClickCheckbox(e, list[m][n])}
|
|
||||||
></Checkbox>
|
|
||||||
</Td>
|
|
||||||
))}
|
|
||||||
</Tr>
|
|
||||||
))}
|
))}
|
||||||
</TBody>
|
<td className="px-2 py-3 text-center">{renderResetButton(type.id)}</td>
|
||||||
</Table>
|
</tr>
|
||||||
) : (
|
))}
|
||||||
<p className="text-center">
|
</tbody>
|
||||||
{translate('::Abp.Identity.NotificationSettings.NoRule')}
|
</table>
|
||||||
</p>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
<div className="flex flex-col gap-3 md:hidden">
|
||||||
)}
|
{notificationTypes.map((type) => (
|
||||||
</>
|
<div key={type.id} className="rounded-xl border border-gray-200 p-4 dark:border-gray-700">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div>{renderTypeTitle(type)}</div>
|
||||||
|
{renderResetButton(type.id)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
|
{channels.map((channel) => (
|
||||||
|
<div
|
||||||
|
key={channel}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<ChannelIcon channel={channel} size="sm" />
|
||||||
|
<span className="text-sm">
|
||||||
|
{translate(`::App.NotificationChannel.${channel}`)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{renderSwitcher(type.id, channel)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue