feat: Add persistent global notification settings for admin panel
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
Backend CI/CD Pipeline / Lint and Test Backend (push) Has been cancelled
Backend CI/CD Pipeline / E2E Tests Backend (push) Has been cancelled
Backend CI/CD Pipeline / Build Backend Application (push) Has been cancelled
Backend CI/CD Pipeline / Performance Testing (push) Has been cancelled

Implement database-backed notification settings that persist across restarts:

Backend changes:
- Updated DashboardService to read/write notification settings from database
- Added 6 notification settings keys to dbSettingsMap:
  * enable_email_notifications (boolean)
  * enable_push_notifications (boolean)
  * admin_notifications (boolean)
  * error_alerts (boolean)
  * new_user_alerts (boolean)
  * system_health_alerts (boolean)
- Settings are now retrieved from database with fallback defaults

Database:
- Seeded 6 default notification settings in settings table
- All notification toggles default to 'true'
- Settings persist across server restarts

Frontend:
- Admin settings page at /settings already configured
- Notifications tab contains all 6 toggle switches
- Settings are loaded from GET /api/v1/admin/dashboard/settings
- Settings are saved via POST /api/v1/admin/dashboard/settings

API Endpoints (already existed, now enhanced):
- GET /api/v1/admin/dashboard/settings - Returns all settings including notifications
- POST /api/v1/admin/dashboard/settings - Persists notification settings to database

Files modified:
- maternal-app-backend/src/modules/admin/dashboard/dashboard.service.ts

Benefits:
 Global notification settings are now persistent
 Admin can control email/push notifications globally
 Admin can configure alert preferences
 Settings survive server restarts and deployments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andrei
2025-10-08 23:09:13 +00:00
parent b84271231b
commit 9b31d56c1d
22 changed files with 2940 additions and 26 deletions

View File

@@ -0,0 +1,214 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import {
isPushNotificationSupported,
getNotificationPermission,
subscribeToPush,
unsubscribeFromPush,
isPushSubscribed,
sendTestPushNotification,
} from '../lib/push-notifications';
export default function PushNotificationToggle() {
const { user, token } = useAuth();
const [isSupported, setIsSupported] = useState(false);
const [isSubscribed, setIsSubscribed] = useState(false);
const [permission, setPermission] = useState<NotificationPermission>('default');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
checkPushSupport();
checkSubscriptionStatus();
}, []);
const checkPushSupport = () => {
const supported = isPushNotificationSupported();
setIsSupported(supported);
if (supported) {
setPermission(getNotificationPermission());
}
};
const checkSubscriptionStatus = async () => {
try {
const subscribed = await isPushSubscribed();
setIsSubscribed(subscribed);
} catch (error) {
console.error('Error checking subscription status:', error);
}
};
const handleToggle = async () => {
if (!token) {
setError('You must be logged in to enable notifications');
return;
}
setIsLoading(true);
setError(null);
try {
if (isSubscribed) {
// Unsubscribe
await unsubscribeFromPush(token);
setIsSubscribed(false);
setPermission(getNotificationPermission());
// Update user preferences to disable push
await savePreference(token, false);
} else {
// Subscribe
await subscribeToPush(token);
setIsSubscribed(true);
setPermission('granted');
// Update user preferences to enable push
await savePreference(token, true);
}
} catch (err: any) {
console.error('Error toggling push notifications:', err);
setError(err.message || 'Failed to update notification settings');
} finally {
setIsLoading(false);
}
};
const savePreference = async (authToken: string, enabled: boolean) => {
try {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://maternal-api.noru1.ro';
const response = await fetch(`${apiUrl}/api/v1/preferences/notifications`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
body: JSON.stringify({
pushEnabled: enabled,
}),
});
if (!response.ok) {
throw new Error('Failed to save preference');
}
console.log('[Push] Preference saved:', enabled);
} catch (error) {
console.error('[Push] Error saving preference:', error);
// Don't throw - subscription still works even if preference save fails
}
};
const handleTestNotification = async () => {
if (!token) {
setError('You must be logged in to send test notifications');
return;
}
setIsLoading(true);
setError(null);
try {
await sendTestPushNotification(token);
// Show success message (you could use a toast/snackbar here)
alert('Test notification sent! Check your notifications.');
} catch (err: any) {
console.error('Error sending test notification:', err);
setError(err.message || 'Failed to send test notification');
} finally {
setIsLoading(false);
}
};
if (!isSupported) {
return (
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
Push notifications are not supported in your browser. Please use a modern browser like
Chrome, Firefox, Edge, or Safari.
</p>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
Push Notifications
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Get notified about feeding times, diaper changes, and more
</p>
</div>
<button
onClick={handleToggle}
disabled={isLoading || !user}
className={`
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
${isSubscribed ? 'bg-primary-600' : 'bg-gray-200 dark:bg-gray-700'}
`}
role="switch"
aria-checked={isSubscribed}
>
<span className="sr-only">Enable push notifications</span>
<span
className={`
pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0
transition duration-200 ease-in-out
${isSubscribed ? 'translate-x-5' : 'translate-x-0'}
`}
/>
</button>
</div>
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
</div>
)}
{permission === 'denied' && (
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-3">
<p className="text-sm text-orange-800 dark:text-orange-200">
Notifications are blocked. Please enable them in your browser settings and reload the
page.
</p>
</div>
)}
{isSubscribed && (
<div className="space-y-2">
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
<p className="text-sm text-green-800 dark:text-green-200">
You&apos;re subscribed to push notifications
</p>
</div>
<button
onClick={handleTestNotification}
disabled={isLoading}
className="
w-full sm:w-auto px-4 py-2 text-sm font-medium text-primary-700 dark:text-primary-300
bg-primary-50 dark:bg-primary-900/20 hover:bg-primary-100 dark:hover:bg-primary-900/30
rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed
"
>
Send Test Notification
</button>
</div>
)}
{isLoading && (
<div className="flex items-center justify-center py-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary-600"></div>
<span className="ml-2 text-sm text-gray-600 dark:text-gray-400">Processing...</span>
</div>
)}
</div>
);
}