diff --git a/src/App.tsx b/src/App.tsx index a0a0211..d630129 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import { Profilers } from "./components/pages/ProfilersEnhanced"; import { ProfilerBuilder } from "./components/pages/ProfilerBuilder"; import { ProfilerMaster } from "./components/pages/ProfilerMaster"; import { ProfilerPreview } from "./components/pages/ProfilerPreview"; +import { ProfilerApproval } from "./components/pages/ProfilerApproval"; import { LandingPages } from "./components/pages/LandingPagesNew"; import { HomeEditor } from "./components/pages/HomeEditor"; @@ -36,7 +37,7 @@ import { Leads } from "./components/pages/Leads"; import { Roles } from "./components/pages/Roles"; import { Analytics } from "./components/pages/Analytics"; import { Toaster } from "./components/ui/sonner"; -import { SESSION_CONFIG, AutoSaveData } from "./data/mockData"; +import { SESSION_CONFIG, AutoSaveData, mockNotifications, Notification, mockApprovalTasks, ApprovalTask } from "./data/mockData"; type Route = | "/login" @@ -58,6 +59,7 @@ type Route = | "/profilers" | "/profilers/new" | "/profilers/preview" + | "/profiler-approval" | "/landing-pages" | "/landing-pages/edit/home" | "/landing-pages/edit/services" @@ -90,6 +92,10 @@ export default function App() { // Auto-save state management const [autoSaveData, setAutoSaveData] = useState<{ [key: string]: any }>({}); + // Notification state management + const [notifications, setNotifications] = useState(mockNotifications); + const [approvalTasks, setApprovalTasks] = useState(mockApprovalTasks); + // Simulate authentication check useEffect(() => { const authToken = localStorage.getItem("klc_auth_token"); @@ -138,6 +144,103 @@ export default function App() { localStorage.removeItem(`autosave_${routeKey}`); }; + // Notification handlers + const handleMarkNotificationAsRead = (notificationId: string) => { + setNotifications(prev => + prev.map(n => n.id === notificationId ? { ...n, read: true } : n) + ); + }; + + const handleMarkAllNotificationsAsRead = () => { + setNotifications(prev => prev.map(n => ({ ...n, read: true }))); + }; + + const handleDeleteNotification = (notificationId: string) => { + setNotifications(prev => prev.filter(n => n.id !== notificationId)); + }; + + // Approval handlers + const handleApproveTask = (taskId: string, comment: string) => { + setApprovalTasks(prev => + prev.map(task => + task.id === taskId ? { ...task, status: 'approved' as const } : task + ) + ); + // Add success notification + const task = approvalTasks.find(t => t.id === taskId); + if (task) { + const newNotification: Notification = { + id: `notif_${Date.now()}`, + type: 'approval', + title: 'Profiler Approved', + message: `${task.entityName} has been successfully approved and published`, + timestamp: new Date().toISOString(), + read: false, + priority: 'medium', + relatedEntity: { + type: 'profiler', + id: task.entityId, + name: task.entityName + } + }; + setNotifications(prev => [newNotification, ...prev]); + } + }; + + const handleRejectTask = (taskId: string, reason: string) => { + setApprovalTasks(prev => + prev.map(task => + task.id === taskId ? { ...task, status: 'rejected' as const } : task + ) + ); + // Add notification + const task = approvalTasks.find(t => t.id === taskId); + if (task) { + const newNotification: Notification = { + id: `notif_${Date.now()}`, + type: 'approval', + title: 'Profiler Rejected', + message: `${task.entityName} has been rejected`, + timestamp: new Date().toISOString(), + read: false, + priority: 'high', + relatedEntity: { + type: 'profiler', + id: task.entityId, + name: task.entityName + } + }; + setNotifications(prev => [newNotification, ...prev]); + } + }; + + const handleRequestChangesTask = (taskId: string, reason: string) => { + setApprovalTasks(prev => + prev.map(task => + task.id === taskId ? { ...task, status: 'changes_requested' as const } : task + ) + ); + // Add notification + const task = approvalTasks.find(t => t.id === taskId); + if (task) { + const newNotification: Notification = { + id: `notif_${Date.now()}`, + type: 'approval', + title: 'Changes Requested', + message: `Changes have been requested for ${task.entityName}`, + timestamp: new Date().toISOString(), + read: false, + priority: 'high', + relatedEntity: { + type: 'profiler', + id: task.entityId, + name: task.entityName + } + }; + setNotifications(prev => [newNotification, ...prev]); + } + }; + const navigate = (route: Route) => { setCurrentRoute(route); }; @@ -179,7 +282,11 @@ export default function App() { user, formData: getAutoSavedData(), onAutoSave: handleAutoSave, - onClearAutoSave: clearAutoSavedData + onClearAutoSave: clearAutoSavedData, + notifications, + onMarkNotificationAsRead: handleMarkNotificationAsRead, + onMarkAllNotificationsAsRead: handleMarkAllNotificationsAsRead, + onDeleteNotification: handleDeleteNotification }; switch (currentRoute) { @@ -218,6 +325,21 @@ export default function App() { return ; case "/profilers/preview": return navigate("/dashboard")} />; + case "/profiler-approval": + // Get the approval task from sessionStorage + const taskData = sessionStorage.getItem('currentApprovalTask'); + const approvalTask = taskData ? JSON.parse(taskData) : approvalTasks[0]; + return ( + + ); case "/admin/profiler-master": return ; case "/landing-pages": diff --git a/src/NOTIFICATIONS_AND_APPROVAL_SYSTEM.md b/src/NOTIFICATIONS_AND_APPROVAL_SYSTEM.md new file mode 100644 index 0000000..1401406 --- /dev/null +++ b/src/NOTIFICATIONS_AND_APPROVAL_SYSTEM.md @@ -0,0 +1,262 @@ +# Notifications and Approval System Implementation + +## Overview +A comprehensive notification and approval workflow system has been implemented for the KLC Super Admin Control Panel. This system enables real-time notifications and a structured approval process for profilers and other content. + +## Components Created + +### 1. NotificationPanel Component (`/components/NotificationPanel.tsx`) +A slide-out panel accessible via the bell icon in the header. + +**Features:** +- Real-time notification badge showing unread count +- Filter tabs: All, Unread, Approvals +- Multiple notification types: + - **Approval**: Profiler/content approval requests and updates + - **Course Launch**: New course publications + - **Feature**: New system features + - **System**: Maintenance announcements +- Priority indicators (High, Medium, Low) +- Mark individual notifications as read +- Mark all as read +- Delete notifications +- Click to navigate to related content +- Timestamp display (smart formatting: "2h ago", "3d ago") +- Related entity tracking (profiler, course, programme, content) + +**Location in UI:** +Top-right header area, next to user avatar, visible on all pages + +### 2. ProfilerApproval Component (`/components/pages/ProfilerApproval.tsx`) +Dedicated page for reviewing and approving profilers in review status. + +**Features:** +- **Submission Information Display:** + - Submitter details (name, email, avatar) + - Submission timestamp + - Version number + - Priority level + +- **Review History:** + - Previous comments from reviewers + - Previous actions (approved, rejected, changes requested) + - Full audit trail with timestamps + +- **Profiler Details Tabs:** + - **Overview**: Question type, total questions, estimated time, scoring method + - **Sections**: Detailed section breakdown with question counts + - **Preview**: Link to interactive profiler preview + +- **Approval Actions:** + - **Approve**: Publish profiler with optional comment + - **Request Changes**: Specify required changes (sends to author) + - **Reject**: Reject with mandatory reason + +**Navigation:** +Dashboard → Approval Tasks table → Review button → ProfilerApproval page + +### 3. Dashboard Approval Tasks Section +New section added to Dashboard displaying pending approval tasks. + +**Features:** +- Shows only pending approval tasks +- Badge showing count of pending tasks +- Table display with columns: + - Item (name and description) + - Type (Profiler, Course, Content) + - Submitted By (name and email) + - Submitted (time ago) + - Priority (High/Medium/Low badge) + - Version number + - Review button (navigates to approval page) +- Real-time updates when tasks are approved/rejected + +**Location:** +Between Quick Actions and KPI Cards on Dashboard + +## Data Structure + +### Notification Interface +```typescript +interface Notification { + id: string; + type: 'approval' | 'course_launch' | 'feature' | 'system' | 'update'; + title: string; + message: string; + timestamp: string; + read: boolean; + actionUrl?: string; + priority: 'high' | 'medium' | 'low'; + relatedEntity?: { + type: 'profiler' | 'course' | 'programme' | 'content'; + id: string; + name: string; + }; +} +``` + +### ApprovalTask Interface +```typescript +interface ApprovalTask { + id: string; + entityType: 'profiler' | 'course' | 'content'; + entityId: string; + entityName: string; + submittedBy: { + name: string; + email: string; + avatar?: string; + }; + submittedAt: string; + status: 'pending' | 'approved' | 'rejected' | 'changes_requested'; + priority: 'high' | 'medium' | 'low'; + description: string; + version: number; + previousComments?: { + author: string; + comment: string; + timestamp: string; + action: 'approve' | 'reject' | 'request_changes'; + }[]; +} +``` + +## User Workflow + +### For Content Authors (Submitting for Approval) +1. Create or edit profiler in ProfilerBuilder +2. Change status to "In Review" +3. Submit for approval +4. Notification appears in approver's notification panel +5. Wait for approval decision +6. Receive notification about approval status +7. If changes requested, make updates and resubmit + +### For Approvers (Reviewing Content) +1. Receive notification of pending approval +2. See task appear in Dashboard "Approval Tasks" section +3. Click "Review" button to open ProfilerApproval page +4. Review all profiler details: + - Read submission information + - Check previous review history (if resubmission) + - Review profiler structure in tabs + - Open interactive preview if needed +5. Make decision: + - **Approve**: Add optional comment, approve + - **Request Changes**: Specify what needs to change + - **Reject**: Provide detailed reason +6. Action triggers notifications to relevant parties +7. Task removed from pending list + +## Integration Points + +### App.tsx Updates +- Added ProfilerApproval route: `/profiler-approval` +- Integrated notification state management +- Added approval task handlers: + - `handleApproveTask` + - `handleRejectTask` + - `handleRequestChangesTask` +- Notification handlers: + - `handleMarkNotificationAsRead` + - `handleMarkAllNotificationsAsRead` + - `handleDeleteNotification` +- Props passed to all pages via `commonProps` + +### AuthenticatedLayout Updates +- Added NotificationPanel to header +- NotificationPanel always visible (on all pages) +- Positioned next to breadcrumb navigation +- Integrated with routing for notification click navigation +- State management for local notification updates + +### Dashboard Updates +- New `renderApprovalTasks()` function +- Approval Tasks section with filtering for pending tasks +- Review button navigation with sessionStorage +- Integration with mockApprovalTasks data + +### Mock Data Updates (`/data/mockData.ts`) +Added: +- `mockNotifications`: Array of 7 sample notifications +- `mockApprovalTasks`: Array of 3 pending approval tasks +- `Notification` interface export +- `ApprovalTask` interface export + +## Mock Data Samples + +### Sample Notifications +1. Profiler approval required (2h ago, High priority) +2. Profiler changes requested (5h ago, High priority) +3. New course published (1d ago, Medium priority) +4. New feature announcement (2d ago, Low priority) +5. Profiler approved (3d ago, Medium priority) +6. System maintenance notice (4d ago, Medium priority) +7. Programme updated (5d ago, Low priority) + +### Sample Approval Tasks +1. Leadership Assessment 360 (v2, High priority, has previous comments) +2. Digital Competency Assessment (v1, High priority) +3. Emotional Intelligence Profiler (v1, Medium priority) + +## Accessibility Features +- Proper ARIA labels on notification button +- Keyboard navigation support +- Screen reader friendly timestamp formatting +- 44px minimum touch targets +- Clear visual indicators for unread items +- Color-coded priority badges +- Focus states on all interactive elements + +## Future Enhancements (Not Implemented) +The following could be added in future iterations: +- Email notifications for approval requests +- Bulk approval actions +- Approval delegation +- Custom approval workflows +- Approval deadline tracking +- Advanced filtering and search +- Export approval history +- Integration with calendar for scheduled approvals +- Mobile push notifications +- Real-time WebSocket updates + +## Testing Considerations +To test the implementation: + +1. **Notification System:** + - Click bell icon in header + - Filter between All/Unread/Approvals + - Mark notifications as read + - Delete notifications + - Click notification to navigate + +2. **Approval Workflow:** + - Navigate to Dashboard + - View Approval Tasks section + - Click "Review" on any pending task + - Review profiler details in tabs + - Test each action (Approve, Request Changes, Reject) + - Verify notifications are created + - Return to Dashboard to see task status updated + +3. **Navigation:** + - Notifications should navigate to correct pages + - Breadcrumbs should show proper path + - Back button should return to Dashboard + +## Files Modified + +### New Files +- `/components/NotificationPanel.tsx` - Notification panel component +- `/components/pages/ProfilerApproval.tsx` - Approval review page +- `/NOTIFICATIONS_AND_APPROVAL_SYSTEM.md` - This documentation + +### Modified Files +- `/App.tsx` - Added route, state management, handlers +- `/components/layout/AuthenticatedLayout.tsx` - Added notification panel +- `/components/pages/Dashboard.tsx` - Added approval tasks section +- `/data/mockData.ts` - Added notifications and approval tasks data + +## Summary +The notification and approval system provides a complete workflow for managing content reviews in the KLC Super Admin Control Panel. It follows the specification requirements for approval flows while maintaining accessibility standards and user-friendly design patterns. The system is extensible and can be adapted for other content types beyond profilers (courses, programmes, landing pages, etc.). \ No newline at end of file diff --git a/src/components/NotificationPanel.tsx b/src/components/NotificationPanel.tsx new file mode 100644 index 0000000..bb4d73c --- /dev/null +++ b/src/components/NotificationPanel.tsx @@ -0,0 +1,308 @@ +/** + * NotificationPanel Component + * + * A comprehensive notification system accessible via the bell icon in the header. + * + * Features: + * - Real-time notification badge with unread count + * - Filter notifications by: All, Unread, Approvals + * - Multiple notification types: + * - Approval: Profiler approval requests and status updates + * - Course Launch: New course publications and updates + * - Feature: New system features and enhancements + * - System: Maintenance and system announcements + * - Priority levels: High, Medium, Low + * - Mark as read/unread functionality + * - Mark all as read option + * - Delete individual notifications + * - Click notification to navigate to related content + * - Time-based sorting (newest first) + * - Contextual information with related entities + * + * The notification system integrates with the approval workflow to keep + * users informed about profiler submissions, reviews, and status changes. + */ + +import React, { useState } from 'react'; +import { Button } from './ui/button'; +import { Badge } from './ui/badge'; +import { ScrollArea } from './ui/scroll-area'; +import { Separator } from './ui/separator'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from './ui/sheet'; +import { + Bell, + Check, + CheckCircle, + AlertCircle, + Info, + X, + Trash2, + Archive, + Eye +} from 'lucide-react'; +import { toast } from "sonner@2.0.3"; + +export interface Notification { + id: string; + type: 'approval' | 'course_launch' | 'feature' | 'system' | 'update'; + title: string; + message: string; + timestamp: string; + read: boolean; + actionUrl?: string; + priority: 'high' | 'medium' | 'low'; + relatedEntity?: { + type: 'profiler' | 'course' | 'programme' | 'content'; + id: string; + name: string; + }; +} + +interface NotificationPanelProps { + notifications: Notification[]; + onNotificationClick?: (notification: Notification) => void; + onMarkAsRead?: (notificationId: string) => void; + onMarkAllAsRead?: () => void; + onDeleteNotification?: (notificationId: string) => void; + onNavigate?: (route: string) => void; +} + +export function NotificationPanel({ + notifications, + onNotificationClick, + onMarkAsRead, + onMarkAllAsRead, + onDeleteNotification, + onNavigate +}: NotificationPanelProps) { + const [open, setOpen] = useState(false); + const [filter, setFilter] = useState<'all' | 'unread' | 'approval'>('all'); + + const unreadCount = notifications.filter(n => !n.read).length; + + const getNotificationIcon = (type: Notification['type']) => { + switch (type) { + case 'approval': + return ; + case 'course_launch': + return ; + case 'feature': + return ; + case 'system': + return ; + default: + return ; + } + }; + + const getPriorityBadgeColor = (priority: Notification['priority']) => { + switch (priority) { + case 'high': + return 'bg-red-100 text-red-800 border-red-200'; + case 'medium': + return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + case 'low': + return 'bg-blue-100 text-blue-800 border-blue-200'; + } + }; + + const filteredNotifications = notifications.filter(n => { + if (filter === 'unread') return !n.read; + if (filter === 'approval') return n.type === 'approval'; + return true; + }); + + const handleNotificationClick = (notification: Notification) => { + if (!notification.read && onMarkAsRead) { + onMarkAsRead(notification.id); + } + + if (notification.actionUrl && onNavigate) { + onNavigate(notification.actionUrl); + setOpen(false); + } + + if (onNotificationClick) { + onNotificationClick(notification); + } + }; + + const handleMarkAllAsRead = () => { + if (onMarkAllAsRead) { + onMarkAllAsRead(); + toast.success('All notifications marked as read'); + } + }; + + const handleDelete = (e: React.MouseEvent, notificationId: string) => { + e.stopPropagation(); + if (onDeleteNotification) { + onDeleteNotification(notificationId); + toast.success('Notification deleted'); + } + }; + + const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + if (hours < 24) return `${hours}h ago`; + if (days < 7) return `${days}d ago`; + return date.toLocaleDateString(); + }; + + return ( + + + + + + +
+ Notifications + {unreadCount > 0 && ( + + )} +
+ + Stay updated with approval requests and system updates + +
+ + {/* Filter Tabs */} +
+
+ + + +
+
+ + + + {/* Notifications List */} + +
+ {filteredNotifications.length === 0 ? ( +
+ +

No notifications

+
+ ) : ( + filteredNotifications.map((notification) => ( +
handleNotificationClick(notification)} + > +
+
+ {getNotificationIcon(notification.type)} +
+
+
+

+ {notification.title} +

+ {!notification.read && ( +
+ )} +
+

+ {notification.message} +

+
+
+ + {formatTimestamp(notification.timestamp)} + + {notification.priority === 'high' && ( + + High Priority + + )} +
+ +
+ {notification.relatedEntity && ( +
+ {notification.relatedEntity.type}: {notification.relatedEntity.name} +
+ )} +
+
+
+ )) + )} +
+ + + + ); +} \ No newline at end of file diff --git a/src/components/layout/AuthenticatedLayout.tsx b/src/components/layout/AuthenticatedLayout.tsx index a46622d..c67e1a3 100644 --- a/src/components/layout/AuthenticatedLayout.tsx +++ b/src/components/layout/AuthenticatedLayout.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { Button } from '../ui/button'; +import { NotificationPanel, Notification } from '../NotificationPanel'; import { Home, FileText, @@ -39,7 +40,7 @@ import { } from '../ui/dropdown-menu'; import { toast } from "sonner@2.0.3"; import klcLogoDark from 'figma:asset/af520440d0fb3ca587ea6a7b2e63956e028f6f37.png'; -import { SESSION_CONFIG, AutoSaveData } from '../../data/mockData'; +import { SESSION_CONFIG, AutoSaveData, mockNotifications } from '../../data/mockData'; interface NavigationItem { id: string; @@ -64,6 +65,10 @@ interface AuthenticatedLayoutProps { breadcrumbs?: Array<{ label: string; href?: string }>; formData?: any; onAutoSave?: (data: any) => void; + notifications?: Notification[]; + onMarkNotificationAsRead?: (notificationId: string) => void; + onMarkAllNotificationsAsRead?: () => void; + onDeleteNotification?: (notificationId: string) => void; } // Updated navigation structure per requirements @@ -203,6 +208,11 @@ const useAutoSave = ( // Session timeout hook const useSessionTimeout = (onLogout: () => void) => { useEffect(() => { + // Session timeout disabled - no warnings or automatic logout + if (!SESSION_CONFIG.ENABLE_SESSION_TIMEOUT) { + return; + } + let timeoutId: NodeJS.Timeout; let warningId: NodeJS.Timeout; @@ -257,14 +267,48 @@ export function AuthenticatedLayout({ user, breadcrumbs = [], formData, - onAutoSave + onAutoSave, + notifications = mockNotifications, + onMarkNotificationAsRead, + onMarkAllNotificationsAsRead, + onDeleteNotification }: AuthenticatedLayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [localNotifications, setLocalNotifications] = useState(notifications); // Initialize auto-save and session timeout useAutoSave(formData, onAutoSave, currentRoute, user.email); useSessionTimeout(onLogout); + // Update local notifications when prop changes + useEffect(() => { + setLocalNotifications(notifications); + }, [notifications]); + + // Notification handlers + const handleMarkAsRead = (notificationId: string) => { + setLocalNotifications(prev => + prev.map(n => n.id === notificationId ? { ...n, read: true } : n) + ); + if (onMarkNotificationAsRead) { + onMarkNotificationAsRead(notificationId); + } + }; + + const handleMarkAllAsRead = () => { + setLocalNotifications(prev => prev.map(n => ({ ...n, read: true }))); + if (onMarkAllNotificationsAsRead) { + onMarkAllNotificationsAsRead(); + } + }; + + const handleDeleteNotification = (notificationId: string) => { + setLocalNotifications(prev => prev.filter(n => n.id !== notificationId)); + if (onDeleteNotification) { + onDeleteNotification(notificationId); + } + }; + // Load auto-saved data on mount useEffect(() => { const savedData = localStorage.getItem(`autosave_${user.email}_${currentRoute}`); @@ -338,39 +382,53 @@ export function AuthenticatedLayout({ // Enhanced breadcrumb handling - Fixed to show proper navigation flow const renderBreadcrumbs = () => { - if (breadcrumbs.length === 0) return null; - return (
- - - - onNavigate('/dashboard')} - className="cursor-pointer" - > - Admin - - - {breadcrumbs.map((crumb, index) => ( - - - - {crumb.href && index < breadcrumbs.length - 1 ? ( - onNavigate(crumb.href!)} - className="cursor-pointer" - > - {crumb.label} - - ) : ( - {crumb.label} - )} - - - ))} - - +
+ + + + onNavigate('/dashboard')} + className="cursor-pointer" + > + Admin + + + {breadcrumbs.map((crumb, index) => ( + + + + {crumb.href && index < breadcrumbs.length - 1 ? ( + onNavigate(crumb.href!)} + className="cursor-pointer" + > + {crumb.label} + + ) : ( + {crumb.label} + )} + + + ))} + + + + {/* Notification Bell */} + { + if (notification.actionUrl) { + onNavigate(notification.actionUrl); + } + }} + onMarkAsRead={handleMarkAsRead} + onMarkAllAsRead={handleMarkAllAsRead} + onDeleteNotification={handleDeleteNotification} + onNavigate={onNavigate} + /> +
); }; diff --git a/src/components/pages/Dashboard.tsx b/src/components/pages/Dashboard.tsx index 9cf1c3e..7e6d54d 100644 --- a/src/components/pages/Dashboard.tsx +++ b/src/components/pages/Dashboard.tsx @@ -48,7 +48,11 @@ import { Minus, GraduationCap, BookOpen, - Award + Award, + CheckCircle, + Clock, + AlertCircle, + FileText } from 'lucide-react'; import { dashboardKPIs, @@ -56,7 +60,9 @@ import { mockCourses, mockProgrammes, mockContent, - mockLeads + mockLeads, + mockApprovalTasks, + ApprovalTask } from '../../data/mockData'; interface DashboardProps { @@ -283,6 +289,104 @@ export function Dashboard({ onNavigate, onLogout, user }: DashboardProps) { ); + const renderApprovalTasks = () => { + const pendingTasks = mockApprovalTasks.filter(task => task.status === 'pending'); + + if (pendingTasks.length === 0) { + return null; + } + + const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const hours = Math.floor(diff / (60 * 60 * 1000)); + const days = Math.floor(hours / 24); + + if (hours < 1) return 'Just now'; + if (hours < 24) return `${hours}h ago`; + if (days < 7) return `${days}d ago`; + return date.toLocaleDateString(); + }; + + return ( +
+
+
+ +

Approval Tasks

+ + {pendingTasks.length} Pending + +
+
+ + + + + + + Item + Type + Submitted By + Submitted + Actions + + + + {pendingTasks.map((task) => ( + + +
+ {task.entityName} + + {task.description} + +
+
+ + + {task.entityType.charAt(0).toUpperCase() + task.entityType.slice(1)} + + + +
+ {task.submittedBy.name} + + {task.submittedBy.email} + +
+
+ +
+ + {formatTimestamp(task.submittedAt)} +
+
+ + + +
+ ))} +
+
+
+
+
+ ); + }; + const renderKPICards = () => (

Key Performance Indicators

@@ -553,6 +657,9 @@ export function Dashboard({ onNavigate, onLogout, user }: DashboardProps) { {/* Quick Actions */} {renderQuickActions()} + {/* Approval Tasks */} + {renderApprovalTasks()} + {/* KPI Strip */} {renderKPICards()} diff --git a/src/components/pages/ProfilerApproval.tsx b/src/components/pages/ProfilerApproval.tsx new file mode 100644 index 0000000..2999475 --- /dev/null +++ b/src/components/pages/ProfilerApproval.tsx @@ -0,0 +1,525 @@ +/** + * ProfilerApproval Component + * + * This component handles the approval workflow for profilers that are in review status. + * + * Flow: + * 1. Users with approval role see pending approval tasks on the Dashboard + * 2. Clicking "Review" navigates to this page with the approval task data + * 3. Approvers can view the complete profiler details including: + * - Submission information (who submitted, when, version) + * - Previous review comments and history + * - Full profiler structure (sections, questions, scoring) + * - Interactive preview capability + * 4. Approvers can take one of three actions: + * - Approve: Publishes the profiler and makes it available + * - Request Changes: Sends feedback to author with specific changes needed + * - Reject: Rejects the submission with reasons + * + * All actions generate notifications to keep relevant parties informed. + */ + +import React, { useState } from 'react'; +import { AuthenticatedLayout } from '../layout/AuthenticatedLayout'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../ui/card'; +import { Button } from '../ui/button'; +import { Badge } from '../ui/badge'; +import { Label } from '../ui/label'; +import { Textarea } from '../ui/textarea'; +import { Separator } from '../ui/separator'; +import { Alert, AlertDescription } from '../ui/alert'; +import { Avatar, AvatarFallback } from '../ui/avatar'; +import { ScrollArea } from '../ui/scroll-area'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '../ui/dialog'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '../ui/tabs'; +import { + CheckCircle, + XCircle, + MessageSquare, + Eye, + Calendar, + User, + FileText, + Clock, + ArrowLeft, + AlertCircle, + ThumbsUp, + ThumbsDown, + Send +} from 'lucide-react'; +import { toast } from "sonner@2.0.3"; +import { ApprovalTask } from '../../data/mockData'; + +interface ProfilerApprovalProps { + approvalTask: ApprovalTask; + onNavigate: (route: string) => void; + onLogout: () => void; + user: { + name: string; + email: string; + role: string; + avatar?: string; + lastLogin: string; + }; + onApprove?: (taskId: string, comment: string) => void; + onReject?: (taskId: string, reason: string) => void; + onRequestChanges?: (taskId: string, reason: string) => void; +} + +export function ProfilerApproval({ + approvalTask, + onNavigate, + onLogout, + user, + onApprove, + onReject, + onRequestChanges +}: ProfilerApprovalProps) { + const [showApproveDialog, setShowApproveDialog] = useState(false); + const [showRejectDialog, setShowRejectDialog] = useState(false); + const [showChangesDialog, setShowChangesDialog] = useState(false); + const [comment, setComment] = useState(''); + const [rejectionReason, setRejectionReason] = useState(''); + const [changesReason, setChangesReason] = useState(''); + + // Mock profiler data - in real app, this would be fetched based on approvalTask.entityId + const profilerData = { + id: approvalTask.entityId, + name: approvalTask.entityName, + description: approvalTask.description, + type: 'Ipsative', + questionType: 'Ipsative', + totalQuestions: 24, + estimatedTime: '15-20 minutes', + sections: [ + { + id: 's1', + title: 'Leadership Style', + description: 'Assess your natural leadership tendencies', + questions: 8 + }, + { + id: 's2', + title: 'Communication Preferences', + description: 'Understand your communication patterns', + questions: 8 + }, + { + id: 's3', + title: 'Decision Making', + description: 'Evaluate your decision-making approach', + questions: 8 + } + ], + scoringMethod: 'Ipsative ranking with forced distribution', + targetAudience: 'Mid to senior level leaders', + createdBy: approvalTask.submittedBy + }; + + const handleApprove = () => { + if (onApprove) { + onApprove(approvalTask.id, comment); + } + toast.success(`${approvalTask.entityName} has been approved`); + setShowApproveDialog(false); + setComment(''); + setTimeout(() => onNavigate('/dashboard'), 1000); + }; + + const handleReject = () => { + if (!rejectionReason.trim()) { + toast.error('Please provide a reason for rejection'); + return; + } + if (onReject) { + onReject(approvalTask.id, rejectionReason); + } + toast.error(`${approvalTask.entityName} has been rejected`); + setShowRejectDialog(false); + setRejectionReason(''); + setTimeout(() => onNavigate('/dashboard'), 1000); + }; + + const handleRequestChanges = () => { + if (!changesReason.trim()) { + toast.error('Please specify what changes are needed'); + return; + } + if (onRequestChanges) { + onRequestChanges(approvalTask.id, changesReason); + } + toast.info('Change request sent to the author'); + setShowChangesDialog(false); + setChangesReason(''); + setTimeout(() => onNavigate('/dashboard'), 1000); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString('en-IN', { + dateStyle: 'medium', + timeStyle: 'short' + }); + }; + + return ( + +
+ {/* Header */} +
+ + +
+
+
+

{approvalTask.entityName}

+ + {approvalTask.priority === 'high' ? 'High Priority' : 'Medium Priority'} + + + Version {approvalTask.version} + +
+

{approvalTask.description}

+
+
+
+ + {/* Submission Info */} + + + Submission Information + + +
+
+ + + {approvalTask.submittedBy.name.split(' ').map(n => n[0]).join('')} + + +
+

Submitted by

+

{approvalTask.submittedBy.name}

+

{approvalTask.submittedBy.email}

+
+
+
+ +
+

Submitted at

+

{formatDate(approvalTask.submittedAt)}

+
+
+
+ +
+

Type

+

{approvalTask.entityType.charAt(0).toUpperCase() + approvalTask.entityType.slice(1)}

+
+
+
+
+
+ + {/* Previous Comments */} + {approvalTask.previousComments && approvalTask.previousComments.length > 0 && ( + + + Previous Review History + Comments and actions from previous review cycles + + +
+ {approvalTask.previousComments.map((comment, index) => ( +
+ +
+
+ {comment.author} + + {comment.action === 'approve' ? 'Approved' : + comment.action === 'reject' ? 'Rejected' : + 'Changes Requested'} + +
+

{comment.comment}

+

+ {formatDate(comment.timestamp)} +

+
+
+ ))} +
+
+
+ )} + + {/* Profiler Details */} + + + Profiler Details + Review the profiler structure and configuration + + + + + Overview + Sections + Preview + + + +
+
+ +

{profilerData.questionType}

+
+
+ +

{profilerData.totalQuestions}

+
+
+ +

{profilerData.estimatedTime}

+
+
+ +

{profilerData.scoringMethod}

+
+
+ +

{profilerData.targetAudience}

+
+
+
+ + +
+ {profilerData.sections.map((section, index) => ( + + +
+ + Section {index + 1}: {section.title} + + {section.questions} questions +
+ {section.description} +
+
+ ))} +
+
+ + + + + + Full interactive preview is available. You can navigate through all questions and test the profiler functionality. + + +
+ +
+
+
+
+
+ + {/* Action Buttons */} + + + Review Actions + + As an approver, you can approve this profiler, request changes, or reject it + + + +
+ + + +
+
+
+
+ + {/* Approve Dialog */} + + + + Approve Profiler + + You are about to approve "{approvalTask.entityName}". This will publish the profiler and make it available for use. + + +
+
+ +