lms/resources/js/pages/course-player/forms/assignment-submission.tsx
2025-12-15 12:26:23 +01:00

312 lines
14 KiB
TypeScript

import InputError from '@/components/input-error';
import LoadingButton from '@/components/loading-button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { useForm } from '@inertiajs/react';
import { AlertCircle, Calendar, CheckCircle2, Clock, FileText, Upload, XCircle } from 'lucide-react';
import { useState } from 'react';
import { Editor } from 'richtor';
import 'richtor/styles';
interface Props {
assignment: CourseAssignment;
submissions?: AssignmentSubmission[];
}
const AssignmentSubmission = ({ assignment, submissions = [] }: Props) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const latestSubmission = submissions.length > 0 ? submissions[0] : null;
const { data, setData, post, reset, errors, processing } = useForm({
course_assignment_id: assignment.id,
submission_text: '',
attachment: null as File | null,
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null;
setSelectedFile(file);
setData('attachment', file);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
post('/dashboard/assignment/submission', {
onSuccess: () => {
reset();
setSelectedFile(null);
},
});
};
const isDeadlinePassed = assignment.deadline ? new Date(assignment.deadline) < new Date() : false;
const isLateDeadlinePassed = assignment.late_deadline ? new Date(assignment.late_deadline) < new Date() : false;
const canSubmit = !isDeadlinePassed || (assignment.late_submission && !isLateDeadlinePassed);
const remainingAttempts = assignment.retake - submissions.length;
const getStatusBadge = (status: string) => {
switch (status) {
case 'graded':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">
<CheckCircle2 className="h-3 w-3" />
Graded
</span>
);
case 'pending':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-700">
<Clock className="h-3 w-3" />
Pending
</span>
);
case 'late':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-1 text-xs font-medium text-orange-700">
<AlertCircle className="h-3 w-3" />
Late Submission
</span>
);
default:
return (
<span className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">{status}</span>
);
}
};
return (
<div className="mx-auto max-w-4xl space-y-6 p-6">
{/* Assignment Details */}
<Card>
<CardHeader>
<CardTitle>{assignment.title}</CardTitle>
<CardDescription>
{assignment.summary && <div className="prose prose-sm mt-2 max-w-none" dangerouslySetInnerHTML={{ __html: assignment.summary }} />}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
<div className="flex items-center gap-2">
<Calendar className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-sm font-medium">Deadline</p>
<p className="text-muted-foreground text-sm">
{assignment.deadline ? new Date(assignment.deadline).toLocaleString() : 'No deadline'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<FileText className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-sm font-medium">Total Marks</p>
<p className="text-muted-foreground text-sm">
{assignment.total_mark} (Pass: {assignment.pass_mark})
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Clock className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-sm font-medium">Attempts</p>
<p className="text-muted-foreground text-sm">
{submissions.length} / {assignment.retake}
</p>
</div>
</div>
{assignment.late_submission && (
<div className="flex items-center gap-2">
<AlertCircle className="text-muted-foreground h-4 w-4" />
<div>
<p className="text-sm font-medium">Late Deadline</p>
<p className="text-muted-foreground text-sm">
{assignment.late_deadline ? new Date(assignment.late_deadline).toLocaleString() : 'N/A'}
</p>
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Latest Submission Status */}
{latestSubmission && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Latest Submission</AlertTitle>
<AlertDescription>
<div className="mt-2 space-y-2">
<div className="flex items-center justify-between">
<span>Status: {getStatusBadge(latestSubmission.status)}</span>
<span className="text-muted-foreground text-sm">
Attempt {latestSubmission.attempt_number} of {assignment.retake}
</span>
</div>
{latestSubmission.marks_obtained !== null && (
<p className="text-sm">
<strong>Grade:</strong> {latestSubmission.marks_obtained} / {assignment.total_mark}
</p>
)}
{latestSubmission.instructor_feedback && (
<div className="bg-muted rounded-md p-3">
<p className="text-sm font-medium">Instructor Feedback:</p>
<p className="text-muted-foreground text-sm">{latestSubmission.instructor_feedback}</p>
</div>
)}
</div>
</AlertDescription>
</Alert>
)}
{/* Submission Form */}
{canSubmit && remainingAttempts > 0 ? (
<Card>
<CardHeader>
<CardTitle>Submit Assignment</CardTitle>
<CardDescription>
{isDeadlinePassed && assignment.late_submission
? `Late submission allowed until ${new Date(assignment.late_deadline!).toLocaleString()}`
: `You have ${remainingAttempts} attempt(s) remaining`}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="submission_text">Your Answer</Label>
<Editor
ssr={true}
output="html"
placeholder={{
paragraph: 'Type your assignment answer here...',
imageCaption: 'Add caption (optional)',
}}
contentMinHeight={200}
contentMaxHeight={500}
initialContent={data.submission_text}
onContentChange={(value) =>
setData((prev) => ({
...prev,
submission_text: value as string,
}))
}
/>
<InputError message={errors.submission_text} />
</div>
<div>
<Label htmlFor="attachment">Attach File (Optional)</Label>
<div className="mt-2">
<label
htmlFor="attachment"
className="flex cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 px-6 py-8 hover:border-gray-400 hover:bg-gray-100"
>
<div className="space-y-2 text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="text-sm text-gray-600">
<span className="font-semibold">Click to upload</span> or drag and drop
</div>
<p className="text-xs text-gray-500">PDF, DOC, DOCX, TXT, ZIP, JPG, PNG (MAX 10MB)</p>
</div>
<input
id="attachment"
type="file"
className="hidden"
onChange={handleFileChange}
accept=".pdf,.doc,.docx,.txt,.zip,.jpg,.jpeg,.png"
/>
</label>
{selectedFile && (
<div className="bg-muted mt-2 flex items-center justify-between rounded-md p-3">
<span className="text-sm">{selectedFile.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setSelectedFile(null);
setData('attachment', null);
}}
>
<XCircle className="h-4 w-4" />
</Button>
</div>
)}
</div>
<InputError message={errors.attachment} />
</div>
<div className="flex justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={() => reset()}>
Reset
</Button>
<LoadingButton loading={processing} disabled={!data.submission_text && !data.attachment}>
Submit Assignment
</LoadingButton>
</div>
</form>
</CardContent>
</Card>
) : (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Cannot Submit</AlertTitle>
<AlertDescription>
{remainingAttempts <= 0 ? 'You have used all your attempts for this assignment.' : 'The deadline for this assignment has passed.'}
</AlertDescription>
</Alert>
)}
{/* Submission History */}
{submissions.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Submission History</CardTitle>
<CardDescription>View all your previous submissions</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{submissions.map((submission, index) => (
<div key={submission.id}>
{index > 0 && <Separator className="my-4" />}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Attempt {submission.attempt_number}</p>
<p className="text-muted-foreground text-sm">
Submitted: {submission.submitted_at ? new Date(submission.submitted_at).toLocaleString() : 'N/A'}
</p>
</div>
{getStatusBadge(submission.status)}
</div>
{submission.attachment_name && (
<div className="flex items-center gap-2 text-sm">
<FileText className="h-4 w-4" />
<span>{submission.attachment_name}</span>
</div>
)}
{submission.marks_obtained !== null && (
<div className="bg-muted rounded-md p-3">
<p className="text-sm font-medium">
Grade: {submission.marks_obtained} / {assignment.total_mark}
</p>
{submission.instructor_feedback && (
<p className="text-muted-foreground mt-1 text-sm">{submission.instructor_feedback}</p>
)}
</div>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
};
export default AssignmentSubmission;