adding new src

This commit is contained in:
priyanshuvish
2025-09-30 18:40:22 +05:30
parent d11112c8e9
commit 1ee2f06db9
9 changed files with 1698 additions and 40 deletions

View File

@@ -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<Notification[]>(mockNotifications);
const [approvalTasks, setApprovalTasks] = useState<ApprovalTask[]>(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 <ProfilerBuilder {...commonProps} />;
case "/profilers/preview":
return <ProfilerPreview onBack={() => 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 (
<ProfilerApproval
approvalTask={approvalTask}
onNavigate={navigate}
onLogout={logout}
user={user}
onApprove={handleApproveTask}
onReject={handleRejectTask}
onRequestChanges={handleRequestChangesTask}
/>
);
case "/admin/profiler-master":
return <ProfilerMaster {...commonProps} />;
case "/landing-pages":

View File

@@ -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.).

View File

@@ -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 <CheckCircle className="h-5 w-5 text-blue-600" />;
case 'course_launch':
return <AlertCircle className="h-5 w-5 text-green-600" />;
case 'feature':
return <Info className="h-5 w-5 text-purple-600" />;
case 'system':
return <Bell className="h-5 w-5 text-orange-600" />;
default:
return <Info className="h-5 w-5 text-gray-600" />;
}
};
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="sm"
className="relative min-h-[44px] min-w-[44px]"
aria-label={`Notifications ${unreadCount > 0 ? `(${unreadCount} unread)` : ''}`}
>
<Bell className="h-5 w-5" />
{unreadCount > 0 && (
<Badge
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs"
style={{ backgroundColor: 'var(--color-brand-accent)', color: 'var(--color-brand-primary)' }}
>
{unreadCount > 9 ? '9+' : unreadCount}
</Badge>
)}
</Button>
</SheetTrigger>
<SheetContent className="w-[400px] sm:w-[540px] p-0">
<SheetHeader className="p-6 pb-4">
<div className="flex items-center justify-between">
<SheetTitle>Notifications</SheetTitle>
{unreadCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleMarkAllAsRead}
className="text-sm"
>
<Check className="h-4 w-4 mr-1" />
Mark all as read
</Button>
)}
</div>
<SheetDescription>
Stay updated with approval requests and system updates
</SheetDescription>
</SheetHeader>
{/* Filter Tabs */}
<div className="px-6 pb-4">
<div className="flex gap-2">
<Button
variant={filter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('all')}
>
All ({notifications.length})
</Button>
<Button
variant={filter === 'unread' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('unread')}
>
Unread ({unreadCount})
</Button>
<Button
variant={filter === 'approval' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('approval')}
>
Approvals ({notifications.filter(n => n.type === 'approval').length})
</Button>
</div>
</div>
<Separator />
{/* Notifications List */}
<ScrollArea className="h-[calc(100vh-180px)]">
<div className="divide-y">
{filteredNotifications.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
<Bell className="h-12 w-12 mx-auto mb-3 opacity-20" />
<p>No notifications</p>
</div>
) : (
filteredNotifications.map((notification) => (
<div
key={notification.id}
className={`p-4 hover:bg-muted/50 transition-colors cursor-pointer ${
!notification.read ? 'bg-blue-50/50' : ''
}`}
onClick={() => handleNotificationClick(notification)}
>
<div className="flex gap-3">
<div className="flex-shrink-0 mt-1">
{getNotificationIcon(notification.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<h4 className={`text-sm ${!notification.read ? 'font-semibold' : 'font-medium'}`}>
{notification.title}
</h4>
{!notification.read && (
<div className="h-2 w-2 rounded-full bg-blue-600 flex-shrink-0 mt-1" />
)}
</div>
<p className="text-sm text-muted-foreground mb-2 line-clamp-2">
{notification.message}
</p>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{formatTimestamp(notification.timestamp)}
</span>
{notification.priority === 'high' && (
<Badge
variant="outline"
className={`text-xs ${getPriorityBadgeColor(notification.priority)}`}
>
High Priority
</Badge>
)}
</div>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={(e) => handleDelete(e, notification.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{notification.relatedEntity && (
<div className="mt-2 text-xs text-muted-foreground">
{notification.relatedEntity.type}: {notification.relatedEntity.name}
</div>
)}
</div>
</div>
</div>
))
)}
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}

View File

@@ -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 (
<div className="border-b bg-muted/30 px-6 py-3">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => onNavigate('/dashboard')}
className="cursor-pointer"
>
Admin
</BreadcrumbLink>
</BreadcrumbItem>
{breadcrumbs.map((crumb, index) => (
<React.Fragment key={index}>
<BreadcrumbSeparator />
<BreadcrumbItem>
{crumb.href && index < breadcrumbs.length - 1 ? (
<BreadcrumbLink
onClick={() => onNavigate(crumb.href!)}
className="cursor-pointer"
>
{crumb.label}
</BreadcrumbLink>
) : (
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
)}
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
<div className="flex items-center justify-between">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => onNavigate('/dashboard')}
className="cursor-pointer"
>
Admin
</BreadcrumbLink>
</BreadcrumbItem>
{breadcrumbs.map((crumb, index) => (
<React.Fragment key={index}>
<BreadcrumbSeparator />
<BreadcrumbItem>
{crumb.href && index < breadcrumbs.length - 1 ? (
<BreadcrumbLink
onClick={() => onNavigate(crumb.href!)}
className="cursor-pointer"
>
{crumb.label}
</BreadcrumbLink>
) : (
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
)}
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
{/* Notification Bell */}
<NotificationPanel
notifications={localNotifications}
onNotificationClick={(notification) => {
if (notification.actionUrl) {
onNavigate(notification.actionUrl);
}
}}
onMarkAsRead={handleMarkAsRead}
onMarkAllAsRead={handleMarkAllAsRead}
onDeleteNotification={handleDeleteNotification}
onNavigate={onNavigate}
/>
</div>
</div>
);
};

View File

@@ -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) {
</section>
);
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 (
<section aria-labelledby="approval-tasks-section" className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5" style={{ color: 'var(--color-brand-primary)' }} />
<h2 id="approval-tasks-section">Approval Tasks</h2>
<Badge variant="outline" className="bg-red-100 text-red-800 border-red-200">
{pendingTasks.length} Pending
</Badge>
</div>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead style={{ width: '35%' }}>Item</TableHead>
<TableHead style={{ width: '12%' }}>Type</TableHead>
<TableHead style={{ width: '25%' }}>Submitted By</TableHead>
<TableHead style={{ width: '15%' }}>Submitted</TableHead>
<TableHead style={{ width: '13%' }} className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pendingTasks.map((task) => (
<TableRow key={task.id}>
<TableCell style={{ width: '35%' }}>
<div className="flex flex-col">
<span>{task.entityName}</span>
<span className="text-sm text-muted-foreground line-clamp-1">
{task.description}
</span>
</div>
</TableCell>
<TableCell style={{ width: '12%' }}>
<Badge variant="outline">
{task.entityType.charAt(0).toUpperCase() + task.entityType.slice(1)}
</Badge>
</TableCell>
<TableCell style={{ width: '25%' }}>
<div className="flex flex-col">
<span className="text-sm">{task.submittedBy.name}</span>
<span className="text-xs text-muted-foreground">
{task.submittedBy.email}
</span>
</div>
</TableCell>
<TableCell style={{ width: '15%' }}>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Clock className="h-3 w-3" />
{formatTimestamp(task.submittedAt)}
</div>
</TableCell>
<TableCell style={{ width: '13%' }} className="text-right">
<Button
size="sm"
onClick={() => {
// Store the task in sessionStorage to be accessed by the approval page
sessionStorage.setItem('currentApprovalTask', JSON.stringify(task));
onNavigate('/profiler-approval');
}}
className="min-h-[44px]"
>
<Eye className="h-4 w-4 mr-2" />
Review
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</section>
);
};
const renderKPICards = () => (
<section aria-labelledby="kpi-section" className="space-y-4">
<h2 id="kpi-section" className="sr-only">Key Performance Indicators</h2>
@@ -553,6 +657,9 @@ export function Dashboard({ onNavigate, onLogout, user }: DashboardProps) {
{/* Quick Actions */}
{renderQuickActions()}
{/* Approval Tasks */}
{renderApprovalTasks()}
{/* KPI Strip */}
{renderKPICards()}

View File

@@ -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 (
<AuthenticatedLayout
currentRoute="/profiler-approval"
onNavigate={onNavigate}
onLogout={onLogout}
user={user}
breadcrumbs={[
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Approval Tasks', href: '/dashboard' },
{ label: approvalTask.entityName }
]}
>
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-6">
<Button
variant="ghost"
onClick={() => onNavigate('/dashboard')}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Dashboard
</Button>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3 mb-2">
<h1>{approvalTask.entityName}</h1>
<Badge
variant="outline"
style={{
backgroundColor: approvalTask.priority === 'high' ? '#FEE2E2' : '#FEF3C7',
color: approvalTask.priority === 'high' ? '#991B1B' : '#92400E',
borderColor: approvalTask.priority === 'high' ? '#FCA5A5' : '#FCD34D'
}}
>
{approvalTask.priority === 'high' ? 'High Priority' : 'Medium Priority'}
</Badge>
<Badge variant="outline">
Version {approvalTask.version}
</Badge>
</div>
<p className="text-muted-foreground">{approvalTask.description}</p>
</div>
</div>
</div>
{/* Submission Info */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Submission Information</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="flex items-start gap-3">
<Avatar className="h-10 w-10">
<AvatarFallback style={{ backgroundColor: 'var(--color-brand-accent)', color: 'var(--color-brand-primary)' }}>
{approvalTask.submittedBy.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div>
<p className="text-sm text-muted-foreground">Submitted by</p>
<p>{approvalTask.submittedBy.name}</p>
<p className="text-sm text-muted-foreground">{approvalTask.submittedBy.email}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Calendar className="h-5 w-5 text-muted-foreground mt-1" />
<div>
<p className="text-sm text-muted-foreground">Submitted at</p>
<p>{formatDate(approvalTask.submittedAt)}</p>
</div>
</div>
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-muted-foreground mt-1" />
<div>
<p className="text-sm text-muted-foreground">Type</p>
<p>{approvalTask.entityType.charAt(0).toUpperCase() + approvalTask.entityType.slice(1)}</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Previous Comments */}
{approvalTask.previousComments && approvalTask.previousComments.length > 0 && (
<Card className="mb-6">
<CardHeader>
<CardTitle>Previous Review History</CardTitle>
<CardDescription>Comments and actions from previous review cycles</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{approvalTask.previousComments.map((comment, index) => (
<div key={index} className="flex gap-3 p-4 bg-muted/30 rounded-lg">
<MessageSquare className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<span>{comment.author}</span>
<Badge
variant="outline"
className={
comment.action === 'approve' ? 'bg-green-50 text-green-800 border-green-200' :
comment.action === 'reject' ? 'bg-red-50 text-red-800 border-red-200' :
'bg-yellow-50 text-yellow-800 border-yellow-200'
}
>
{comment.action === 'approve' ? 'Approved' :
comment.action === 'reject' ? 'Rejected' :
'Changes Requested'}
</Badge>
</div>
<p className="text-muted-foreground mb-1">{comment.comment}</p>
<p className="text-sm text-muted-foreground">
{formatDate(comment.timestamp)}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Profiler Details */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Profiler Details</CardTitle>
<CardDescription>Review the profiler structure and configuration</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="sections">Sections</TabsTrigger>
<TabsTrigger value="preview">Preview</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4 mt-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-muted-foreground">Question Type</Label>
<p>{profilerData.questionType}</p>
</div>
<div>
<Label className="text-muted-foreground">Total Questions</Label>
<p>{profilerData.totalQuestions}</p>
</div>
<div>
<Label className="text-muted-foreground">Estimated Time</Label>
<p>{profilerData.estimatedTime}</p>
</div>
<div>
<Label className="text-muted-foreground">Scoring Method</Label>
<p>{profilerData.scoringMethod}</p>
</div>
<div className="col-span-2">
<Label className="text-muted-foreground">Target Audience</Label>
<p>{profilerData.targetAudience}</p>
</div>
</div>
</TabsContent>
<TabsContent value="sections" className="mt-4">
<div className="space-y-4">
{profilerData.sections.map((section, index) => (
<Card key={section.id}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base">
Section {index + 1}: {section.title}
</CardTitle>
<Badge variant="outline">{section.questions} questions</Badge>
</div>
<CardDescription>{section.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="preview" className="mt-4">
<Alert>
<Eye className="h-4 w-4" />
<AlertDescription>
Full interactive preview is available. You can navigate through all questions and test the profiler functionality.
</AlertDescription>
</Alert>
<div className="mt-4">
<Button
onClick={() => {
// In a real app, this would open the ProfilerPreview with the specific profiler data
toast.info('Preview functionality would open here');
}}
>
<Eye className="h-4 w-4 mr-2" />
Open Full Preview
</Button>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Action Buttons */}
<Card>
<CardHeader>
<CardTitle>Review Actions</CardTitle>
<CardDescription>
As an approver, you can approve this profiler, request changes, or reject it
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-3">
<Button
onClick={() => setShowApproveDialog(true)}
className="min-h-[44px]"
style={{ backgroundColor: '#10B981', color: 'white' }}
>
<CheckCircle className="h-4 w-4 mr-2" />
Approve
</Button>
<Button
variant="outline"
onClick={() => setShowChangesDialog(true)}
className="min-h-[44px]"
>
<MessageSquare className="h-4 w-4 mr-2" />
Request Changes
</Button>
<Button
variant="outline"
onClick={() => setShowRejectDialog(true)}
className="min-h-[44px] border-red-200 text-red-700 hover:bg-red-50"
>
<XCircle className="h-4 w-4 mr-2" />
Reject
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Approve Dialog */}
<Dialog open={showApproveDialog} onOpenChange={setShowApproveDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Approve Profiler</DialogTitle>
<DialogDescription>
You are about to approve "{approvalTask.entityName}". This will publish the profiler and make it available for use.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="approve-comment">Comment (Optional)</Label>
<Textarea
id="approve-comment"
placeholder="Add any comments or feedback..."
value={comment}
onChange={(e) => setComment(e.target.value)}
className="mt-2"
rows={4}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowApproveDialog(false)}>
Cancel
</Button>
<Button onClick={handleApprove} style={{ backgroundColor: '#10B981', color: 'white' }}>
<CheckCircle className="h-4 w-4 mr-2" />
Approve
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Reject Dialog */}
<Dialog open={showRejectDialog} onOpenChange={setShowRejectDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Reject Profiler</DialogTitle>
<DialogDescription>
Please provide a reason for rejecting "{approvalTask.entityName}". This will be sent to the author.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="reject-reason">Reason for Rejection *</Label>
<Textarea
id="reject-reason"
placeholder="Explain why this profiler is being rejected..."
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
className="mt-2"
rows={5}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowRejectDialog(false)}>
Cancel
</Button>
<Button
onClick={handleReject}
className="bg-red-600 hover:bg-red-700 text-white"
>
<XCircle className="h-4 w-4 mr-2" />
Reject
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Request Changes Dialog */}
<Dialog open={showChangesDialog} onOpenChange={setShowChangesDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Request Changes</DialogTitle>
<DialogDescription>
Specify what changes are needed for "{approvalTask.entityName}". The author will be notified.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<Label htmlFor="changes-reason">Changes Required *</Label>
<Textarea
id="changes-reason"
placeholder="Describe what needs to be changed..."
value={changesReason}
onChange={(e) => setChangesReason(e.target.value)}
className="mt-2"
rows={5}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowChangesDialog(false)}>
Cancel
</Button>
<Button onClick={handleRequestChanges}>
<Send className="h-4 w-4 mr-2" />
Send Request
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</AuthenticatedLayout>
);
}

View File

@@ -2157,10 +2157,18 @@ export function ProfilerBuilder({ onNavigate, onLogout, user, formData, onAutoSa
className="space-y-3"
>
{questionTypeOptions.map((option) => (
<div key={option.value} className="flex items-center space-x-2">
<div
key={option.value}
className={`flex items-center space-x-3 p-4 border-2 rounded-lg transition-all cursor-pointer hover:border-[#04045B] hover:bg-blue-50 ${
questionType === option.value
? 'border-[#04045B] bg-blue-50'
: 'border-gray-300 bg-white'
}`}
onClick={() => handleQuestionTypeChange(option.value)}
>
<RadioGroupItem value={option.value} id={option.value} />
<div className="flex-1">
<Label htmlFor={option.value} className="font-medium">
<Label htmlFor={option.value} className="font-medium cursor-pointer">
{option.label}
</Label>
<p className="text-sm text-gray-600">{option.description}</p>

View File

@@ -4166,11 +4166,208 @@ export const mockProfilerTypes: ProfilerType[] = [
}
];
// Notifications data
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;
};
}
export const mockNotifications: Notification[] = [
{
id: "notif_001",
type: "approval",
title: "Profiler Approval Required",
message: "Leadership Assessment 360 profiler is pending your review and approval",
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), // 2 hours ago
read: false,
actionUrl: "/dashboard",
priority: "high",
relatedEntity: {
type: "profiler",
id: "prof_leadership_360",
name: "Leadership Assessment 360"
}
},
{
id: "notif_002",
type: "approval",
title: "Profiler Changes Requested",
message: "Digital Competency Assessment has been updated and requires re-approval",
timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString(), // 5 hours ago
read: false,
actionUrl: "/dashboard",
priority: "high",
relatedEntity: {
type: "profiler",
id: "prof_digital_comp",
name: "Digital Competency Assessment"
}
},
{
id: "notif_003",
type: "course_launch",
title: "New Course Published",
message: "Strategic Decision Making course has been successfully published and is now live",
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // 1 day ago
read: false,
actionUrl: "/courses",
priority: "medium",
relatedEntity: {
type: "course",
id: "crs_strategic_thinking",
name: "Strategic Decision Making"
}
},
{
id: "notif_004",
type: "feature",
title: "New Feature: Interactive Profiler Preview",
message: "You can now preview profilers with interactive question navigation and real-time validation",
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago
read: true,
priority: "low"
},
{
id: "notif_005",
type: "approval",
title: "Team Dynamics Profiler Approved",
message: "Your submission for Team Dynamics Profiler has been approved and published",
timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
read: true,
actionUrl: "/profilers",
priority: "medium",
relatedEntity: {
type: "profiler",
id: "prof_team_dynamics",
name: "Team Dynamics Profiler"
}
},
{
id: "notif_006",
type: "system",
title: "Scheduled Maintenance",
message: "System maintenance is scheduled for Saturday, 2:00 AM - 4:00 AM IST",
timestamp: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString(), // 4 days ago
read: true,
priority: "medium"
},
{
id: "notif_007",
type: "course_launch",
title: "Programme Updated",
message: "Executive Leadership Programme has been updated with new modules",
timestamp: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days ago
read: true,
actionUrl: "/programmes",
priority: "low",
relatedEntity: {
type: "programme",
id: "prg_exec_leadership",
name: "Executive Leadership Programme"
}
}
];
// Approval Tasks data
export 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';
}[];
}
export const mockApprovalTasks: ApprovalTask[] = [
{
id: "approval_001",
entityType: "profiler",
entityId: "prof_leadership_360",
entityName: "Leadership Assessment 360",
submittedBy: {
name: "Dr. Priya Sharma",
email: "priya.sharma@klc.edu",
avatar: ""
},
submittedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
status: "pending",
priority: "high",
description: "Comprehensive 360-degree leadership assessment with multi-rater feedback",
version: 2,
previousComments: [
{
author: "Admin User",
comment: "Please update the scoring rubric for consistency",
timestamp: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(),
action: "request_changes"
}
]
},
{
id: "approval_002",
entityType: "profiler",
entityId: "prof_digital_comp",
entityName: "Digital Competency Assessment",
submittedBy: {
name: "Rajesh Kumar",
email: "rajesh.kumar@klc.edu",
avatar: ""
},
submittedAt: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString(),
status: "pending",
priority: "high",
description: "Assessment to evaluate digital skills and competencies for modern workplace",
version: 1
},
{
id: "approval_003",
entityType: "profiler",
entityId: "prof_emotional_intelligence",
entityName: "Emotional Intelligence Profiler",
submittedBy: {
name: "Sneha Patel",
email: "sneha.patel@klc.edu",
avatar: ""
},
submittedAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
status: "pending",
priority: "medium",
description: "Profiler to measure emotional intelligence across key dimensions",
version: 1
}
];
// Session configuration
export const SESSION_CONFIG = {
LOGOUT_TIMEOUT: 8 * 60 * 60 * 1000, // 8 hours in milliseconds
AUTO_SAVE_INTERVAL: 30 * 1000, // 30 seconds
WARNING_TIME: 10 * 60 * 1000, // 10 minutes before logout
WARNING_TIME: 10 * 60 * 1000, // 10 minutes before logout (fixed from 10 hours)
ENABLE_SESSION_TIMEOUT: false, // Disable session timeout warnings
};
// Auto-save functionality data structure

View File

@@ -545,6 +545,10 @@
inset-block: calc(var(--spacing) * 0);
}
.-top-1 {
top: calc(var(--spacing) * -1);
}
.-top-3 {
top: calc(var(--spacing) * -3);
}
@@ -569,6 +573,10 @@
top: 50%;
}
.-right-1 {
right: calc(var(--spacing) * -1);
}
.right-0 {
right: calc(var(--spacing) * 0);
}
@@ -866,6 +874,13 @@
overflow: hidden;
}
.line-clamp-2 {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
display: -webkit-box;
overflow: hidden;
}
.\!block {
display: block !important;
}
@@ -989,6 +1004,10 @@
height: calc(var(--spacing) * 6);
}
.h-7 {
height: calc(var(--spacing) * 7);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
@@ -1045,6 +1064,10 @@
height: calc(100vh - 120px);
}
.h-\[calc\(100vh-180px\)\] {
height: calc(100vh - 180px);
}
.h-\[var\(--radix-select-trigger-height\)\] {
height: var(--radix-select-trigger-height);
}
@@ -1201,6 +1224,10 @@
width: calc(var(--spacing) * 6);
}
.w-7 {
width: calc(var(--spacing) * 7);
}
.w-8 {
width: calc(var(--spacing) * 8);
}
@@ -1869,6 +1896,10 @@
border-style: none;
}
.border-\[\#04045B\] {
border-color: #04045b;
}
.border-\[var\(--color-brand-primary\)\] {
border-color: var(--color-brand-primary);
}
@@ -2021,6 +2052,16 @@
background-color: var(--color-blue-50);
}
.bg-blue-50\/50 {
background-color: color-mix(in srgb, oklch(.97 .014 254.604) 50%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.bg-blue-50\/50 {
background-color: color-mix(in oklab, var(--color-blue-50) 50%, transparent);
}
}
.bg-blue-100 {
background-color: var(--color-blue-100);
}
@@ -2029,6 +2070,10 @@
background-color: var(--color-blue-500);
}
.bg-blue-600 {
background-color: var(--color-blue-600);
}
.bg-border {
background-color: var(--border);
}
@@ -2544,6 +2589,10 @@
text-align: left;
}
.text-right {
text-align: right;
}
.align-middle {
vertical-align: middle;
}
@@ -2846,6 +2895,10 @@
text-underline-offset: 4px;
}
.opacity-20 {
opacity: .2;
}
.opacity-50 {
opacity: .5;
}
@@ -3055,6 +3108,12 @@
--tw-ring-color: var(--color-brand-primary);
}
@media (hover: hover) {
.hover\:border-\[\#04045B\]:hover {
border-color: #04045b;
}
}
@media (hover: hover) {
.hover\:border-\[var\(--color-brand-primary\)\]\/50:hover {
border-color: color-mix(in srgb, var(--color-brand-primary) 50%, transparent);
@@ -3115,6 +3174,12 @@
}
}
@media (hover: hover) {
.hover\:bg-blue-50:hover {
background-color: var(--color-blue-50);
}
}
@media (hover: hover) {
.hover\:bg-destructive\/10:hover {
background-color: var(--destructive);
@@ -3169,6 +3234,12 @@
}
}
@media (hover: hover) {
.hover\:bg-red-50:hover {
background-color: var(--color-red-50);
}
}
@media (hover: hover) {
.hover\:bg-red-700:hover {
background-color: var(--color-red-700);