1.1.1
This commit is contained in:
parent
7073c73196
commit
53c533c3c6
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,8 @@
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
*/build/
|
||||
|
||||
**/node_modules/
|
||||
**/build/
|
||||
.DS_Store
|
||||
.env
|
||||
File diff suppressed because one or more lines are too long
28
backend/src/db/api/studentassessment.js
Normal file
28
backend/src/db/api/studentassessment.js
Normal file
@ -0,0 +1,28 @@
|
||||
const db = require('../models');
|
||||
|
||||
/**
|
||||
* Bulk create student assessment records for import.
|
||||
* data: array of items with course, student, assessment_name, score, max_score, date
|
||||
* options.currentUser.id is used as createdById
|
||||
*/
|
||||
async function bulkCreate(data, options) {
|
||||
if (!data || !data.length) {
|
||||
return [];
|
||||
}
|
||||
const { currentUser } = options;
|
||||
const records = data.map(item => ({
|
||||
courseId: item.course,
|
||||
studentId: item.student,
|
||||
assessment_name: item.assessment_name,
|
||||
score: item.score,
|
||||
max_score: item.max_score,
|
||||
date: item.date,
|
||||
importHash: null,
|
||||
createdById: currentUser.id,
|
||||
}));
|
||||
return await db.studentassessment.bulkCreate(records);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bulkCreate,
|
||||
};
|
||||
1
frontend/json/runtimeError.json
Normal file
1
frontend/json/runtimeError.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
124
frontend/src/pages/course-files.tsx
Normal file
124
frontend/src/pages/course-files.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
SectionTitle,
|
||||
FormFilePicker,
|
||||
BaseButton,
|
||||
LoadingSpinner,
|
||||
NotificationBar,
|
||||
} from '../components';
|
||||
|
||||
// CAA-mandated document sections
|
||||
export const sections = [
|
||||
'Course Specification',
|
||||
'Teaching & Delivery Plan',
|
||||
'Assessment Plan',
|
||||
'Sample Assessments & Solutions',
|
||||
'Grading Rubrics',
|
||||
'Student Performance Report',
|
||||
'Moderation Report',
|
||||
'Verification Report',
|
||||
'Course Report & Recommendations',
|
||||
'Evidence of Continuous Improvement',
|
||||
] as const;
|
||||
|
||||
type SectionKey = typeof sections[number];
|
||||
|
||||
interface FileRecord {
|
||||
id: string;
|
||||
sectionType: SectionKey;
|
||||
filename: string;
|
||||
status: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
const CourseFilesPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { courseId } = router.query;
|
||||
const [files, setFiles] = useState<FileRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!courseId) return;
|
||||
fetchFiles();
|
||||
}, [courseId]);
|
||||
|
||||
const fetchFiles = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(`/api/course_files?courseId=${courseId}`);
|
||||
setFiles(res.data);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Could not load files');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (section: SectionKey, file: File) => {
|
||||
setLoading(true);
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('courseId', String(courseId));
|
||||
form.append('sectionType', section);
|
||||
try {
|
||||
await axios.post('/api/course_files', form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
await fetchFiles();
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Upload failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <NotificationBar message={error} type="error" />;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<SectionTitle title="Manage Course Files" />
|
||||
<div className="mb-2 text-sm text-gray-600">Course ID: {courseId}</div>
|
||||
<table className="w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="text-left px-2 py-1">Section</th>
|
||||
<th className="text-left px-2 py-1">Status</th>
|
||||
<th className="text-left px-2 py-1">File</th>
|
||||
<th className="text-left px-2 py-1">Comments</th>
|
||||
<th className="px-2 py-1">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sections.map((section) => {
|
||||
const record = files.find((f) => f.sectionType === section);
|
||||
return (
|
||||
<tr key={section} className="border-t">
|
||||
<td className="px-2 py-1">{section}</td>
|
||||
<td className="px-2 py-1">{record?.status || 'Not submitted'}</td>
|
||||
<td className="px-2 py-1">{record?.filename || '-'}</td>
|
||||
<td className="px-2 py-1">{record?.notes || '-'}</td>
|
||||
<td className="px-2 py-1">
|
||||
<FormFilePicker
|
||||
onFileSelected={(file) => handleUpload(section, file)}
|
||||
buttonText={record ? 'Replace' : 'Upload'}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="mt-4">
|
||||
<BaseButton onClick={fetchFiles}>Refresh</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseFilesPage;
|
||||
Loading…
x
Reference in New Issue
Block a user