Skip to content

Commit 3bb45c7

Browse files
authored
Merge pull request #263 from makeopensource/#87-Attendance-page
#87 attendance page
2 parents 29ba0af + df18334 commit 3bb45c7

File tree

10 files changed

+433
-412
lines changed

10 files changed

+433
-412
lines changed

.DS_Store

0 Bytes
Binary file not shown.

devU-client/src/components/listItems/assignmentProblemListItem.tsx

Lines changed: 49 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,104 +8,95 @@ import FaIcon from 'components/shared/icons/faIcon'
88

99
type Props = {
1010
problem: AssignmentProblem
11-
handleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
11+
handleChange?: (e : React.ChangeEvent<HTMLInputElement>) => void
1212
disabled?: boolean
1313
}
1414

15-
const AssignmentProblemListItem = ({ problem, handleChange, disabled }: Props) => {
16-
const [meta, setMeta] = useState<{ options: { [key: string]: string }, type: string }>()
15+
const AssignmentProblemListItem = ({problem, handleChange, disabled}: Props) => {
16+
const [meta, setMeta] = useState<{options: {[key:string]: string}, type: string}>()
1717

1818

1919
const getMeta = () => {
2020
setMeta(problem.metadata)
2121
}
22-
22+
2323
useEffect(() => {
2424
getMeta()
2525
}, [problem])
26+
2627

27-
28-
if (!meta) {
28+
if (!meta){
2929
return (
30-
<div className={styles.problem}>
31-
<div>File Input Problems are not done yet pending backend changes! :D</div>
32-
</div>)
30+
<div className={styles.problem}>
31+
<div>File Input Problems are not done yet pending backend changes! :D</div>
32+
</div>)
3333
}
3434

35-
const type = meta.type
35+
const type = meta.type
3636

3737
if (type == "Text") {
3838
return (
39-
<div key={problem.id} className={styles.problem}>
40-
<h4 className={styles.problem_header}>{problem.problemName}</h4>
41-
<input className={styles.textField}
42-
type='text'
43-
placeholder='Answer'
44-
onChange={handleChange ?? undefined}
45-
disabled={disabled ?? false}
46-
47-
id={problem.problemName}
39+
<div key={problem.id} className={styles.problem}>
40+
<h4 className={styles.problem_header}>{problem.problemName}</h4>
41+
<input className={styles.textField}
42+
type='text'
43+
placeholder='Answer'
44+
onChange={handleChange ?? undefined}
45+
disabled={disabled ?? false}
46+
47+
id={problem.problemName}
4848
/>
49-
</div>
50-
)
51-
}
52-
53-
else if (type == "MCQ-mult") {
49+
</div>
50+
)}
51+
52+
else if(type == "MCQ-mult") {
5453
const options = meta.options
55-
if (!options) {
54+
if (!options){
5655
return <div></div>
5756
}
5857
return (
5958
<div key={problem.id} className={styles.problem}>
6059
<h4 className={styles.problem_header}>{problem.problemName}</h4>
61-
{Object.keys(options).map((key: string) => (
62-
<label key={key} className={styles.mcqLabel} style={disabled ? { cursor: 'default' } : undefined}>
63-
<input id={problem.problemName}
64-
type='checkbox'
65-
value={key}
66-
onChange={handleChange}
67-
disabled={disabled ?? false} /> {options[key]}
68-
60+
{Object.keys(options).map((key : string) => (
61+
<label key={key} className={styles.mcqLabel} style={disabled ? {cursor: 'default'} : undefined}>
62+
<input id={problem.problemName}
63+
type='checkbox'
64+
value={key}
65+
onChange={handleChange}
66+
disabled={disabled ?? false}/> {options[key]}
67+
6968
<span className={styles.checkbox}>
70-
<FaIcon icon='check' className={styles.checkboxCheck} />
69+
<FaIcon icon='check' className={styles.checkboxCheck}/>
7170
</span>{/* custom checkbox */}
7271
</label>))}
7372
</div>)
74-
}
75-
76-
else if (type == "MCQ-single") {
73+
}
74+
75+
else if(type == "MCQ-single") {
7776
const options = meta.options
78-
if (!options) {
77+
if (!options){
7978
return <div></div>
8079
}
8180
return (
8281
<div key={problem.id} className={styles.problem}>
8382
<h4 className={styles.problem_header}>{problem.problemName}</h4>
84-
{Object.keys(options).map((key: string) => (
85-
<label key={key} className={styles.mcqLabel} style={disabled ? { cursor: 'default' } : undefined}>
86-
<input id={problem.problemName}
87-
type='radio'
88-
name={`${problem.id}_answer`}
89-
value={key}
90-
onChange={handleChange}
91-
disabled={disabled ?? false} /> {options[key]}
83+
{Object.keys(options).map((key : string) => (
84+
<label key={key} className={styles.mcqLabel} style={disabled ? {cursor: 'default'} : undefined}>
85+
<input id={problem.problemName}
86+
type='radio'
87+
name={`${problem.id}_answer`}
88+
value={key}
89+
onChange={handleChange}
90+
disabled={disabled ?? false}/> {options[key]}
9291
<span className={styles.radio}></span>{/* custom radio button */}
9392
</label>))}
9493
</div>)
95-
}
96-
97-
else if (type == "File") {
98-
return (
99-
<div key={problem.id} className={styles.problem}>
100-
<h4 className={styles.problem_header}>{problem.problemName}</h4>
101-
</div>
102-
)
103-
}
104-
94+
}
95+
10596
else {
106-
return (
97+
return(
10798
<div>Unknown type, something is wrong on the backend!</div>)
108-
}
99+
}
109100
}
110101

111102

devU-client/src/components/pages/Attendence/InstructorAttendanceModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,5 @@ const InstructorAttendanceModal: React.FC<Props> = ({ open, onClose, onSubmit, c
124124
);
125125
};
126126

127+
127128
export default InstructorAttendanceModal;

devU-client/src/components/pages/Attendence/InstructorAttendancePage.tsx

Lines changed: 114 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,110 @@ interface AttendanceRecord {
2020
code: string;
2121
duration: string;
2222
description?: string;
23+
id: string; // Required ID for records
24+
isLocal?: boolean; // Flag to identify locally created records
2325
}
2426

2527
const InstructorAttendancePage: React.FC<Props> = () => {
2628
const { courseId } = useParams<{ courseId: string }>();
2729
const [modalOpen, setModalOpen] = useState(false);
2830
const [courseInfo, setCourseInfo] = useState<CourseInfo | null>(null);
2931
const [attendanceRecords, setAttendanceRecords] = useState<AttendanceRecord[]>([]);
32+
const [isLoading, setIsLoading] = useState(true);
3033

34+
// Load course info
3135
useEffect(() => {
3236
if (!courseId) return;
3337

38+
setIsLoading(true);
3439
RequestService.get(`/api/courses/${courseId}`)
3540
.then(data => {
36-
setCourseInfo({
41+
const course = {
3742
id: data.id,
3843
number: data.number,
3944
name: data.name,
4045
semester: data.semester
41-
});
46+
};
47+
setCourseInfo(course);
4248
})
43-
.catch(err => console.error('Error fetching course info:', err));
49+
.catch(err => {
50+
console.error('Error fetching course info:', err);
51+
setIsLoading(false);
52+
});
4453
}, [courseId]);
4554

55+
// Load attendance records - combine API records and localStorage records
4656
useEffect(() => {
4757
if (!courseInfo?.id) return;
4858

59+
// First, get records from API
4960
RequestService.get(`/api/courses/${courseInfo.id}/attendance`)
50-
.then(data => setAttendanceRecords(data))
51-
.catch(err => console.error('Failed to load attendance:', err));
61+
.then(apiRecords => {
62+
// Make sure API records have IDs
63+
const formattedApiRecords = apiRecords.map((record: any, index: number) => ({
64+
...record,
65+
id: record.id || `api-${index}-${Date.now()}`
66+
}));
67+
68+
// Then, get local records from localStorage
69+
const localStorageKey = `attendance_${courseInfo.id}`;
70+
const localRecordsString = localStorage.getItem(localStorageKey);
71+
let localRecords: AttendanceRecord[] = [];
72+
73+
if (localRecordsString) {
74+
try {
75+
localRecords = JSON.parse(localRecordsString);
76+
} catch (e) {
77+
console.error('Error parsing local attendance records:', e);
78+
localStorage.removeItem(localStorageKey); // Clear invalid data
79+
}
80+
}
81+
82+
// Combine and set records
83+
const allRecords = [...localRecords, ...formattedApiRecords];
84+
setAttendanceRecords(allRecords);
85+
setIsLoading(false);
86+
})
87+
.catch(err => {
88+
console.error('Failed to load attendance from API:', err);
89+
90+
// Still try to load local records on API failure
91+
const localStorageKey = `attendance_${courseInfo.id}`;
92+
const localRecordsString = localStorage.getItem(localStorageKey);
93+
if (localRecordsString) {
94+
try {
95+
const localRecords = JSON.parse(localRecordsString);
96+
setAttendanceRecords(localRecords);
97+
} catch (e) {
98+
console.error('Error parsing local attendance records:', e);
99+
}
100+
}
101+
setIsLoading(false);
102+
});
52103
}, [courseInfo]);
53104

105+
// Save local records to localStorage whenever they change
106+
useEffect(() => {
107+
if (!courseInfo?.id || attendanceRecords.length === 0) return;
108+
109+
// Filter out only local records
110+
const localRecords = attendanceRecords.filter(record => record.isLocal === true);
111+
if (localRecords.length === 0) return;
112+
113+
// Save to localStorage
114+
const localStorageKey = `attendance_${courseInfo.id}`;
115+
localStorage.setItem(localStorageKey, JSON.stringify(localRecords));
116+
}, [attendanceRecords, courseInfo]);
117+
54118
const saveToCsv = () => {
119+
if (!attendanceRecords.length || !courseInfo) return;
120+
55121
const toCSV = [];
56122
let header = 'Course,Date,Code,Duration (min),Description';
57123
toCSV.push(header + '\n');
58124

59125
attendanceRecords.forEach(record => {
60-
const row = `${record.courseInfo.number}: ${record.courseInfo.name},${record.date},${record.code},${record.duration},${record.description || ''}`;
126+
const row = `"${record.courseInfo.number}: ${record.courseInfo.name}","${record.date}","${record.code}","${record.duration}","${record.description || ''}"`;
61127
toCSV.push(row + '\n');
62128
});
63129

@@ -69,14 +135,49 @@ const InstructorAttendancePage: React.FC<Props> = () => {
69135
const encodedUri = encodeURI(final);
70136
const link = document.createElement('a');
71137
link.setAttribute('href', encodedUri);
72-
link.setAttribute('download', `${courseInfo?.number.replace(" ", '').toLowerCase()}_attendance.csv`);
138+
link.setAttribute('download', `${courseInfo.number.replace(/\s+/g, '').toLowerCase()}_attendance.csv`);
73139
document.body.appendChild(link);
74140
link.click();
75141
document.body.removeChild(link);
76142
};
77143

78-
if (!courseInfo) {
79-
return <PageWrapper><p className='info'>Loading course info...</p></PageWrapper>;
144+
const handleAttendanceSubmit = (newSession: any) => {
145+
if (!courseInfo) return;
146+
147+
// Create a new record directly without a POST request
148+
const newRecord: AttendanceRecord = {
149+
id: `local-${Date.now()}`, // Generate a unique local ID
150+
courseInfo: courseInfo,
151+
date: newSession.date,
152+
code: newSession.code,
153+
duration: newSession.duration,
154+
description: newSession.description,
155+
isLocal: true // Mark as locally created
156+
};
157+
158+
// Add the new record to the beginning of the array
159+
setAttendanceRecords(prev => [newRecord, ...prev]);
160+
setModalOpen(false);
161+
162+
// Save this record to localStorage immediately
163+
const localStorageKey = `attendance_${courseInfo.id}`;
164+
const existingRecordsString = localStorage.getItem(localStorageKey);
165+
let existingRecords: AttendanceRecord[] = [];
166+
167+
if (existingRecordsString) {
168+
try {
169+
existingRecords = JSON.parse(existingRecordsString);
170+
} catch (e) {
171+
console.error('Error parsing local attendance records:', e);
172+
}
173+
}
174+
175+
localStorage.setItem(localStorageKey, JSON.stringify([newRecord, ...existingRecords]));
176+
console.log('Attendance record created and saved locally:', newRecord);
177+
};
178+
179+
if (isLoading || !courseInfo) {
180+
return <PageWrapper><p style={{ padding: '2rem' }}>Loading course info...</p></PageWrapper>;
80181
}
81182

82183
return (
@@ -98,33 +199,7 @@ const InstructorAttendancePage: React.FC<Props> = () => {
98199
<InstructorAttendanceModal
99200
open={modalOpen}
100201
onClose={() => setModalOpen(false)}
101-
onSubmit={(newSession) => {
102-
const payload = {
103-
courseId: courseInfo.id,
104-
date: newSession.date,
105-
code: newSession.code,
106-
duration: newSession.duration,
107-
description: newSession.description
108-
};
109-
110-
console.log('Creating attendance with payload:', payload);
111-
112-
RequestService.post(`/api/attendance`, payload)
113-
.then((savedSession) => {
114-
setAttendanceRecords(prev => [
115-
{
116-
...savedSession,
117-
courseInfo
118-
},
119-
...prev
120-
]);
121-
setModalOpen(false);
122-
})
123-
.catch(err => {
124-
console.error('Failed to save attendance:', err.response?.data || err.message || err);
125-
alert('Could not create attendance. Please try again.');
126-
});
127-
}}
202+
onSubmit={handleAttendanceSubmit}
128203
courseInfo={courseInfo}
129204
/>
130205

@@ -143,8 +218,8 @@ const InstructorAttendancePage: React.FC<Props> = () => {
143218
</tr>
144219
</thead>
145220
<tbody>
146-
{attendanceRecords.map((entry, index) => (
147-
<tr key={`${entry.code}-${index}`}>
221+
{attendanceRecords.map((entry) => (
222+
<tr key={entry.id}>
148223
<td>{entry.courseInfo.number}: {entry.courseInfo.name}</td>
149224
<td>{entry.date}</td>
150225
<td>{entry.code}</td>
@@ -161,5 +236,4 @@ const InstructorAttendancePage: React.FC<Props> = () => {
161236
);
162237
};
163238

164-
165-
export default InstructorAttendancePage;
239+
export default InstructorAttendancePage;

0 commit comments

Comments
 (0)