Notification updates
This commit is contained in:
23
frontend/src/features/notifications/api/notifications.api.ts
Normal file
23
frontend/src/features/notifications/api/notifications.api.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
8
frontend/src/features/notifications/index.ts
Normal file
8
frontend/src/features/notifications/index.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user