erp-core-frontend-v2/src/features/notifications/hooks/useNotifications.ts
rckrdmrd 6b7f438669 feat(frontend): Add Audit and Notifications modules (MGN-007, MGN-008)
Audit module (MGN-007):
- AuditLogsPage.tsx: Full audit logs UI with filtering, stats cards, detail modal
- Uses existing types, api, hooks from settings feature

Notifications module (MGN-008):
- types/notifications.types.ts: Complete types for channels, templates, preferences, in-app
- api/notifications.api.ts: API clients for all notification operations
- hooks/useNotifications.ts: 5 hooks (useChannels, useTemplates, useNotificationPreferences,
  useInAppNotifications, useNotificationBell)
- NotificationsPage.tsx: Full notifications center with category cards, filtering,
  read/unread management

Both modules complement complete backends with comprehensive frontend UIs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 11:20:25 -06:00

357 lines
10 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import {
channelsApi,
templatesApi,
preferencesApi,
inAppApi,
} from '../api';
import type {
NotificationChannel,
NotificationTemplate,
NotificationTemplateCreateInput,
NotificationPreference,
NotificationPreferenceUpdateInput,
InAppNotification,
InAppNotificationsFilters,
} from '../types';
// ============================================================================
// Channels Hook
// ============================================================================
interface UseChannelsReturn {
channels: NotificationChannel[];
isLoading: boolean;
error: Error | null;
refresh: () => Promise<void>;
getByCode: (code: string) => Promise<NotificationChannel>;
}
export function useChannels(): UseChannelsReturn {
const [channels, setChannels] = useState<NotificationChannel[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchChannels = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const data = await channelsApi.getAll();
setChannels(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Error fetching channels'));
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetchChannels();
}, [fetchChannels]);
const getByCode = useCallback(async (code: string): Promise<NotificationChannel> => {
return channelsApi.getByCode(code);
}, []);
return {
channels,
isLoading,
error,
refresh: fetchChannels,
getByCode,
};
}
// ============================================================================
// Templates Hook
// ============================================================================
interface UseTemplatesReturn {
templates: NotificationTemplate[];
isLoading: boolean;
error: Error | null;
refresh: () => Promise<void>;
getByCode: (code: string) => Promise<NotificationTemplate>;
create: (data: NotificationTemplateCreateInput) => Promise<NotificationTemplate>;
update: (id: string, data: Partial<NotificationTemplateCreateInput>) => Promise<NotificationTemplate>;
remove: (id: string) => Promise<void>;
}
export function useTemplates(): UseTemplatesReturn {
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchTemplates = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const data = await templatesApi.getAll();
setTemplates(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Error fetching templates'));
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetchTemplates();
}, [fetchTemplates]);
const getByCode = useCallback(async (code: string): Promise<NotificationTemplate> => {
return templatesApi.getByCode(code);
}, []);
const create = useCallback(async (data: NotificationTemplateCreateInput): Promise<NotificationTemplate> => {
const result = await templatesApi.create(data);
await fetchTemplates();
return result;
}, [fetchTemplates]);
const update = useCallback(async (id: string, data: Partial<NotificationTemplateCreateInput>): Promise<NotificationTemplate> => {
const result = await templatesApi.update(id, data);
await fetchTemplates();
return result;
}, [fetchTemplates]);
const remove = useCallback(async (id: string): Promise<void> => {
await templatesApi.delete(id);
await fetchTemplates();
}, [fetchTemplates]);
return {
templates,
isLoading,
error,
refresh: fetchTemplates,
getByCode,
create,
update,
remove,
};
}
// ============================================================================
// Preferences Hook
// ============================================================================
interface UseNotificationPreferencesReturn {
preferences: NotificationPreference | null;
isLoading: boolean;
isSaving: boolean;
error: Error | null;
refresh: () => Promise<void>;
update: (data: NotificationPreferenceUpdateInput) => Promise<NotificationPreference>;
}
export function useNotificationPreferences(): UseNotificationPreferencesReturn {
const [preferences, setPreferences] = useState<NotificationPreference | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchPreferences = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const data = await preferencesApi.get();
setPreferences(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Error fetching preferences'));
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetchPreferences();
}, [fetchPreferences]);
const update = useCallback(async (data: NotificationPreferenceUpdateInput): Promise<NotificationPreference> => {
setIsSaving(true);
setError(null);
try {
const result = await preferencesApi.update(data);
setPreferences(result);
return result;
} catch (err) {
const error = err instanceof Error ? err : new Error('Error updating preferences');
setError(error);
throw error;
} finally {
setIsSaving(false);
}
}, []);
return {
preferences,
isLoading,
isSaving,
error,
refresh: fetchPreferences,
update,
};
}
// ============================================================================
// In-App Notifications Hook
// ============================================================================
interface UseInAppNotificationsOptions {
initialFilters?: InAppNotificationsFilters;
autoLoad?: boolean;
pollInterval?: number;
}
interface UseInAppNotificationsReturn {
notifications: InAppNotification[];
unreadCount: number;
total: number;
page: number;
totalPages: number;
isLoading: boolean;
error: Error | null;
filters: InAppNotificationsFilters;
setFilters: (filters: InAppNotificationsFilters) => void;
refresh: () => Promise<void>;
markAsRead: (id: string) => Promise<void>;
markAllAsRead: () => Promise<void>;
archive: (id: string) => Promise<void>;
remove: (id: string) => Promise<void>;
}
export function useInAppNotifications(
options: UseInAppNotificationsOptions = {}
): UseInAppNotificationsReturn {
const { initialFilters = {}, autoLoad = true, pollInterval } = options;
const [notifications, setNotifications] = useState<InAppNotification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [filters, setFilters] = useState<InAppNotificationsFilters>(initialFilters);
const fetchNotifications = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await inAppApi.getAll(filters);
setNotifications(response.data);
setUnreadCount(response.unreadCount);
setTotal(response.total);
setPage(response.page);
setTotalPages(response.totalPages);
} catch (err) {
setError(err instanceof Error ? err : new Error('Error fetching notifications'));
} finally {
setIsLoading(false);
}
}, [filters]);
const fetchUnreadCount = useCallback(async () => {
try {
const count = await inAppApi.getUnreadCount();
setUnreadCount(count);
} catch (err) {
// Silently fail for unread count polling
}
}, []);
useEffect(() => {
if (autoLoad) {
fetchNotifications();
}
}, [autoLoad, fetchNotifications]);
// Poll for unread count
useEffect(() => {
if (!pollInterval) return;
const interval = setInterval(fetchUnreadCount, pollInterval);
return () => clearInterval(interval);
}, [pollInterval, fetchUnreadCount]);
const markAsRead = useCallback(async (id: string): Promise<void> => {
await inAppApi.markAsRead(id);
setNotifications(prev =>
prev.map(n => n.id === id ? { ...n, isRead: true, readAt: new Date().toISOString() } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
}, []);
const markAllAsRead = useCallback(async (): Promise<void> => {
await inAppApi.markAllAsRead();
setNotifications(prev =>
prev.map(n => ({ ...n, isRead: true, readAt: new Date().toISOString() }))
);
setUnreadCount(0);
}, []);
const archive = useCallback(async (id: string): Promise<void> => {
await inAppApi.archive(id);
setNotifications(prev => prev.filter(n => n.id !== id));
}, []);
const remove = useCallback(async (id: string): Promise<void> => {
await inAppApi.delete(id);
setNotifications(prev => prev.filter(n => n.id !== id));
}, []);
return {
notifications,
unreadCount,
total,
page,
totalPages,
isLoading,
error,
filters,
setFilters,
refresh: fetchNotifications,
markAsRead,
markAllAsRead,
archive,
remove,
};
}
// ============================================================================
// Notification Bell Hook (for header/navbar)
// ============================================================================
interface UseNotificationBellReturn {
unreadCount: number;
recentNotifications: InAppNotification[];
isLoading: boolean;
refresh: () => Promise<void>;
markAsRead: (id: string) => Promise<void>;
markAllAsRead: () => Promise<void>;
}
export function useNotificationBell(pollInterval = 30000): UseNotificationBellReturn {
const {
notifications: recentNotifications,
unreadCount,
isLoading,
refresh,
markAsRead,
markAllAsRead,
} = useInAppNotifications({
initialFilters: { limit: 5, includeRead: false },
pollInterval,
});
return {
unreadCount,
recentNotifications,
isLoading,
refresh,
markAsRead,
markAllAsRead,
};
}