279 lines
13 KiB
TypeScript
279 lines
13 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { Bell } from 'lucide-react';
|
|
import { useNotifications } from '../hooks/useNotifications';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { FaCheck, FaTimes, FaBell } from 'react-icons/fa';
|
|
|
|
const stripHtml = (html: string): string => {
|
|
if (!html) return '';
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
return (doc.body.textContent || '').replace(/\uFEFF/g, '').replace(/\s+/g, ' ').trim();
|
|
};
|
|
|
|
const NotificationBell: React.FC = () => {
|
|
const { notifications, unreadCount, markAsRead, markAllAsRead, loading } = useNotifications();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [markingAll, setMarkingAll] = useState(false);
|
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
const navigate = useNavigate();
|
|
|
|
// If notifications are not available (empty array and not loading),
|
|
// the bell will still show but with 0 count - this is fine
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [isOpen]);
|
|
|
|
const handleMarkAllAsRead = async () => {
|
|
setMarkingAll(true);
|
|
try {
|
|
await markAllAsRead();
|
|
} catch (error) {
|
|
console.warn('[NotificationBell] Could not mark all as read:', error);
|
|
} finally {
|
|
setMarkingAll(false);
|
|
}
|
|
};
|
|
|
|
const handleNotificationClick = async (notification: any) => {
|
|
console.log('[NotificationBell] Clicked notification:', notification);
|
|
console.log('[NotificationBell] document_type:', notification.document_type);
|
|
console.log('[NotificationBell] document_name:', notification.document_name);
|
|
|
|
// Try to mark as read, but don't block navigation if it fails
|
|
if (!notification.read) {
|
|
try {
|
|
await markAsRead(notification.name);
|
|
} catch (error) {
|
|
console.warn('[NotificationBell] Could not mark as read (permission issue):', error);
|
|
// Continue anyway - navigate to document
|
|
}
|
|
}
|
|
|
|
// Navigate based on document type
|
|
if (notification.document_type && notification.document_name) {
|
|
const docType = notification.document_type;
|
|
const docName = notification.document_name;
|
|
|
|
// Normalize document type (handle both spaces and underscores)
|
|
const normalizedType = docType.replace(/_/g, ' ').trim();
|
|
|
|
console.log('[NotificationBell] Normalized type:', normalizedType);
|
|
console.log('[NotificationBell] Document name:', docName);
|
|
|
|
// Map document types to routes
|
|
if (normalizedType === 'Asset Maintenance Log' || normalizedType === 'Asset Maintenance') {
|
|
console.log('[NotificationBell] Navigating to maintenance:', `/maintenance/${docName}`);
|
|
navigate(`/maintenance/${docName}`);
|
|
} else if (normalizedType === 'Work Order' || normalizedType === 'Asset Repair') {
|
|
console.log('[NotificationBell] Navigating to work order:', `/work-orders/${docName}`);
|
|
navigate(`/work-orders/${docName}`);
|
|
} else if (normalizedType === 'Asset') {
|
|
console.log('[NotificationBell] Navigating to asset:', `/assets/${docName}`);
|
|
navigate(`/assets/${docName}`);
|
|
} else if (normalizedType === 'PM Schedule Generator' || normalizedType === 'PM Schedule') {
|
|
console.log('[NotificationBell] Navigating to PPM planner:', `/ppm-planner/${docName}`);
|
|
navigate(`/ppm-planner/${docName}`);
|
|
} else if (normalizedType === 'PPM') {
|
|
console.log('[NotificationBell] Navigating to PPM:', `/ppm/${docName}`);
|
|
navigate(`/ppm/${docName}`);
|
|
} else if (normalizedType === 'Item') {
|
|
console.log('[NotificationBell] Navigating to inventory:', `/inventory/${docName}`);
|
|
navigate(`/inventory/${docName}`);
|
|
} else if (normalizedType === 'Inspection') {
|
|
console.log('[NotificationBell] Navigating to inspection:', `/inspections/${docName}`);
|
|
navigate(`/inspections/${docName}`);
|
|
} else if (normalizedType === 'Issue') {
|
|
console.log('[NotificationBell] Navigating to issue', `/issues/${docName}`);
|
|
navigate(`/support/${docName}`);
|
|
}else if (normalizedType === 'SFDA Entries') {
|
|
console.log('[NotificationBell] Navigating to SFDA Entries', `/sfda-entries/${docName}`);
|
|
navigate(`/sfda-entries/${docName}`);
|
|
}
|
|
else {
|
|
// Fallback: Try to open in Frappe if route not found
|
|
console.warn(`[NotificationBell] Unknown document type: ${docType}, opening in Frappe`);
|
|
const frappeRoute = docType.toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-');
|
|
window.open(`/app/${frappeRoute}/${docName}`, '_blank');
|
|
}
|
|
} else {
|
|
console.warn('[NotificationBell] No document_type or document_name found:', {
|
|
document_type: notification.document_type,
|
|
document_name: notification.document_name,
|
|
notification
|
|
});
|
|
}
|
|
|
|
setIsOpen(false);
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
return date.toLocaleDateString();
|
|
};
|
|
|
|
const unreadNotifications = notifications.filter(n => !n.read);
|
|
const readNotifications = notifications.filter(n => n.read).slice(0, 10);
|
|
|
|
return (
|
|
<div className="relative" ref={panelRef}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="relative p-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
title="Notifications"
|
|
>
|
|
<Bell size={20} />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute top-0 right-0 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">
|
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute right-0 mt-2 w-80 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 z-[9999] max-h-96 overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<h3 className="font-semibold text-gray-800 dark:text-white flex items-center gap-2">
|
|
<FaBell />
|
|
Notifications
|
|
{unreadCount > 0 && (
|
|
<span className="text-xs bg-red-500 text-white px-2 py-0.5 rounded-full">
|
|
{unreadCount} new
|
|
</span>
|
|
)}
|
|
</h3>
|
|
{unreadCount > 0 && (
|
|
<button
|
|
onClick={handleMarkAllAsRead}
|
|
disabled={markingAll}
|
|
className={`text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline ${
|
|
markingAll ? 'opacity-50 cursor-not-allowed' : ''
|
|
}`}
|
|
>
|
|
{markingAll ? 'Marking...' : 'Mark all read'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Notifications List */}
|
|
<div className="overflow-y-auto flex-1">
|
|
{notifications.length === 0 ? (
|
|
<div className="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
<FaBell className="mx-auto text-3xl mb-2 opacity-50" />
|
|
<p>No notifications</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{unreadNotifications.length > 0 && (
|
|
<div className="p-2">
|
|
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 px-2 mb-1">
|
|
NEW
|
|
</div>
|
|
{unreadNotifications.map(notif => (
|
|
<div
|
|
key={notif.name}
|
|
onClick={() => handleNotificationClick(notif)}
|
|
className="p-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-l-4 border-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
{stripHtml((notif as any).subject || '') || notif.document_type || 'Notification'}
|
|
</p>
|
|
{(notif as any).email_content && (
|
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
|
|
{stripHtml((notif as any).email_content || '')}
|
|
</p>
|
|
)}
|
|
{/* <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
{(notif as any).subject || notif.document_type || 'Notification'}
|
|
</p>
|
|
{(notif as any).email_content && (
|
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
|
|
{(notif as any).email_content}
|
|
</p>
|
|
)} */}
|
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
|
{formatDate(notif.creation)}
|
|
</p>
|
|
</div>
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 mt-1"></div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{readNotifications.length > 0 && (
|
|
<div className="p-2 border-t border-gray-200 dark:border-gray-700">
|
|
{unreadNotifications.length > 0 && (
|
|
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 px-2 mb-1">
|
|
EARLIER
|
|
</div>
|
|
)}
|
|
{readNotifications.map(notif => (
|
|
<div
|
|
key={notif.name}
|
|
onClick={() => handleNotificationClick(notif)}
|
|
className="p-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
|
{stripHtml((notif as any).subject || '') || notif.document_type || 'Notification'}
|
|
</p>
|
|
{(notif as any).email_content && (
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">
|
|
{stripHtml((notif as any).email_content || '')}
|
|
</p>
|
|
)}
|
|
{/* <p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
|
{(notif as any).subject || notif.document_type || 'Notification'}
|
|
</p>
|
|
{(notif as any).email_content && (
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">
|
|
{(notif as any).email_content}
|
|
</p>
|
|
)} */}
|
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
{formatDate(notif.creation)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default NotificationBell; |