![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/components/payments/ |
'use client';
import React, { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import {
Bell,
CheckCircle,
AlertCircle,
Clock,
DollarSign,
XCircle,
Eye,
Download
} from 'lucide-react';
interface PaymentNotification {
id: string;
type: 'payment_success' | 'payment_failed' | 'payment_pending' | 'escrow_released' | 'invoice_due';
title: string;
message: string;
amount?: number;
currency?: string;
caseId?: string;
caseTitle?: string;
createdAt: string;
read: boolean;
actionUrl?: string;
}
interface PaymentNotificationsProps {
userId: string;
userRole: string;
}
const PaymentNotifications: React.FC<PaymentNotificationsProps> = ({ userId, userRole }) => {
const [notifications, setNotifications] = useState<PaymentNotification[]>([]);
const [loading, setLoading] = useState(true);
const [unreadCount, setUnreadCount] = useState(0);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
fetchNotifications();
// Set up real-time updates
const interval = setInterval(fetchNotifications, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, [userId]);
const fetchNotifications = async () => {
try {
const response = await fetch('/api/user/payment-notifications');
if (response.ok) {
const data = await response.json();
setNotifications(data.notifications || []);
setUnreadCount(data.notifications?.filter((n: PaymentNotification) => !n.read).length || 0);
}
} catch (error) {
console.error('Error fetching notifications:', error);
} finally {
setLoading(false);
}
};
const markAsRead = async (notificationId: string) => {
try {
const response = await fetch(`/api/user/payment-notifications/${notificationId}/read`, {
method: 'POST'
});
if (response.ok) {
setNotifications(prev =>
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
}
} catch (error) {
console.error('Error marking notification as read:', error);
}
};
const markAllAsRead = async () => {
try {
const response = await fetch('/api/user/payment-notifications/read-all', {
method: 'POST'
});
if (response.ok) {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
}
} catch (error) {
console.error('Error marking all notifications as read:', error);
}
};
const getNotificationIcon = (type: string) => {
switch (type) {
case 'payment_success':
return <CheckCircle className="h-5 w-5 text-green-500" />;
case 'payment_failed':
return <XCircle className="h-5 w-5 text-red-500" />;
case 'payment_pending':
return <Clock className="h-5 w-5 text-yellow-500" />;
case 'escrow_released':
return <DollarSign className="h-5 w-5 text-blue-500" />;
case 'invoice_due':
return <AlertCircle className="h-5 w-5 text-orange-500" />;
default:
return <Bell className="h-5 w-5 text-gray-500" />;
}
};
const getNotificationColor = (type: string) => {
switch (type) {
case 'payment_success':
return 'border-l-green-500 bg-green-50';
case 'payment_failed':
return 'border-l-red-500 bg-red-50';
case 'payment_pending':
return 'border-l-yellow-500 bg-yellow-50';
case 'escrow_released':
return 'border-l-blue-500 bg-blue-50';
case 'invoice_due':
return 'border-l-orange-500 bg-orange-50';
default:
return 'border-l-gray-500 bg-gray-50';
}
};
const formatCurrency = (amount: number, currency: string = 'CAD') => {
return new Intl.NumberFormat('en-CA', {
style: 'currency',
currency: currency
}).format(amount);
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffInHours < 1) {
return 'Just now';
} else if (diffInHours < 24) {
return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`;
} else {
return date.toLocaleDateString('en-CA', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
};
const handleNotificationClick = (notification: PaymentNotification) => {
if (!notification.read) {
markAsRead(notification.id);
}
if (notification.actionUrl) {
window.location.href = notification.actionUrl;
}
};
const displayedNotifications = showAll ? notifications : notifications.slice(0, 5);
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading notifications...</span>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div className="flex items-center">
<Bell className="h-6 w-6 text-gray-600 mr-2" />
<h2 className="text-2xl font-bold text-gray-900">Payment Notifications</h2>
{unreadCount > 0 && (
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
{unreadCount} new
</span>
)}
</div>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className="text-sm text-blue-600 hover:text-blue-800"
>
Mark all as read
</button>
)}
</div>
{/* Notifications List */}
<div className="space-y-3">
{displayedNotifications.map((notification) => (
<div
key={notification.id}
className={`border-l-4 p-4 rounded-r-lg ${getNotificationColor(notification.type)} ${
!notification.read ? 'ring-2 ring-blue-200' : ''
}`}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
<div className="flex-shrink-0 mt-0.5">
{getNotificationIcon(notification.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-900">
{notification.title}
</p>
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500">
{formatDate(notification.createdAt)}
</span>
{!notification.read && (
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
)}
</div>
</div>
<p className="text-sm text-gray-600 mt-1">
{notification.message}
</p>
{notification.amount && (
<p className="text-sm font-medium text-gray-900 mt-1">
{formatCurrency(notification.amount, notification.currency)}
</p>
)}
{notification.caseTitle && (
<p className="text-xs text-gray-500 mt-1">
Case: {notification.caseTitle}
</p>
)}
</div>
</div>
<div className="flex items-center space-x-2 ml-4">
{notification.actionUrl && (
<button
onClick={() => handleNotificationClick(notification)}
className="text-blue-600 hover:text-blue-800"
>
<Eye className="h-4 w-4" />
</button>
)}
{!notification.read && (
<button
onClick={() => markAsRead(notification.id)}
className="text-gray-400 hover:text-gray-600"
>
<CheckCircle className="h-4 w-4" />
</button>
)}
</div>
</div>
</div>
))}
</div>
{/* Show More/Less Button */}
{notifications.length > 5 && (
<div className="text-center">
<button
onClick={() => setShowAll(!showAll)}
className="text-sm text-blue-600 hover:text-blue-800"
>
{showAll ? 'Show less' : `Show ${notifications.length - 5} more`}
</button>
</div>
)}
{/* Empty State */}
{notifications.length === 0 && (
<div className="text-center py-12">
<Bell className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No notifications</h3>
<p className="mt-1 text-sm text-gray-500">
You're all caught up! New payment notifications will appear here.
</p>
</div>
)}
{/* Notification Types Summary */}
{notifications.length > 0 && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Notification Summary</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{[
{ type: 'payment_success', label: 'Successful', color: 'text-green-600' },
{ type: 'payment_failed', label: 'Failed', color: 'text-red-600' },
{ type: 'payment_pending', label: 'Pending', color: 'text-yellow-600' },
{ type: 'escrow_released', label: 'Escrow', color: 'text-blue-600' },
{ type: 'invoice_due', label: 'Due', color: 'text-orange-600' }
].map(({ type, label, color }) => {
const count = notifications.filter(n => n.type === type).length;
return (
<div key={type} className="text-center">
<div className={`text-2xl font-bold ${color}`}>{count}</div>
<div className="text-sm text-gray-500">{label}</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
};
export default PaymentNotifications;