diff --git a/CHANGELOG.md b/CHANGELOG.md index 300b307..5c3115b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Notification settings.** New Notifications tab in organization settings lets owners and admins toggle billing and uptime notification categories. Disabling a category stops new notifications of that type from being created. - **In-app notification center.** Bell icon in the header with dropdown of recent notifications. Uptime monitor status changes (down, degraded, recovered) create in-app notifications with links to the uptime page. - **Payment failed notifications.** When Stripe sends `invoice.payment_failed`, owners and admins receive an in-app notification with a link to update payment method. Members do not see billing notifications. - **Pageview limit notifications.** Owners and admins are notified when usage reaches 80%, 90%, or 100% of the plan limit (checked every 6 hours). diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index 70d613d..c44fb31 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -19,6 +19,7 @@ import { import { getSubscription, createPortalSession, getInvoices, cancelSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Invoice } from '@/lib/api/billing' import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex } from '@/lib/plans' import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit' +import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { motion, AnimatePresence } from 'framer-motion' @@ -33,8 +34,19 @@ import { BookOpenIcon, DownloadIcon, ExternalLinkIcon, - LayoutDashboardIcon + LayoutDashboardIcon, + Checkbox } from '@ciphera-net/ui' + +// * Bell icon for notifications tab +function BellIcon({ className }: { className?: string }) { + return ( + + + + + ) +} // @ts-ignore import { Button, Input } from '@ciphera-net/ui' @@ -43,13 +55,13 @@ export default function OrganizationSettings() { const router = useRouter() const searchParams = useSearchParams() // Initialize from URL, default to 'general' - const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing' | 'audit'>(() => { + const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing' | 'notifications' | 'audit'>(() => { const tab = searchParams.get('tab') - return (tab === 'billing' || tab === 'members' || tab === 'audit') ? tab : 'general' + return (tab === 'billing' || tab === 'members' || tab === 'notifications' || tab === 'audit') ? tab : 'general' }) // Sync URL with state without triggering navigation/reload - const handleTabChange = (tab: 'general' | 'members' | 'billing' | 'audit') => { + const handleTabChange = (tab: 'general' | 'members' | 'billing' | 'notifications' | 'audit') => { setActiveTab(tab) const url = new URL(window.location.href) url.searchParams.set('tab', tab) @@ -107,6 +119,12 @@ export default function OrganizationSettings() { const [auditStartDate, setAuditStartDate] = useState('') const [auditEndDate, setAuditEndDate] = useState('') + // Notification settings state + const [notificationSettings, setNotificationSettings] = useState>({}) + const [notificationCategories, setNotificationCategories] = useState<{ id: string; label: string; description: string }[]>([]) + const [isLoadingNotificationSettings, setIsLoadingNotificationSettings] = useState(false) + const [isSavingNotificationSettings, setIsSavingNotificationSettings] = useState(false) + // Refs for filters to keep loadAudit stable and avoid rapid re-renders const filtersRef = useRef({ action: auditActionFilter, @@ -248,6 +266,27 @@ export default function OrganizationSettings() { } }, [activeTab, currentOrgId, loadAudit, auditFetchTrigger]) + const loadNotificationSettings = useCallback(async () => { + if (!currentOrgId) return + setIsLoadingNotificationSettings(true) + try { + const res = await getNotificationSettings() + setNotificationSettings(res.settings || {}) + setNotificationCategories(res.categories || []) + } catch (error) { + console.error('Failed to load notification settings', error) + toast.error(getAuthErrorMessage(error as Error) || 'Failed to load notification settings') + } finally { + setIsLoadingNotificationSettings(false) + } + }, [currentOrgId]) + + useEffect(() => { + if (activeTab === 'notifications' && currentOrgId) { + loadNotificationSettings() + } + }, [activeTab, currentOrgId, loadNotificationSettings]) + // If no org ID, we are in personal organization context, so don't show org settings if (!currentOrgId) { return ( @@ -460,6 +499,19 @@ export default function OrganizationSettings() { Billing +