This repository has been archived on 2026-04-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
KLC-Admin-Panel-Frontend-Fi…/src/components/pages/CourseAssignment.tsx
2025-10-29 19:21:35 +05:30

807 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import { toast } from "sonner";
import {
ChevronLeft,
ChevronRight,
Calendar,
Users,
FileUp,
Download,
AlertCircle,
Clock,
MapPin,
User,
Building2,
CheckCircle
} from 'lucide-react';
import { klcMockData } from '../../data/mockData';
import { Route } from '../../types/routes';
interface CourseAssignmentProps {
courseId: string;
onNavigate: (route: Route) => void;
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>
);
}