2025-09-26 19:45:02 +05:30
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
import { AuthenticatedLayout } from '../layout/AuthenticatedLayout';
|
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
|
|
|
|
|
import { Button } from '../ui/button';
|
|
|
|
|
|
import { Badge } from '../ui/badge';
|
|
|
|
|
|
import { Input } from '../ui/input';
|
|
|
|
|
|
import { Label } from '../ui/label';
|
|
|
|
|
|
import { Textarea } from '../ui/textarea';
|
|
|
|
|
|
import { Checkbox } from '../ui/checkbox';
|
|
|
|
|
|
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from '../ui/select';
|
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
|
|
|
|
|
import { Progress } from '../ui/progress';
|
|
|
|
|
|
import { Separator } from '../ui/separator';
|
2025-10-29 19:21:35 +05:30
|
|
|
|
import { toast } from "sonner";
|
2025-09-26 19:45:02 +05:30
|
|
|
|
import {
|
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
|
ChevronRight,
|
|
|
|
|
|
Calendar,
|
|
|
|
|
|
Users,
|
|
|
|
|
|
FileUp,
|
|
|
|
|
|
Download,
|
|
|
|
|
|
AlertCircle,
|
|
|
|
|
|
Clock,
|
|
|
|
|
|
MapPin,
|
|
|
|
|
|
User,
|
|
|
|
|
|
Building2,
|
|
|
|
|
|
CheckCircle
|
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
import { klcMockData } from '../../data/mockData';
|
2025-10-29 19:21:35 +05:30
|
|
|
|
import { Route } from '../../types/routes';
|
2025-09-26 19:45:02 +05:30
|
|
|
|
|
|
|
|
|
|
interface CourseAssignmentProps {
|
|
|
|
|
|
courseId: string;
|
2025-10-29 19:21:35 +05:30
|
|
|
|
onNavigate: (route: Route) => void;
|
2025-09-26 19:45:02 +05:30
|
|
|
|
onLogout: () => void;
|
|
|
|
|
|
user: any;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface AssignmentData {
|
|
|
|
|
|
type: 'course';
|
|
|
|
|
|
itemId: string;
|
|
|
|
|
|
scope: 'organization' | 'individual';
|
|
|
|
|
|
organizationId?: string;
|
|
|
|
|
|
userId?: string;
|
|
|
|
|
|
hrContacts: string[];
|
|
|
|
|
|
startDate: string;
|
|
|
|
|
|
endDate: string;
|
|
|
|
|
|
completionDate?: string;
|
|
|
|
|
|
maxParticipants: number;
|
|
|
|
|
|
integrationId?: string;
|
|
|
|
|
|
flags: {
|
|
|
|
|
|
allowAddFeedbackGiver: boolean;
|
|
|
|
|
|
hrAccessReports: boolean;
|
|
|
|
|
|
hrAccessCertificate: boolean;
|
|
|
|
|
|
onlineProgram: boolean;
|
|
|
|
|
|
blockSystemEmails: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
participantsSource: 'directory' | 'upload';
|
|
|
|
|
|
participants: string[];
|
|
|
|
|
|
participantsFile?: File;
|
|
|
|
|
|
feedbackGiversFile?: File;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Mock integration options
|
|
|
|
|
|
const integrationOptions = [
|
|
|
|
|
|
{ id: 'int_001', name: 'Microsoft Teams' },
|
|
|
|
|
|
{ id: 'int_002', name: 'Zoom Workplace' },
|
|
|
|
|
|
{ id: 'int_003', name: 'Google Workspace' },
|
|
|
|
|
|
{ id: 'int_004', name: 'Slack Enterprise' },
|
|
|
|
|
|
{ id: 'int_005', name: 'Custom API' }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
export function CourseAssignment({ courseId, onNavigate, onLogout, user }: CourseAssignmentProps) {
|
|
|
|
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
|
|
|
|
const [assignmentData, setAssignmentData] = useState<AssignmentData>({
|
|
|
|
|
|
type: 'course',
|
|
|
|
|
|
itemId: courseId,
|
|
|
|
|
|
scope: 'organization',
|
|
|
|
|
|
hrContacts: [],
|
|
|
|
|
|
startDate: '',
|
|
|
|
|
|
endDate: '',
|
|
|
|
|
|
maxParticipants: 25,
|
|
|
|
|
|
flags: {
|
|
|
|
|
|
allowAddFeedbackGiver: false,
|
|
|
|
|
|
hrAccessReports: false,
|
|
|
|
|
|
hrAccessCertificate: false,
|
|
|
|
|
|
onlineProgram: false,
|
|
|
|
|
|
blockSystemEmails: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
participantsSource: 'directory',
|
|
|
|
|
|
participants: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Get course data
|
|
|
|
|
|
const course = klcMockData.courses?.find(c => c.id === courseId);
|
|
|
|
|
|
|
|
|
|
|
|
// Mock organizations and users data
|
|
|
|
|
|
const organizations = klcMockData.users?.organizations || [];
|
|
|
|
|
|
const [filteredLearners, setFilteredLearners] = useState<any[]>([]);
|
|
|
|
|
|
const [selectedParticipants, setSelectedParticipants] = useState<string[]>([]);
|
|
|
|
|
|
const [participantsTab, setParticipantsTab] = useState('directory');
|
|
|
|
|
|
const [hrContactInput, setHrContactInput] = useState('');
|
|
|
|
|
|
|
|
|
|
|
|
// Load learners when organization changes
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (assignmentData.organizationId) {
|
|
|
|
|
|
// Mock learners for selected organization
|
|
|
|
|
|
const mockLearners = [
|
|
|
|
|
|
{ id: 'usr_001', name: 'Ravi Kumar', email: 'ravi.kumar@org.example', department: 'Technology' },
|
|
|
|
|
|
{ id: 'usr_002', name: 'Priya Sharma', email: 'priya.sharma@org.example', department: 'Marketing' },
|
|
|
|
|
|
{ id: 'usr_003', name: 'Amit Singh', email: 'amit.singh@org.example', department: 'Operations' },
|
|
|
|
|
|
{ id: 'usr_004', name: 'Sneha Patel', email: 'sneha.patel@org.example', department: 'HR' },
|
|
|
|
|
|
{ id: 'usr_005', name: 'Rajesh Kumar', email: 'rajesh.kumar@org.example', department: 'Finance' }
|
|
|
|
|
|
];
|
|
|
|
|
|
setFilteredLearners(mockLearners);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [assignmentData.organizationId]);
|
|
|
|
|
|
|
|
|
|
|
|
const updateAssignmentData = (updates: Partial<AssignmentData>) => {
|
|
|
|
|
|
setAssignmentData(prev => ({ ...prev, ...updates }));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const validateStep = (step: number): boolean => {
|
|
|
|
|
|
switch (step) {
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
if (assignmentData.scope === 'organization' && !assignmentData.organizationId) {
|
|
|
|
|
|
toast.error("Please select an organization");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assignmentData.scope === 'individual' && !assignmentData.userId) {
|
|
|
|
|
|
toast.error("Please select a user");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assignmentData.hrContacts.length === 0) {
|
|
|
|
|
|
toast.error("Please enter at least one HR contact");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assignmentData.maxParticipants <= 0) {
|
|
|
|
|
|
toast.error("Max participants must be greater than 0");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
if (!assignmentData.startDate) {
|
|
|
|
|
|
toast.error("Please select a start date");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!assignmentData.endDate) {
|
|
|
|
|
|
toast.error("Please select an end date");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (new Date(assignmentData.endDate) < new Date(assignmentData.startDate)) {
|
|
|
|
|
|
toast.error("End date must be after start date");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assignmentData.completionDate && new Date(assignmentData.completionDate) < new Date(assignmentData.endDate)) {
|
|
|
|
|
|
toast.error("Completion date must be after end date");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
case 3:
|
|
|
|
|
|
if (participantsTab === 'directory' && selectedParticipants.length === 0) {
|
|
|
|
|
|
toast.error("Please select at least one participant");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selectedParticipants.length > assignmentData.maxParticipants) {
|
|
|
|
|
|
toast.error(`Cannot exceed maximum of ${assignmentData.maxParticipants} participants`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNext = () => {
|
|
|
|
|
|
if (validateStep(currentStep)) {
|
|
|
|
|
|
setCurrentStep(prev => Math.min(prev + 1, 4));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
|
setCurrentStep(prev => Math.max(prev - 1, 1));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleAssign = () => {
|
|
|
|
|
|
if (validateStep(3)) {
|
|
|
|
|
|
toast.success("Assignment created.");
|
|
|
|
|
|
// Navigate to assignment details or back to courses
|
|
|
|
|
|
onNavigate('/courses');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const addHrContact = () => {
|
|
|
|
|
|
if (hrContactInput.trim()) {
|
|
|
|
|
|
updateAssignmentData({
|
|
|
|
|
|
hrContacts: [...assignmentData.hrContacts, hrContactInput.trim()]
|
|
|
|
|
|
});
|
|
|
|
|
|
setHrContactInput('');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeHrContact = (index: number) => {
|
|
|
|
|
|
const newContacts = assignmentData.hrContacts.filter((_, i) => i !== index);
|
|
|
|
|
|
updateAssignmentData({ hrContacts: newContacts });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleParticipant = (participantId: string) => {
|
|
|
|
|
|
setSelectedParticipants(prev =>
|
|
|
|
|
|
prev.includes(participantId)
|
|
|
|
|
|
? prev.filter(id => id !== participantId)
|
|
|
|
|
|
: [...prev, participantId]
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Check if course has feedback components (profilers)
|
|
|
|
|
|
const hasFeedbackComponents = course?.assessments?.some((assessment: any) =>
|
|
|
|
|
|
assessment.type === 'Profiler' || assessment.title.toLowerCase().includes('360')
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const breadcrumbItems = [
|
|
|
|
|
|
{ label: "Admin", href: "/dashboard" },
|
|
|
|
|
|
{ label: "Courses", href: "/courses" },
|
|
|
|
|
|
{ label: course?.title || "Course", href: `/courses/${courseId}` },
|
|
|
|
|
|
{ label: "Assign", current: true }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const renderStepIndicator = () => (
|
|
|
|
|
|
<div className="flex items-center justify-between mb-8">
|
|
|
|
|
|
{[1, 2, 3, 4].map((step) => (
|
|
|
|
|
|
<div key={step} className="flex items-center">
|
|
|
|
|
|
<div className={`
|
|
|
|
|
|
w-10 h-10 rounded-full flex items-center justify-center border-2 font-medium
|
|
|
|
|
|
${step <= currentStep
|
|
|
|
|
|
? 'bg-[var(--color-brand-primary)] text-white border-[var(--color-brand-primary)]'
|
|
|
|
|
|
: 'bg-background text-muted-foreground border-muted-foreground'}
|
|
|
|
|
|
`}>
|
|
|
|
|
|
{step < currentStep ? <CheckCircle className="h-5 w-5" /> : step}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{step < 4 && (
|
|
|
|
|
|
<div className={`
|
|
|
|
|
|
w-20 h-0.5 mx-2
|
|
|
|
|
|
${step < currentStep ? 'bg-[var(--color-brand-primary)]' : 'bg-muted-foreground'}
|
|
|
|
|
|
`} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const renderSummaryCard = () => (
|
|
|
|
|
|
<Card className="w-80">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg">Course Summary</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Title</Label>
|
|
|
|
|
|
<p className="font-medium">{course?.title}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">ID/Code</Label>
|
|
|
|
|
|
<p className="font-mono text-sm">{course?.id}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Status</Label>
|
|
|
|
|
|
<Badge variant={course?.status === 'Published' ? 'default' : 'secondary'}>
|
|
|
|
|
|
{course?.status}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Owner</Label>
|
|
|
|
|
|
<p>{course?.instructor}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Duration</Label>
|
|
|
|
|
|
<p>{course?.duration}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{course?.linkedIntegrations && course.linkedIntegrations.length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Linked Integrations</Label>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
{course.linkedIntegrations.join(', ')}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const renderStep1 = () => (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="text-lg font-medium mb-4">Who to assign</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-6">
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="assign-to" className="text-base font-medium">Assign to*</Label>
|
|
|
|
|
|
<RadioGroup
|
|
|
|
|
|
value={assignmentData.scope}
|
|
|
|
|
|
onValueChange={(value: 'organization' | 'individual') =>
|
|
|
|
|
|
updateAssignmentData({ scope: value, organizationId: undefined, userId: undefined })
|
|
|
|
|
|
}
|
|
|
|
|
|
className="mt-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<RadioGroupItem value="organization" id="org" />
|
|
|
|
|
|
<Label htmlFor="org">Organization</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<RadioGroupItem value="individual" id="individual" />
|
|
|
|
|
|
<Label htmlFor="individual">Individual</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</RadioGroup>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{assignmentData.scope === 'organization' ? (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="organization-select">Select Organisation*</Label>
|
|
|
|
|
|
<Select value={assignmentData.organizationId} onValueChange={(value) => updateAssignmentData({ organizationId: value })}>
|
|
|
|
|
|
<SelectTrigger className="min-h-[44px]">
|
|
|
|
|
|
<SelectValue placeholder="Choose organization..." />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{organizations.map((org: any) => (
|
|
|
|
|
|
<SelectItem key={org.id} value={org.id}>
|
|
|
|
|
|
{org.name}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="user-select">Select User*</Label>
|
|
|
|
|
|
<Select value={assignmentData.userId} onValueChange={(value) => updateAssignmentData({ userId: value })}>
|
|
|
|
|
|
<SelectTrigger className="min-h-[44px]">
|
|
|
|
|
|
<SelectValue placeholder="Search and select user..." />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{filteredLearners.map((user: any) => (
|
|
|
|
|
|
<SelectItem key={user.id} value={user.id}>
|
|
|
|
|
|
{user.name} ({user.email})
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="hr-contacts">Enter HR Names*</Label>
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={hrContactInput}
|
|
|
|
|
|
onChange={(e) => setHrContactInput(e.target.value)}
|
|
|
|
|
|
placeholder="Enter HR contact name"
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
onKeyPress={(e) => e.key === 'Enter' && addHrContact()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button type="button" onClick={addHrContact} className="min-h-[44px]">Add</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Primary, secondary HR contacts</p>
|
|
|
|
|
|
{assignmentData.hrContacts.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{assignmentData.hrContacts.map((contact, index) => (
|
|
|
|
|
|
<Badge key={index} variant="secondary" className="px-2 py-1">
|
|
|
|
|
|
{contact}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => removeHrContact(index)}
|
|
|
|
|
|
className="ml-2 text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="max-participants">Max. Number of Participants*</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={assignmentData.maxParticipants}
|
|
|
|
|
|
onChange={(e) => updateAssignmentData({ maxParticipants: parseInt(e.target.value) || 0 })}
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="integration">Select Integration</Label>
|
|
|
|
|
|
<Select value={assignmentData.integrationId} onValueChange={(value) => updateAssignmentData({ integrationId: value })}>
|
|
|
|
|
|
<SelectTrigger className="min-h-[44px]">
|
|
|
|
|
|
<SelectValue placeholder="Choose integration..." />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{integrationOptions.map((integration) => (
|
|
|
|
|
|
<SelectItem key={integration.id} value={integration.id}>
|
|
|
|
|
|
{integration.name}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="mt-6">
|
|
|
|
|
|
<Label className="text-base font-medium">Permissions</Label>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4 mt-3">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="feedback-giver"
|
|
|
|
|
|
checked={assignmentData.flags.allowAddFeedbackGiver}
|
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
updateAssignmentData({
|
|
|
|
|
|
flags: { ...assignmentData.flags, allowAddFeedbackGiver: !!checked }
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="feedback-giver" className="text-sm">Allow Participant to add Feedback Giver</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="hr-reports"
|
|
|
|
|
|
checked={assignmentData.flags.hrAccessReports}
|
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
updateAssignmentData({
|
|
|
|
|
|
flags: { ...assignmentData.flags, hrAccessReports: !!checked }
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="hr-reports" className="text-sm">Allow HR to Access Reports</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="hr-certificate"
|
|
|
|
|
|
checked={assignmentData.flags.hrAccessCertificate}
|
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
updateAssignmentData({
|
|
|
|
|
|
flags: { ...assignmentData.flags, hrAccessCertificate: !!checked }
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="hr-certificate" className="text-sm">Allow HR to Access Certificate</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="online-program"
|
|
|
|
|
|
checked={assignmentData.flags.onlineProgram}
|
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
updateAssignmentData({
|
|
|
|
|
|
flags: { ...assignmentData.flags, onlineProgram: !!checked }
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="online-program" className="text-sm">Online Program</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="block-emails"
|
|
|
|
|
|
checked={assignmentData.flags.blockSystemEmails}
|
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
updateAssignmentData({
|
|
|
|
|
|
flags: { ...assignmentData.flags, blockSystemEmails: !!checked }
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="block-emails" className="text-sm">Block System Generated Emails</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{assignmentData.flags.blockSystemEmails && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
|
|
|
|
No onboarding emails will be sent for this assignment.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const renderStep2 = () => (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="text-lg font-medium mb-4">Schedule & Completion</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
|
|
|
|
<div className="flex items-start gap-2">
|
|
|
|
|
|
<AlertCircle className="h-5 w-5 text-blue-600 mt-0.5" />
|
|
|
|
|
|
<p className="text-sm text-blue-800">
|
|
|
|
|
|
Dates apply to this assignment and are not set on the master course.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-3 gap-6 max-w-2xl">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="start-date">Start Date*</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={assignmentData.startDate}
|
|
|
|
|
|
onChange={(e) => updateAssignmentData({ startDate: e.target.value })}
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="end-date">End Date*</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={assignmentData.endDate}
|
|
|
|
|
|
onChange={(e) => updateAssignmentData({ endDate: e.target.value })}
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="completion-date">Completion Date</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={assignmentData.completionDate || ''}
|
|
|
|
|
|
onChange={(e) => updateAssignmentData({ completionDate: e.target.value })}
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const renderStep3 = () => (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="text-lg font-medium mb-4">Participants (and Feedback Givers)</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-6">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Tabs value={participantsTab} onValueChange={setParticipantsTab}>
|
|
|
|
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
|
|
|
|
<TabsTrigger value="directory">Add from Directory</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="upload">Upload</TabsTrigger>
|
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="directory" className="space-y-4">
|
|
|
|
|
|
{assignmentData.scope === 'organization' ? (
|
|
|
|
|
|
assignmentData.organizationId ? (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Select participants from {organizations.find(o => o.id === assignmentData.organizationId)?.name}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{filteredLearners.map((learner) => (
|
|
|
|
|
|
<div key={learner.id} className="flex items-center space-x-2 p-2 border rounded">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={learner.id}
|
|
|
|
|
|
checked={selectedParticipants.includes(learner.id)}
|
|
|
|
|
|
onCheckedChange={() => toggleParticipant(learner.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<Label htmlFor={learner.id} className="font-medium">{learner.name}</Label>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">{learner.email} • {learner.department}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground py-4">
|
|
|
|
|
|
Select an organization to browse learners.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="py-4">
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Individual user will be automatically added as participant.</p>
|
|
|
|
|
|
{assignmentData.userId && (
|
|
|
|
|
|
<Badge variant="secondary" className="mt-2">
|
|
|
|
|
|
{filteredLearners.find(u => u.id === assignmentData.userId)?.name || 'Selected User'}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="upload" className="space-y-4">
|
|
|
|
|
|
<Card className="border-dashed border-2">
|
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-6">
|
|
|
|
|
|
<FileUp className="h-8 w-8 text-muted-foreground mb-2" />
|
|
|
|
|
|
<Button variant="outline" className="mb-2">
|
|
|
|
|
|
Choose file
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Upload participant list</p>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
<Button variant="link" className="p-0 h-auto">
|
|
|
|
|
|
<Download className="h-4 w-4 mr-2" />
|
|
|
|
|
|
Download Participants Template
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{hasFeedbackComponents && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-base font-medium">Feedback Givers</Label>
|
|
|
|
|
|
<Card className="border-dashed border-2 mt-3">
|
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-6">
|
|
|
|
|
|
<FileUp className="h-8 w-8 text-muted-foreground mb-2" />
|
|
|
|
|
|
<Button variant="outline" className="mb-2">
|
|
|
|
|
|
Choose file
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Upload feedback givers list</p>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
<Button variant="link" className="p-0 h-auto mt-2">
|
|
|
|
|
|
<Download className="h-4 w-4 mr-2" />
|
|
|
|
|
|
Download Feedback Givers Template
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between py-3 px-4 bg-muted/30 rounded-lg">
|
|
|
|
|
|
<span className="text-sm font-medium">
|
|
|
|
|
|
Participants selected: {selectedParticipants.length} (Max {assignmentData.maxParticipants})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const renderStep4 = () => (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 className="text-lg font-medium mb-4">Review & Assign</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-6 max-w-4xl">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-6">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Course Name</Label>
|
|
|
|
|
|
<p className="font-medium">{course?.title}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">
|
|
|
|
|
|
{assignmentData.scope === 'organization' ? 'Select Organisation' : 'Select User'}
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
|
{assignmentData.scope === 'organization'
|
|
|
|
|
|
? organizations.find(o => o.id === assignmentData.organizationId)?.name
|
|
|
|
|
|
: filteredLearners.find(u => u.id === assignmentData.userId)?.name
|
|
|
|
|
|
}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">HR Names</Label>
|
|
|
|
|
|
<p className="font-medium">{assignmentData.hrContacts.join(', ')}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Max. Number of Participants</Label>
|
|
|
|
|
|
<p className="font-medium">{assignmentData.maxParticipants}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Start Date • End Date</Label>
|
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
|
{assignmentData.startDate} • {assignmentData.endDate}
|
|
|
|
|
|
{assignmentData.completionDate && ` • ${assignmentData.completionDate}`}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Select Integration</Label>
|
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
|
{assignmentData.integrationId
|
|
|
|
|
|
? integrationOptions.find(i => i.id === assignmentData.integrationId)?.name
|
|
|
|
|
|
: 'None selected'
|
|
|
|
|
|
}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Permissions</Label>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
|
|
|
|
<p className="text-sm">Allow Participant to add Feedback Giver: <span className="font-medium">{assignmentData.flags.allowAddFeedbackGiver ? 'Yes' : 'No'}</span></p>
|
|
|
|
|
|
<p className="text-sm">Allow HR to Access Reports: <span className="font-medium">{assignmentData.flags.hrAccessReports ? 'Yes' : 'No'}</span></p>
|
|
|
|
|
|
<p className="text-sm">Allow HR to Access Certificate: <span className="font-medium">{assignmentData.flags.hrAccessCertificate ? 'Yes' : 'No'}</span></p>
|
|
|
|
|
|
<p className="text-sm">Online Program: <span className="font-medium">{assignmentData.flags.onlineProgram ? 'Yes' : 'No'}</span></p>
|
|
|
|
|
|
<p className="text-sm">Block System Generated Emails: <span className="font-medium">{assignmentData.flags.blockSystemEmails ? 'Yes' : 'No'}</span></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground">Participants</Label>
|
|
|
|
|
|
<p className="font-medium">
|
|
|
|
|
|
{participantsTab === 'directory'
|
|
|
|
|
|
? `${selectedParticipants.length} selected from directory`
|
|
|
|
|
|
: 'Via upload'
|
|
|
|
|
|
}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{hasFeedbackComponents && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Label className="text-sm font-medium text-muted-foreground mt-2 block">Feedback Givers</Label>
|
|
|
|
|
|
<p className="font-medium">Via upload</p>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const stepTitles = [
|
|
|
|
|
|
'Who to assign',
|
|
|
|
|
|
'Schedule & Completion',
|
|
|
|
|
|
'Participants (and Feedback Givers)',
|
|
|
|
|
|
'Review & Assign'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<AuthenticatedLayout
|
|
|
|
|
|
user={user}
|
|
|
|
|
|
onLogout={onLogout}
|
|
|
|
|
|
currentRoute={`/courses/${courseId}/assign`}
|
|
|
|
|
|
breadcrumbItems={breadcrumbItems}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="text-2xl font-medium">Assignment — Course</h1>
|
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
|
Assign course to organizations or individuals
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-6">
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<CardTitle>{stepTitles[currentStep - 1]}</CardTitle>
|
|
|
|
|
|
<Badge variant="outline">Step {currentStep} of 4</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{renderStepIndicator()}
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
{currentStep === 1 && renderStep1()}
|
|
|
|
|
|
{currentStep === 2 && renderStep2()}
|
|
|
|
|
|
{currentStep === 3 && renderStep3()}
|
|
|
|
|
|
{currentStep === 4 && renderStep4()}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
<div className="flex items-center justify-between px-6 py-4 border-t">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={handleBack}
|
|
|
|
|
|
disabled={currentStep === 1}
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronLeft className="h-4 w-4 mr-2" />
|
|
|
|
|
|
Back
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
{currentStep < 4 ? (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={handleNext}
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Next
|
|
|
|
|
|
<ChevronRight className="h-4 w-4 ml-2" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={handleAssign}
|
|
|
|
|
|
className="min-h-[44px]"
|
|
|
|
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Assign
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{renderSummaryCard()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</AuthenticatedLayout>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|