import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Bell, X, Check, CheckCheck, Clock, FileText, Package, CreditCard, AlertCircle, Edit3 } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { supabase } from '../supabase/client'; import { useAuth } from '../contexts/AuthContext'; interface Notification { id: string; type: 'step_completed' | 'plan_created' | 'plan_updated' | 'inventory_in' | 'payment_pending' | 'system'; title: string; content: string; is_read: boolean; created_at: string; plan_id?: string | null; sender_company_name?: string; } const typeConfig: Record = { step_completed: { icon: Check, color: 'text-emerald-600', bgColor: 'bg-emerald-50' }, plan_created: { icon: FileText, color: 'text-blue-600', bgColor: 'bg-blue-50' }, plan_updated: { icon: AlertCircle, color: 'text-amber-600', bgColor: 'bg-amber-50' }, plan_confirmed: { icon: Check, color: 'text-emerald-600', bgColor: 'bg-emerald-50' }, plan_changed: { icon: Edit3, color: 'text-orange-600', bgColor: 'bg-orange-50' }, plan_cancelled: { icon: X, color: 'text-red-600', bgColor: 'bg-red-50' }, plan_rejected: { icon: AlertCircle, color: 'text-rose-600', bgColor: 'bg-rose-50' }, inventory_in: { icon: Package, color: 'text-indigo-600', bgColor: 'bg-indigo-50' }, payment_pending: { icon: CreditCard, color: 'text-purple-600', bgColor: 'bg-purple-50' }, system: { icon: Bell, color: 'text-gray-600', bgColor: 'bg-gray-50' } }; interface NotificationCenterProps { theme?: 'amber' | 'emerald' | 'purple' | 'blue' | 'violet'; } const themeConfig = { amber: { bg: 'bg-amber-100', hover: 'hover:bg-amber-200', icon: 'text-amber-700' }, emerald: { bg: 'bg-emerald-100', hover: 'hover:bg-emerald-200', icon: 'text-emerald-700' }, purple: { bg: 'bg-purple-100', hover: 'hover:bg-purple-200', icon: 'text-purple-700' }, blue: { bg: 'bg-blue-100', hover: 'hover:bg-blue-200', icon: 'text-blue-700' }, violet: { bg: 'bg-violet-100', hover: 'hover:bg-violet-200', icon: 'text-violet-500' }, }; export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps = {}) { const [isOpen, setIsOpen] = useState(false); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const { auth } = useAuth(); const navigate = useNavigate(); const colors = themeConfig[theme]; useEffect(() => { if (!auth.user) return; fetchNotifications(); const channel = subscribeToNotifications(); return () => { channel.unsubscribe(); }; }, [auth.user]); const fetchNotifications = async () => { const { data } = await supabase .from('notifications') .select('*, sender_company_id(name)') .eq('recipient_id', auth.user!.id) .order('created_at', { ascending: false }) .limit(20); if (data) { const formatted = data.map(n => ({ id: n.id, type: n.type as Notification['type'], title: n.title, content: n.content, is_read: n.is_read || false, created_at: n.created_at || '', plan_id: n.plan_id, sender_company_name: n.sender_company_id?.name })); setNotifications(formatted); setUnreadCount(formatted.filter(n => !n.is_read).length); } }; const subscribeToNotifications = () => { const channel = supabase.channel('notifications'); channel .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'notifications', filter: `recipient_id=eq.${auth.user!.id}` }, () => { fetchNotifications(); }) .subscribe((status) => { if (status === 'SUBSCRIBED') { console.log('Successfully subscribed to notifications'); } }); return channel; }; const markAsRead = async (id: string) => { await supabase .from('notifications') .update({ is_read: true, read_at: new Date().toISOString() }) .eq('id', id); setNotifications(prev => prev.map(n => n.id === id ? { ...n, is_read: true } : n )); setUnreadCount(prev => Math.max(0, prev - 1)); }; const markAllAsRead = async () => { await supabase .from('notifications') .update({ is_read: true, read_at: new Date().toISOString() }) .eq('recipient_id', auth.user!.id) .eq('is_read', false); setNotifications(prev => prev.map(n => ({ ...n, is_read: true }))); setUnreadCount(0); }; const formatTime = (date: string) => { const d = new Date(date); const now = new Date(); const diff = now.getTime() - d.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return '刚刚'; if (minutes < 60) return `${minutes}分钟前`; if (hours < 24) return `${hours}小时前`; if (days < 7) return `${days}天前`; return d.toLocaleDateString('zh-CN'); }; return ( <> {/* 通知按钮 */} setIsOpen(true)} className={`relative flex items-center justify-center w-10 h-10 rounded-xl ${colors.bg} ${colors.hover} transition-colors`} > {unreadCount > 0 && ( {unreadCount > 99 ? '99+' : unreadCount} )} {/* 通知弹窗 */} {isOpen && ( setIsOpen(false)} > e.stopPropagation()} > {/* 头部 */}

通知中心

{unreadCount > 0 && ( {unreadCount} 未读 )}
{unreadCount > 0 && ( )}
{/* 通知列表 */}
{notifications.length === 0 ? (

暂无通知

) : (
{notifications.map((notification, index) => { const config = typeConfig[notification.type] || typeConfig.system; const Icon = config.icon; const handleNotificationClick = async () => { await markAsRead(notification.id); // 如果有 plan_id,跳转到计划总览页面 if (notification.plan_id) { setIsOpen(false); // 根据用户角色跳转到对应页面 const role = auth.company?.role; if (role === 'purchaser') { navigate('/purchaser/plans'); } else if (role === 'textile') { navigate('/textile/plans'); } else if (role === 'washing') { navigate('/washing/plans'); } } }; return (

{notification.title}

{!notification.is_read && ( )}

{notification.content}

{formatTime(notification.created_at)} {notification.sender_company_name && ( · {notification.sender_company_name} )}
); })}
)}
)}
); }