Notification updates

This commit is contained in:
Eric Gullickson
2025-12-21 19:56:52 -06:00
parent 144f1d5bb0
commit 719c80ecd8
80 changed files with 7552 additions and 678 deletions

View File

@@ -0,0 +1,23 @@
/**
* @ai-summary API calls for notifications feature
*/
import { apiClient } from '../../../core/api/client';
import { NotificationSummary, DueMaintenanceItem, ExpiringDocument } from '../types/notifications.types';
export const notificationsApi = {
getSummary: async (): Promise<NotificationSummary> => {
const response = await apiClient.get('/notifications/summary');
return response.data;
},
getDueMaintenanceItems: async (): Promise<DueMaintenanceItem[]> => {
const response = await apiClient.get('/notifications/maintenance');
return response.data;
},
getExpiringDocuments: async (): Promise<ExpiringDocument[]> => {
const response = await apiClient.get('/notifications/documents');
return response.data;
},
};

View File

@@ -0,0 +1,50 @@
/**
* @ai-summary Email notification toggle component
* @ai-context Mobile-first responsive toggle switch for email notifications
*/
import React from 'react';
interface EmailNotificationToggleProps {
enabled: boolean;
onChange: (enabled: boolean) => void;
label?: string;
className?: string;
}
export const EmailNotificationToggle: React.FC<EmailNotificationToggleProps> = ({
enabled,
onChange,
label = 'Email notifications',
className = '',
}) => {
return (
<div className={`flex items-center justify-between gap-3 ${className}`}>
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
{label}
</label>
<button
type="button"
role="switch"
aria-checked={enabled}
aria-label={label}
onClick={() => onChange(!enabled)}
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
${enabled ? 'bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}
`}
style={{ minWidth: '44px', minHeight: '44px', padding: '9px 0' }}
>
<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
${enabled ? 'translate-x-5' : 'translate-x-0'}
`}
/>
</button>
</div>
);
};

View File

@@ -0,0 +1,48 @@
/**
* @ai-summary Hook to show login notifications toast based on notification summary
* @ai-context Shows once per session on successful authentication
*/
import { useEffect, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useAuth0 } from '@auth0/auth0-react';
import toast from 'react-hot-toast';
import { notificationsApi } from '../api/notifications.api';
export function useLoginNotifications() {
const { isAuthenticated } = useAuth0();
const hasShownToast = useRef(false);
const { data: summary } = useQuery({
queryKey: ['notificationSummary'],
queryFn: notificationsApi.getSummary,
enabled: isAuthenticated && !hasShownToast.current,
staleTime: 5 * 60 * 1000, // 5 minutes
});
useEffect(() => {
if (summary && !hasShownToast.current) {
const maintenanceCount = summary.maintenanceDueSoon + summary.maintenanceOverdue;
const documentCount = summary.documentsExpiringSoon + summary.documentsExpired;
const total = maintenanceCount + documentCount;
if (total > 0) {
const parts: string[] = [];
if (maintenanceCount > 0) {
parts.push(`${maintenanceCount} maintenance item${maintenanceCount > 1 ? 's' : ''}`);
}
if (documentCount > 0) {
parts.push(`${documentCount} document${documentCount > 1 ? 's' : ''}`);
}
toast(`You have ${parts.join(' and ')} requiring attention`, {
duration: 6000,
icon: '🔔',
});
}
hasShownToast.current = true;
}
}, [summary]);
return summary;
}

View File

@@ -0,0 +1,8 @@
/**
* @ai-summary Notifications feature exports
*/
export * from './api/notifications.api';
export * from './types/notifications.types';
export * from './hooks/useLoginNotifications';
export * from './components/EmailNotificationToggle';

View File

@@ -0,0 +1,36 @@
/**
* @ai-summary Type definitions for notifications feature
* @ai-context Supports maintenance due/overdue and document expiring/expired notifications
*/
export interface NotificationSummary {
maintenanceDueSoon: number;
maintenanceOverdue: number;
documentsExpiringSoon: number;
documentsExpired: number;
}
export interface DueMaintenanceItem {
scheduleId: string;
vehicleId: string;
vehicleName: string;
category: string;
subtypes: string[];
dueDate?: string;
dueMileage?: number;
isDueSoon: boolean;
isOverdue: boolean;
emailNotifications: boolean;
}
export interface ExpiringDocument {
documentId: string;
vehicleId: string;
vehicleName: string;
documentType: string;
title: string;
expirationDate: string;
isExpiringSoon: boolean;
isExpired: boolean;
emailNotifications: boolean;
}