new version frontend-webpage

This commit is contained in:
José Salazar
2025-11-21 09:13:05 -05:00
parent 23dfba35cc
commit de1cc96ba0
56 changed files with 7736 additions and 3367 deletions

View File

@@ -1,4 +1,3 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
@@ -16,10 +15,12 @@ import {
AlertCircle,
CheckCircle,
ArrowRight,
MoreVertical
MoreVertical,
CheckSquare,
Package
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { formatDistanceToNow } from "date-fns";
import { formatDistanceToNow, format, isToday, isYesterday, isThisWeek, startOfDay } from "date-fns";
const iconMap = {
calendar: Calendar,
@@ -41,6 +42,7 @@ const colorMap = {
export default function NotificationPanel({ isOpen, onClose }) {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [activeFilter, setActiveFilter] = useState('all');
const { data: user } = useQuery({
queryKey: ['current-user-notifications'],
@@ -126,15 +128,96 @@ export default function NotificationPanel({ isOpen, onClose }) {
},
});
const newNotifications = notifications.filter(n => !n.is_read);
const olderNotifications = notifications.filter(n => n.is_read);
// Categorize by type
const categorizeByType = (notif) => {
const type = notif.activity_type || '';
const title = (notif.title || '').toLowerCase();
if (type.includes('message') || title.includes('message') || title.includes('comment') || title.includes('mentioned')) {
return 'mentions';
} else if (type.includes('staff_assigned') || type.includes('user') || title.includes('invited') || title.includes('followed')) {
return 'invites';
} else {
return 'all';
}
};
// Filter notifications based on active filter
const filteredNotifications = notifications.filter(notif => {
if (activeFilter === 'all') return true;
return categorizeByType(notif) === activeFilter;
});
// Group by day
const groupByDay = (notifList) => {
const groups = {
today: [],
yesterday: [],
thisWeek: [],
older: []
};
notifList.forEach(notif => {
const date = new Date(notif.created_date);
if (isToday(date)) {
groups.today.push(notif);
} else if (isYesterday(date)) {
groups.yesterday.push(notif);
} else if (isThisWeek(date)) {
groups.thisWeek.push(notif);
} else {
groups.older.push(notif);
}
});
return groups;
};
const groupedNotifications = groupByDay(filteredNotifications);
// Count by type
const allCount = notifications.length;
const mentionsCount = notifications.filter(n => categorizeByType(n) === 'mentions').length;
const invitesCount = notifications.filter(n => categorizeByType(n) === 'invites').length;
const handleAction = (notification) => {
if (notification.action_link) {
navigate(createPageUrl(notification.action_link));
// Mark as read when clicking
if (!notification.is_read) {
markAsReadMutation.mutate({ id: notification.id });
onClose();
}
const entityType = notification.related_entity_type;
const entityId = notification.related_entity_id;
const activityType = notification.activity_type || '';
// Route based on entity type
if (entityType === 'event' || activityType.includes('event') || activityType.includes('order')) {
if (entityId) {
navigate(createPageUrl(`EventDetail?id=${entityId}`));
} else {
navigate(createPageUrl('Events'));
}
} else if (entityType === 'task' || activityType.includes('task')) {
navigate(createPageUrl('TaskBoard'));
} else if (entityType === 'invoice' || activityType.includes('invoice')) {
if (entityId) {
navigate(createPageUrl(`Invoices?id=${entityId}`));
} else {
navigate(createPageUrl('Invoices'));
}
} else if (entityType === 'staff' || activityType.includes('staff')) {
if (entityId) {
navigate(createPageUrl(`EditStaff?id=${entityId}`));
} else {
navigate(createPageUrl('StaffDirectory'));
}
} else if (entityType === 'message' || activityType.includes('message')) {
navigate(createPageUrl('Messages'));
} else if (notification.action_link) {
navigate(createPageUrl(notification.action_link));
}
onClose();
};
return (
@@ -159,133 +242,376 @@ export default function NotificationPanel({ isOpen, onClose }) {
className="fixed right-0 top-0 h-full w-full sm:w-[440px] bg-white shadow-2xl z-50 flex flex-col"
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-200">
<div className="flex items-center gap-3">
<Bell className="w-6 h-6 text-[#1C323E]" />
<h2 className="text-xl font-bold text-[#1C323E]">Notifications</h2>
</div>
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-pink-500 to-purple-500 flex items-center justify-center text-white font-bold">
{user?.full_name?.split(' ').map(n => n[0]).join('').slice(0, 2) || 'U'}
<div className="border-b border-slate-200">
<div className="flex items-center justify-between p-6">
<div className="flex items-center gap-3">
<Bell className="w-6 h-6 text-[#1C323E]" />
<h2 className="text-xl font-bold text-[#1C323E]">Notifications</h2>
</div>
<Button variant="ghost" size="icon" onClick={onClose}>
<MoreVertical className="w-5 h-5" />
</Button>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="w-5 h-5" />
</Button>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="w-5 h-5" />
</Button>
</div>
</div>
{/* Filter Tabs */}
<div className="flex items-center gap-2 px-6 pb-4">
<button
onClick={() => setActiveFilter('all')}
className={`px-4 py-2 rounded-lg text-sm font-semibold transition-all ${
activeFilter === 'all'
? 'bg-blue-50 text-blue-600'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
View all <span className="ml-1">{allCount}</span>
</button>
<button
onClick={() => setActiveFilter('mentions')}
className={`px-4 py-2 rounded-lg text-sm font-semibold transition-all ${
activeFilter === 'mentions'
? 'bg-blue-50 text-blue-600'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
Mentions <span className="ml-1">{mentionsCount}</span>
</button>
<button
onClick={() => setActiveFilter('invites')}
className={`px-4 py-2 rounded-lg text-sm font-semibold transition-all ${
activeFilter === 'invites'
? 'bg-blue-50 text-blue-600'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
Invites <span className="ml-1">{invitesCount}</span>
</button>
</div>
</div>
{/* Notifications List */}
<div className="flex-1 overflow-y-auto">
{newNotifications.length > 0 && (
<div className="p-6">
<h3 className="text-sm font-bold text-slate-900 mb-4">New</h3>
<div className="space-y-4">
{newNotifications.map((notification) => {
const Icon = iconMap[notification.icon_type] || AlertCircle;
const colorClass = colorMap[notification.icon_color] || colorMap.blue;
return (
<div key={notification.id} className="relative">
<div className="absolute left-0 top-0 w-2 h-2 bg-red-500 rounded-full" />
<div className="flex gap-4 pl-4">
<div className={`w-12 h-12 rounded-full ${colorClass} flex items-center justify-center flex-shrink-0`}>
<Icon className="w-6 h-6" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-1">
<h4 className="font-semibold text-slate-900">{notification.title}</h4>
<span className="text-xs text-slate-500 whitespace-nowrap ml-2">
{formatDistanceToNow(new Date(notification.created_date), { addSuffix: true })}
</span>
</div>
<p className="text-sm text-slate-600 mb-3">{notification.description}</p>
<div className="flex items-center gap-4">
{notification.action_link && (
<button
onClick={() => handleAction(notification)}
className="text-blue-600 hover:text-blue-700 text-sm font-medium flex items-center gap-1"
>
{notification.action_label || 'View'}
<ArrowRight className="w-4 h-4" />
</button>
)}
<button
onClick={() => markAsReadMutation.mutate({ id: notification.id })}
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
Mark as Read
</button>
<button
onClick={() => deleteMutation.mutate({ id: notification.id })}
className="text-red-600 hover:text-red-700 text-sm font-medium"
>
Delete
</button>
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{olderNotifications.length > 0 && (
<div className="p-6 border-t border-slate-100">
<h3 className="text-sm font-bold text-slate-900 mb-4">Older</h3>
<div className="space-y-4">
{olderNotifications.map((notification) => {
const Icon = iconMap[notification.icon_type] || AlertCircle;
const colorClass = colorMap[notification.icon_color] || colorMap.blue;
return (
<div key={notification.id} className="flex gap-4 opacity-70 hover:opacity-100 transition-opacity">
<div className={`w-12 h-12 rounded-full ${colorClass} flex items-center justify-center flex-shrink-0`}>
<Icon className="w-6 h-6" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-1">
<h4 className="font-semibold text-slate-900">{notification.title}</h4>
<span className="text-xs text-slate-500 whitespace-nowrap ml-2">
{formatDistanceToNow(new Date(notification.created_date), { addSuffix: true })}
</span>
</div>
<p className="text-sm text-slate-600 mb-3">{notification.description}</p>
<div className="flex items-center gap-4">
{notification.action_link && (
<button
onClick={() => handleAction(notification)}
className="text-blue-600 hover:text-blue-700 text-sm font-medium flex items-center gap-1"
>
{notification.action_label || 'View'}
<ArrowRight className="w-4 h-4" />
</button>
)}
<button
onClick={() => deleteMutation.mutate({ id: notification.id })}
className="text-red-600 hover:text-red-700 text-sm font-medium"
>
Delete
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{notifications.length === 0 && (
{filteredNotifications.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-8">
<Bell className="w-16 h-16 text-slate-300 mb-4" />
<h3 className="text-lg font-semibold text-slate-900 mb-2">No notifications</h3>
<p className="text-slate-600">You're all caught up!</p>
</div>
) : (
<>
{/* TODAY */}
{groupedNotifications.today.length > 0 && (
<div className="px-6 py-4">
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-4">TODAY</h3>
<div className="space-y-4">
{groupedNotifications.today.map((notification) => {
const Icon = iconMap[notification.icon_type] || AlertCircle;
const isUnread = !notification.is_read;
const notifDate = new Date(notification.created_date);
return (
<div key={notification.id} className="flex gap-3 group hover:bg-slate-50 p-3 rounded-lg transition-all -mx-3 relative">
{isUnread && (
<div className="flex-shrink-0 w-2 h-2 bg-blue-600 rounded-full mt-2" />
)}
<div
onClick={() => handleAction(notification)}
className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 cursor-pointer ${
notification.icon_color === 'blue' ? 'bg-blue-100' :
notification.icon_color === 'green' ? 'bg-green-100' :
notification.icon_color === 'red' ? 'bg-red-100' :
notification.icon_color === 'purple' ? 'bg-purple-100' :
'bg-slate-100'
}`}>
<Icon className={`w-5 h-5 ${
notification.icon_color === 'blue' ? 'text-blue-600' :
notification.icon_color === 'green' ? 'text-green-600' :
notification.icon_color === 'red' ? 'text-red-600' :
notification.icon_color === 'purple' ? 'text-purple-600' :
'text-slate-600'
}`} />
</div>
<div
className="flex-1 min-w-0 cursor-pointer"
onClick={() => handleAction(notification)}
>
<div className="flex items-start justify-between mb-1">
<p className="text-sm text-slate-900">
<span className="font-semibold">{notification.title}</span> {notification.description}
</p>
<span className="text-xs text-slate-500 whitespace-nowrap ml-3">
{formatDistanceToNow(notifDate, { addSuffix: true })}
</span>
</div>
<div className="flex items-center gap-3 text-xs">
{notification.action_link && (
<span className="font-medium text-blue-600">{notification.action_label}</span>
)}
<span className="text-slate-500">{format(notifDate, 'h:mm a')}</span>
{isUnread && (
<button
onClick={(e) => {
e.stopPropagation();
markAsReadMutation.mutate({ id: notification.id });
}}
className="text-blue-600 hover:text-blue-700 font-medium"
>
Mark as Read
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
deleteMutation.mutate({ id: notification.id });
}}
className="text-red-600 hover:text-red-700 font-medium"
>
Delete
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* YESTERDAY */}
{groupedNotifications.yesterday.length > 0 && (
<div className="px-6 py-4 border-t border-slate-100">
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-4">YESTERDAY</h3>
<div className="space-y-4">
{groupedNotifications.yesterday.map((notification) => {
const Icon = iconMap[notification.icon_type] || AlertCircle;
const isUnread = !notification.is_read;
const notifDate = new Date(notification.created_date);
return (
<div key={notification.id} className="flex gap-3 group hover:bg-slate-50 p-3 rounded-lg transition-all -mx-3 relative">
{isUnread && (
<div className="flex-shrink-0 w-2 h-2 bg-blue-600 rounded-full mt-2" />
)}
<div
onClick={() => handleAction(notification)}
className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 cursor-pointer ${
notification.icon_color === 'blue' ? 'bg-blue-100' :
notification.icon_color === 'green' ? 'bg-green-100' :
notification.icon_color === 'red' ? 'bg-red-100' :
notification.icon_color === 'purple' ? 'bg-purple-100' :
'bg-slate-100'
}`}>
<Icon className={`w-5 h-5 ${
notification.icon_color === 'blue' ? 'text-blue-600' :
notification.icon_color === 'green' ? 'text-green-600' :
notification.icon_color === 'red' ? 'text-red-600' :
notification.icon_color === 'purple' ? 'text-purple-600' :
'text-slate-600'
}`} />
</div>
<div
className="flex-1 min-w-0"
onClick={() => notification.action_link && handleAction(notification)}
>
<div className={`flex items-start justify-between mb-1 ${notification.action_link ? 'cursor-pointer' : ''}`}>
<p className="text-sm text-slate-900">
<span className="font-semibold">{notification.title}</span> {notification.description}
</p>
<span className="text-xs text-slate-500 whitespace-nowrap ml-3">
{formatDistanceToNow(notifDate, { addSuffix: true })}
</span>
</div>
<div className="flex items-center gap-3 text-xs">
{notification.action_link && (
<span className="font-medium text-blue-600">{notification.action_label}</span>
)}
<span className="text-slate-500">{format(notifDate, 'MM/dd/yy h:mm a')}</span>
{isUnread && (
<button
onClick={(e) => {
e.stopPropagation();
markAsReadMutation.mutate({ id: notification.id });
}}
className="text-blue-600 hover:text-blue-700 font-medium"
>
Mark as Read
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
deleteMutation.mutate({ id: notification.id });
}}
className="text-red-600 hover:text-red-700 font-medium"
>
Delete
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* THIS WEEK */}
{groupedNotifications.thisWeek.length > 0 && (
<div className="px-6 py-4 border-t border-slate-100">
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-4">THIS WEEK</h3>
<div className="space-y-4">
{groupedNotifications.thisWeek.map((notification) => {
const Icon = iconMap[notification.icon_type] || AlertCircle;
const isUnread = !notification.is_read;
const notifDate = new Date(notification.created_date);
return (
<div key={notification.id} className="flex gap-3 group hover:bg-slate-50 p-3 rounded-lg transition-all -mx-3 opacity-80 hover:opacity-100">
{isUnread && (
<div className="flex-shrink-0 w-2 h-2 bg-blue-600 rounded-full mt-2" />
)}
<div
onClick={() => handleAction(notification)}
className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 cursor-pointer ${
notification.icon_color === 'blue' ? 'bg-blue-100' :
notification.icon_color === 'green' ? 'bg-green-100' :
notification.icon_color === 'red' ? 'bg-red-100' :
notification.icon_color === 'purple' ? 'bg-purple-100' :
'bg-slate-100'
}`}>
<Icon className={`w-5 h-5 ${
notification.icon_color === 'blue' ? 'text-blue-600' :
notification.icon_color === 'green' ? 'text-green-600' :
notification.icon_color === 'red' ? 'text-red-600' :
notification.icon_color === 'purple' ? 'text-purple-600' :
'text-slate-600'
}`} />
</div>
<div
className="flex-1 min-w-0"
onClick={() => notification.action_link && handleAction(notification)}
>
<div className={`flex items-start justify-between mb-1 ${notification.action_link ? 'cursor-pointer' : ''}`}>
<p className="text-sm text-slate-900">
<span className="font-semibold">{notification.title}</span> {notification.description}
</p>
<span className="text-xs text-slate-500 whitespace-nowrap ml-3">
{formatDistanceToNow(notifDate, { addSuffix: true })}
</span>
</div>
<div className="flex items-center gap-3 text-xs">
{notification.action_link && (
<span className="font-medium text-blue-600">{notification.action_label}</span>
)}
<span className="text-slate-500">{format(notifDate, 'MM/dd/yy h:mm a')}</span>
{isUnread && (
<button
onClick={(e) => {
e.stopPropagation();
markAsReadMutation.mutate({ id: notification.id });
}}
className="text-blue-600 hover:text-blue-700 font-medium"
>
Mark as Read
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
deleteMutation.mutate({ id: notification.id });
}}
className="text-red-600 hover:text-red-700 font-medium"
>
Delete
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* OLDER */}
{groupedNotifications.older.length > 0 && (
<div className="px-6 py-4 border-t border-slate-100">
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-4">OLDER</h3>
<div className="space-y-4">
{groupedNotifications.older.map((notification) => {
const Icon = iconMap[notification.icon_type] || AlertCircle;
const isUnread = !notification.is_read;
const notifDate = new Date(notification.created_date);
return (
<div key={notification.id} className="flex gap-3 group hover:bg-slate-50 p-3 rounded-lg transition-all -mx-3 opacity-70 hover:opacity-100">
{isUnread && (
<div className="flex-shrink-0 w-2 h-2 bg-blue-600 rounded-full mt-2" />
)}
<div
onClick={() => handleAction(notification)}
className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 cursor-pointer ${
notification.icon_color === 'blue' ? 'bg-blue-100' :
notification.icon_color === 'green' ? 'bg-green-100' :
notification.icon_color === 'red' ? 'bg-red-100' :
notification.icon_color === 'purple' ? 'bg-purple-100' :
'bg-slate-100'
}`}>
<Icon className={`w-5 h-5 ${
notification.icon_color === 'blue' ? 'text-blue-600' :
notification.icon_color === 'green' ? 'text-green-600' :
notification.icon_color === 'red' ? 'text-red-600' :
notification.icon_color === 'purple' ? 'text-purple-600' :
'text-slate-600'
}`} />
</div>
<div
className="flex-1 min-w-0"
onClick={() => notification.action_link && handleAction(notification)}
>
<div className={`flex items-start justify-between mb-1 ${notification.action_link ? 'cursor-pointer' : ''}`}>
<p className="text-sm text-slate-900">
<span className="font-semibold">{notification.title}</span> {notification.description}
</p>
<span className="text-xs text-slate-500 whitespace-nowrap ml-3">
{formatDistanceToNow(notifDate, { addSuffix: true })}
</span>
</div>
<div className="flex items-center gap-3 text-xs">
{notification.action_link && (
<span className="font-medium text-blue-600">{notification.action_label}</span>
)}
<span className="text-slate-500">{format(notifDate, 'MM/dd/yy h:mm a')}</span>
{isUnread && (
<button
onClick={(e) => {
e.stopPropagation();
markAsReadMutation.mutate({ id: notification.id });
}}
className="text-blue-600 hover:text-blue-700 font-medium"
>
Mark as Read
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
deleteMutation.mutate({ id: notification.id });
}}
className="text-red-600 hover:text-red-700 font-medium"
>
Delete
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
</>
)}
</div>
</motion.div>
@@ -293,4 +619,4 @@ export default function NotificationPanel({ isOpen, onClose }) {
)}
</AnimatePresence>
);
}
}