adding new src
This commit is contained in:
126
src/App.tsx
126
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<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":
|
||||
|
||||
262
src/NOTIFICATIONS_AND_APPROVAL_SYSTEM.md
Normal file
262
src/NOTIFICATIONS_AND_APPROVAL_SYSTEM.md
Normal 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.).
|
||||
308
src/components/NotificationPanel.tsx
Normal file
308
src/components/NotificationPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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()}
|
||||
|
||||
|
||||
525
src/components/pages/ProfilerApproval.tsx
Normal file
525
src/components/pages/ProfilerApproval.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user