fixed notification system and added a new tenant notifications receipt table to track read status and filter messages by scope.
This commit is contained in:
@@ -266,6 +266,18 @@ export default function MobileEventPhotosPage() {
|
||||
))}
|
||||
</XStack>
|
||||
|
||||
{!loading ? (
|
||||
<LimitWarnings
|
||||
limits={limits}
|
||||
addons={catalogAddons}
|
||||
onCheckout={(scopeOrKey) => { void handleCheckout(scopeOrKey, slug, catalogAddons, setBusyScope, t); }}
|
||||
busyScope={busyScope}
|
||||
translate={translateLimits(t)}
|
||||
textColor={text}
|
||||
borderColor={border}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
@@ -281,15 +293,6 @@ export default function MobileEventPhotosPage() {
|
||||
</MobileCard>
|
||||
) : (
|
||||
<YStack space="$3">
|
||||
<LimitWarnings
|
||||
limits={limits}
|
||||
addons={catalogAddons}
|
||||
onCheckout={(scopeOrKey) => { void handleCheckout(scopeOrKey, slug, catalogAddons, setBusyScope, t); }}
|
||||
busyScope={busyScope}
|
||||
translate={translateLimits(t)}
|
||||
textColor={text}
|
||||
borderColor={border}
|
||||
/>
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('mobilePhotos.count', '{{count}} photos', { count: totalCount })}
|
||||
</Text>
|
||||
@@ -490,6 +493,7 @@ function translateLimits(t: (key: string, defaultValue?: string, options?: Recor
|
||||
galleryWarningDays: 'Gallery expires in {{days}} days.',
|
||||
buyMorePhotos: 'Buy more photos',
|
||||
extendGallery: 'Extend gallery',
|
||||
buyMoreGuests: 'Add more guests',
|
||||
};
|
||||
return (key, options) => t(`limits.${key}`, defaults[key] ?? key, options);
|
||||
}
|
||||
@@ -524,7 +528,7 @@ function LimitWarnings({
|
||||
<Text fontSize="$sm" color={textColor} fontWeight="700">
|
||||
{warning.message}
|
||||
</Text>
|
||||
{(warning.scope === 'photos' || warning.scope === 'gallery') && addons.length ? (
|
||||
{(warning.scope === 'photos' || warning.scope === 'gallery' || warning.scope === 'guests') && addons.length ? (
|
||||
<MobileAddonsPicker
|
||||
scope={warning.scope}
|
||||
addons={addons}
|
||||
@@ -539,7 +543,7 @@ function LimitWarnings({
|
||||
? translate('buyMorePhotos')
|
||||
: warning.scope === 'gallery'
|
||||
? translate('extendGallery')
|
||||
: translate('buyMorePhotos')
|
||||
: translate('buyMoreGuests')
|
||||
}
|
||||
onPress={() => onCheckout(warning.scope)}
|
||||
loading={busyScope === warning.scope}
|
||||
@@ -557,7 +561,7 @@ function MobileAddonsPicker({
|
||||
onCheckout,
|
||||
translate,
|
||||
}: {
|
||||
scope: 'photos' | 'gallery';
|
||||
scope: 'photos' | 'gallery' | 'guests';
|
||||
addons: EventAddonCatalogItem[];
|
||||
busy: boolean;
|
||||
onCheckout: (addonKey: string) => void;
|
||||
@@ -603,7 +607,13 @@ function MobileAddonsPicker({
|
||||
))}
|
||||
</select>
|
||||
<CTAButton
|
||||
label={scope === 'gallery' ? translate('extendGallery') : translate('buyMorePhotos')}
|
||||
label={
|
||||
scope === 'gallery'
|
||||
? translate('extendGallery')
|
||||
: scope === 'guests'
|
||||
? translate('buyMoreGuests')
|
||||
: translate('buyMorePhotos')
|
||||
}
|
||||
disabled={!selected || busy}
|
||||
onPress={() => selected && onCheckout(selected)}
|
||||
loading={busy}
|
||||
@@ -640,15 +650,25 @@ function EventAddonList({ addons, textColor, mutedColor }: { addons: EventAddonS
|
||||
}
|
||||
|
||||
async function handleCheckout(
|
||||
scopeOrKey: 'photos' | 'gallery' | string,
|
||||
scopeOrKey: 'photos' | 'gallery' | 'guests' | string,
|
||||
slug: string | null,
|
||||
addons: EventAddonCatalogItem[],
|
||||
setBusyScope: (scope: string | null) => void,
|
||||
t: (key: string, defaultValue?: string) => string,
|
||||
): Promise<void> {
|
||||
if (!slug) return;
|
||||
const scope = scopeOrKey === 'photos' || scopeOrKey === 'gallery' ? scopeOrKey : scopeOrKey.includes('gallery') ? 'gallery' : 'photos';
|
||||
const addonKey = scopeOrKey === 'photos' || scopeOrKey === 'gallery' ? selectAddonKeyForScope(addons, scope) : scopeOrKey;
|
||||
const scope =
|
||||
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
|
||||
? scopeOrKey
|
||||
: scopeOrKey.includes('gallery')
|
||||
? 'gallery'
|
||||
: scopeOrKey.includes('guest')
|
||||
? 'guests'
|
||||
: 'photos';
|
||||
const addonKey =
|
||||
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
|
||||
? selectAddonKeyForScope(addons, scope)
|
||||
: scopeOrKey;
|
||||
const currentUrl = typeof window !== 'undefined' ? `${window.location.origin}${adminPath(`/mobile/events/${slug}/photos`)}` : '';
|
||||
const successUrl = `${currentUrl}?addon_success=1`;
|
||||
setBusyScope(scope);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { MobileShell } from './components/MobileShell';
|
||||
import { MobileCard, PillBadge } from './components/Primitives';
|
||||
import { GuestNotificationSummary, listGuestNotifications } from '../api';
|
||||
import { listNotificationLogs, markNotificationLogs, NotificationLogEntry } from '../api';
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
import { getApiErrorMessage } from '../lib/apiError';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -21,18 +21,184 @@ type NotificationItem = {
|
||||
body: string;
|
||||
time: string;
|
||||
tone: 'info' | 'warning';
|
||||
eventId?: number | null;
|
||||
is_read?: boolean;
|
||||
scope: 'photos' | 'guests' | 'gallery' | 'events' | 'package' | 'general';
|
||||
};
|
||||
|
||||
async function loadNotifications(slug?: string): Promise<NotificationItem[]> {
|
||||
function formatLog(
|
||||
log: NotificationLogEntry,
|
||||
t: (key: string, defaultValue?: string, options?: Record<string, unknown>) => string,
|
||||
eventName?: string | null
|
||||
): NotificationItem {
|
||||
const ctx = log.context ?? {};
|
||||
const limit = typeof ctx.limit === 'number' ? ctx.limit : null;
|
||||
const used = typeof ctx.used === 'number' ? ctx.used : null;
|
||||
const remaining = typeof ctx.remaining === 'number' ? ctx.remaining : null;
|
||||
const days = typeof ctx.day === 'number' ? ctx.day : null;
|
||||
const ctxEventId = ctx.event_id ?? ctx.eventId;
|
||||
const eventId = typeof ctxEventId === 'string' ? Number(ctxEventId) : (typeof ctxEventId === 'number' ? ctxEventId : null);
|
||||
const name = eventName ?? t('mobileNotifications.unknownEvent', 'Event');
|
||||
const isRead = log.is_read === true;
|
||||
const scope = (() => {
|
||||
switch (log.type) {
|
||||
case 'photo_limit':
|
||||
case 'photo_threshold':
|
||||
return 'photos';
|
||||
case 'guest_limit':
|
||||
case 'guest_threshold':
|
||||
return 'guests';
|
||||
case 'gallery_warning':
|
||||
case 'gallery_expired':
|
||||
return 'gallery';
|
||||
case 'event_limit':
|
||||
case 'event_threshold':
|
||||
return 'events';
|
||||
case 'package_expiring':
|
||||
case 'package_expired':
|
||||
return 'package';
|
||||
default:
|
||||
return 'general';
|
||||
}
|
||||
})();
|
||||
|
||||
switch (log.type) {
|
||||
case 'photo_limit':
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: t('notificationLogs.photoLimit.title', 'Photo limit reached'),
|
||||
body: t('notificationLogs.photoLimit.body', '{{event}} reached its photo limit of {{limit}}.', {
|
||||
event: name,
|
||||
limit: limit ?? '—',
|
||||
}),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'warning',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
case 'guest_limit':
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: t('notificationLogs.guestLimit.title', 'Guest limit reached'),
|
||||
body: t('notificationLogs.guestLimit.body', '{{event}} reached its guest limit of {{limit}}.', {
|
||||
event: name,
|
||||
limit: limit ?? '—',
|
||||
}),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'warning',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
case 'event_limit':
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: t('notificationLogs.eventLimit.title', 'Event quota reached'),
|
||||
body: t('notificationLogs.eventLimit.body', 'Your package allows no more events. Limit: {{limit}}.', {
|
||||
limit: limit ?? '—',
|
||||
}),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'warning',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
case 'gallery_warning':
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: t('notificationLogs.galleryWarning.title', 'Gallery expiring soon'),
|
||||
body: t('notificationLogs.galleryWarning.body', '{{event}} expires in {{days}} days.', {
|
||||
event: name,
|
||||
days: days ?? ctx.threshold ?? '—',
|
||||
}),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'warning',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
case 'gallery_expired':
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: t('notificationLogs.galleryExpired.title', 'Gallery expired'),
|
||||
body: t('notificationLogs.galleryExpired.body', '{{event}} gallery is offline. Extend to reactivate.', {
|
||||
event: name,
|
||||
}),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'warning',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
case 'photo_threshold':
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: t('notificationLogs.photoThreshold.title', 'Photo usage warning'),
|
||||
body: t('notificationLogs.photoThreshold.body', '{{event}} is at {{used}} / {{limit}} photos.', {
|
||||
event: name,
|
||||
used: used ?? '—',
|
||||
limit: limit ?? '—',
|
||||
}),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'info',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
case 'guest_threshold':
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: t('notificationLogs.guestThreshold.title', 'Guest usage warning'),
|
||||
body: t('notificationLogs.guestThreshold.body', '{{event}} is at {{used}} / {{limit}} guests.', {
|
||||
event: name,
|
||||
used: used ?? '—',
|
||||
limit: limit ?? '—',
|
||||
}),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'info',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
id: String(log.id),
|
||||
title: log.type,
|
||||
body: t('notificationLogs.generic.body', 'Notification sent via {{channel}}.', { channel: log.channel }),
|
||||
time: log.sent_at ?? log.failed_at ?? '',
|
||||
tone: 'info',
|
||||
eventId,
|
||||
is_read: isRead,
|
||||
scope,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotifications(
|
||||
t: (key: string, defaultValue?: string, options?: Record<string, unknown>) => string,
|
||||
events?: TenantEvent[],
|
||||
filters?: { scope?: string; status?: string; eventSlug?: string }
|
||||
): Promise<NotificationItem[]> {
|
||||
try {
|
||||
const result = slug ? await listGuestNotifications(slug) : [];
|
||||
return (result ?? []).map((item: GuestNotificationSummary) => ({
|
||||
id: String(item.id),
|
||||
title: item.title || 'Notification',
|
||||
body: item.body ?? '',
|
||||
time: item.created_at ?? '',
|
||||
tone: item.type === 'support_tip' ? 'warning' : 'info',
|
||||
}));
|
||||
const eventId = filters?.eventSlug ? (events ?? []).find((ev) => ev.slug === filters.eventSlug)?.id ?? undefined : undefined;
|
||||
const response = await listNotificationLogs({
|
||||
perPage: 50,
|
||||
scope: filters?.scope && filters.scope !== 'all' ? filters.scope : undefined,
|
||||
status: filters?.status === 'all' ? undefined : filters?.status,
|
||||
eventId: eventId,
|
||||
});
|
||||
const lookup = new Map<number, string>();
|
||||
(events ?? []).forEach((event) => {
|
||||
lookup.set(event.id, typeof event.name === 'string' ? event.name : (event.name as Record<string, string>)?.en ?? '');
|
||||
});
|
||||
|
||||
return (response.data ?? [])
|
||||
.map((log) => {
|
||||
const ctxEventId = log.context?.event_id ?? log.context?.eventId;
|
||||
const parsed = typeof ctxEventId === 'string' ? Number(ctxEventId) : ctxEventId;
|
||||
return formatLog(log, t, lookup.get(parsed as number));
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
@@ -43,6 +209,8 @@ export default function MobileNotificationsPage() {
|
||||
const { t } = useTranslation('management');
|
||||
const search = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
||||
const slug = search.get('event') ?? undefined;
|
||||
const scopeParam = search.get('scope') ?? 'all';
|
||||
const statusParam = search.get('status') ?? 'unread';
|
||||
const [notifications, setNotifications] = React.useState<NotificationItem[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
@@ -62,7 +230,7 @@ export default function MobileNotificationsPage() {
|
||||
const reload = React.useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await loadNotifications(slug ?? undefined);
|
||||
const data = await loadNotifications(t, events, { scope: scopeParam, status: statusParam, eventSlug: slug });
|
||||
setNotifications(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
@@ -74,7 +242,7 @@ export default function MobileNotificationsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [slug, t]);
|
||||
}, [slug, t, events]);
|
||||
|
||||
React.useEffect(() => {
|
||||
void reload();
|
||||
@@ -91,6 +259,31 @@ export default function MobileNotificationsPage() {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const filtered = React.useMemo(() => {
|
||||
if (!slug) return notifications;
|
||||
const target = events.find((ev) => ev.slug === slug);
|
||||
if (!target) return notifications;
|
||||
return notifications.filter((item) => item.eventId === target.id || item.body.includes(String(target.name)) || item.title.includes(String(target.name)));
|
||||
}, [notifications, slug, events]);
|
||||
|
||||
const scoped = React.useMemo(() => {
|
||||
if (scopeParam === 'all') return filtered;
|
||||
return filtered.filter((item) => item.scope === scopeParam);
|
||||
}, [filtered, scopeParam]);
|
||||
|
||||
const statusFiltered = React.useMemo(() => {
|
||||
if (statusParam === 'all') return scoped;
|
||||
if (statusParam === 'read') return scoped.filter((item) => item.is_read);
|
||||
return scoped.filter((item) => !item.is_read);
|
||||
}, [scoped, statusParam]);
|
||||
|
||||
const unreadIds = React.useMemo(
|
||||
() => scoped.filter((item) => !item.is_read).map((item) => Number(item.id)).filter((id) => Number.isFinite(id)),
|
||||
[scoped]
|
||||
);
|
||||
|
||||
const showFilterNotice = Boolean(slug) && filtered.length === 0 && notifications.length > 0;
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
activeTab="home"
|
||||
@@ -110,13 +303,69 @@ export default function MobileNotificationsPage() {
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
{showFilterNotice ? (
|
||||
<MobileCard space="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('notificationLogs.filterEmpty', 'No notifications for this event.')}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
navigate('/admin/mobile/notifications', { replace: true });
|
||||
}}
|
||||
>
|
||||
<Text fontSize="$sm" color={primary} fontWeight="700">
|
||||
{t('notificationLogs.clearFilter', 'Show all notifications')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<XStack space="$2" marginBottom="$2">
|
||||
<select
|
||||
value={statusParam}
|
||||
onChange={(e) => navigate(`/admin/mobile/notifications?${new URLSearchParams({ status: e.target.value, scope: scopeParam, event: slug ?? '' }).toString()}`)}
|
||||
style={{ border: `1px solid ${border}`, borderRadius: 10, padding: '6px 10px', fontSize: 13 }}
|
||||
>
|
||||
<option value="unread">{t('notificationLogs.filter.unread', 'Unread')}</option>
|
||||
<option value="read">{t('notificationLogs.filter.read', 'Read')}</option>
|
||||
<option value="all">{t('notificationLogs.filter.all', 'All')}</option>
|
||||
</select>
|
||||
<select
|
||||
value={scopeParam}
|
||||
onChange={(e) => navigate(`/admin/mobile/notifications?${new URLSearchParams({ scope: e.target.value, status: statusParam, event: slug ?? '' }).toString()}`)}
|
||||
style={{ border: `1px solid ${border}`, borderRadius: 10, padding: '6px 10px', fontSize: 13 }}
|
||||
>
|
||||
<option value="all">{t('notificationLogs.scope.all', 'All scopes')}</option>
|
||||
<option value="photos">{t('notificationLogs.scope.photos', 'Photos')}</option>
|
||||
<option value="guests">{t('notificationLogs.scope.guests', 'Guests')}</option>
|
||||
<option value="gallery">{t('notificationLogs.scope.gallery', 'Gallery')}</option>
|
||||
<option value="events">{t('notificationLogs.scope.events', 'Events')}</option>
|
||||
<option value="package">{t('notificationLogs.scope.package', 'Package')}</option>
|
||||
<option value="general">{t('notificationLogs.scope.general', 'General')}</option>
|
||||
</select>
|
||||
{unreadIds.length ? (
|
||||
<CTAButton
|
||||
label={t('notificationLogs.markAllRead', 'Mark all read')}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await markNotificationLogs(unreadIds, 'read');
|
||||
void reload();
|
||||
} catch {
|
||||
toast.error(t('notificationLogs.markFailed', 'Could not update notifications.'));
|
||||
}
|
||||
}}
|
||||
tone="ghost"
|
||||
/>
|
||||
) : null}
|
||||
</XStack>
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
<MobileCard key={`al-${idx}`} height={70} opacity={0.6} />
|
||||
))}
|
||||
</YStack>
|
||||
) : notifications.length === 0 ? (
|
||||
) : statusFiltered.length === 0 ? (
|
||||
<MobileCard alignItems="center" justifyContent="center" space="$2">
|
||||
<Bell size={24} color={String(theme.gray9?.val ?? '#9ca3af')} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
@@ -132,8 +381,8 @@ export default function MobileNotificationsPage() {
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{notifications.map((item) => (
|
||||
<MobileCard key={item.id} space="$2">
|
||||
{statusFiltered.map((item) => (
|
||||
<MobileCard key={item.id} space="$2" borderColor={item.is_read ? border : primary}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack
|
||||
width={36}
|
||||
@@ -153,6 +402,7 @@ export default function MobileNotificationsPage() {
|
||||
{item.body}
|
||||
</Text>
|
||||
</YStack>
|
||||
{!item.is_read ? <PillBadge tone="warning">{t('notificationLogs.unread', 'Unread')}</PillBadge> : null}
|
||||
<PillBadge tone={item.tone === 'warning' ? 'warning' : 'muted'}>{item.time}</PillBadge>
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { EventAddonCatalogItem } from '../api';
|
||||
|
||||
export const scopeDefaults: Record<'photos' | 'gallery', string[]> = {
|
||||
export const scopeDefaults: Record<'photos' | 'gallery' | 'guests', string[]> = {
|
||||
photos: ['extra_photos_500', 'extra_photos_2000'],
|
||||
gallery: ['extend_gallery_30d', 'extend_gallery_90d'],
|
||||
guests: ['extra_guests_300', 'extra_guests_100', 'extra_guests_50', 'extra_guests'],
|
||||
};
|
||||
|
||||
export function selectAddonKeyForScope(addons: EventAddonCatalogItem[], scope: 'photos' | 'gallery'): string {
|
||||
const fallback = scope === 'photos' ? 'extra_photos_500' : 'extend_gallery_30d';
|
||||
export function selectAddonKeyForScope(addons: EventAddonCatalogItem[], scope: 'photos' | 'gallery' | 'guests'): string {
|
||||
const fallback = scope === 'photos' ? 'extra_photos_500' : scope === 'gallery' ? 'extend_gallery_30d' : 'extra_guests';
|
||||
const filtered = addons.filter((addon) => addon.price_id && scopeDefaults[scope].includes(addon.key));
|
||||
if (filtered.length) {
|
||||
return filtered[0].key;
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEventContext } from '../../context/EventContext';
|
||||
import { listGuestNotifications } from '../../api';
|
||||
import { listNotificationLogs } from '../../api';
|
||||
|
||||
/**
|
||||
* Badge count for notifications bell in the mobile shell.
|
||||
* Fetches guest notifications for the active event and returns count.
|
||||
* Uses tenant notification logs so the badge matches the notifications screen.
|
||||
*/
|
||||
export function useNotificationsBadge() {
|
||||
const { activeEvent } = useEventContext();
|
||||
const slug = activeEvent?.slug;
|
||||
|
||||
const { data: count = 0 } = useQuery<number>({
|
||||
queryKey: ['mobile', 'notifications', 'badge', slug],
|
||||
enabled: Boolean(slug),
|
||||
queryKey: ['mobile', 'notifications', 'badge', 'tenant'],
|
||||
staleTime: 60_000,
|
||||
queryFn: async () => {
|
||||
if (!slug) {
|
||||
return 0;
|
||||
const logs = await listNotificationLogs({ perPage: 1 });
|
||||
const meta: any = logs.meta ?? {};
|
||||
if (typeof meta.unread_count === 'number') {
|
||||
return meta.unread_count;
|
||||
}
|
||||
const notifications = await listGuestNotifications(slug);
|
||||
return Array.isArray(notifications) ? notifications.length : 0;
|
||||
return Array.isArray(logs.data) ? logs.data.filter((log) => log.is_read === false).length : 0;
|
||||
},
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
return React.useMemo(() => ({ count }), [count]);
|
||||
|
||||
Reference in New Issue
Block a user