Skip to content

Commit 85818c5

Browse files
Merge pull request #4580 from OneCommunityGlobal/bhavpreet_ep_down_upload
Bhavpreet: education portal report download
2 parents bb5db2c + f8c8b7f commit 85818c5

7 files changed

Lines changed: 843 additions & 107 deletions

File tree

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
import React, { useState, useCallback, useMemo } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { toast } from 'react-toastify';
4+
import axios from 'axios';
5+
import { ENDPOINTS } from '../../../utils/URL';
6+
import styles from './ReportView.module.css';
7+
8+
const EXPORT_FORMATS = [
9+
{ value: 'pdf', label: 'PDF Document' },
10+
{ value: 'csv', label: 'CSV Spreadsheet' },
11+
];
12+
13+
const ReportDownloadButton = ({
14+
reportType = 'student',
15+
classId = null,
16+
startDate = null,
17+
endDate = null,
18+
onDownloadSuccess = null,
19+
onDownloadError = null,
20+
}) => {
21+
const [isLoading, setIsLoading] = useState(false);
22+
const [selectedFormat, setSelectedFormat] = useState('pdf');
23+
const [error, setError] = useState(null);
24+
25+
const darkMode = useSelector(state => state.theme?.darkMode || false);
26+
const authUser = useSelector(state => state.auth?.user);
27+
const userId = useSelector(state => state.auth?.user?.userid);
28+
const userProfile = useSelector(state => state.userProfile);
29+
30+
const educatorName = useMemo(() => {
31+
if (userProfile?.firstName && userProfile?.lastName) {
32+
return `${userProfile.firstName} ${userProfile.lastName}`;
33+
}
34+
return authUser?.firstName && authUser?.lastName
35+
? `${authUser.firstName} ${authUser.lastName}`
36+
: 'Current User';
37+
}, [userProfile, authUser]);
38+
39+
const formattedReportType = useMemo(
40+
() => reportType.charAt(0).toUpperCase() + reportType.slice(1),
41+
[reportType],
42+
);
43+
44+
const handleDownload = useCallback(async () => {
45+
if (!selectedFormat) {
46+
toast.error('Please select an export format', {
47+
position: 'top-right',
48+
autoClose: 2000,
49+
});
50+
return;
51+
}
52+
53+
// Validate required parameters
54+
if (reportType === 'student' && !userId) {
55+
toast.error('User ID is required for student reports', {
56+
position: 'top-right',
57+
autoClose: 2000,
58+
});
59+
return;
60+
}
61+
62+
if (reportType === 'class' && !classId) {
63+
toast.error('Class ID is required for class reports', {
64+
position: 'top-right',
65+
autoClose: 2000,
66+
});
67+
return;
68+
}
69+
70+
try {
71+
setIsLoading(true);
72+
setError(null);
73+
74+
toast.info(`Generating ${selectedFormat.toUpperCase()} report...`, {
75+
position: 'top-right',
76+
autoClose: 2000,
77+
});
78+
79+
const params = {
80+
studentId: userId,
81+
classId,
82+
startDate,
83+
endDate,
84+
};
85+
86+
const url = ENDPOINTS.EDUCATOR_REPORT_EXPORT(reportType, selectedFormat, params);
87+
88+
// Get token from localStorage or auth state
89+
const token = localStorage.getItem('token') || authUser?.token;
90+
91+
const response = await axios.get(url, {
92+
responseType: 'blob',
93+
headers: {
94+
Authorization: token,
95+
'Content-Type': 'application/json',
96+
},
97+
});
98+
99+
// Create download link
100+
const blob = new Blob([response.data], {
101+
type: selectedFormat === 'pdf' ? 'application/pdf' : 'text/csv',
102+
});
103+
const downloadUrl = window.URL.createObjectURL(blob);
104+
const link = document.createElement('a');
105+
link.href = downloadUrl;
106+
107+
const timestamp = new Date().toISOString().split('T')[0];
108+
link.download = `${reportType}_report_${timestamp}.${selectedFormat}`;
109+
110+
document.body.appendChild(link);
111+
link.click();
112+
document.body.removeChild(link);
113+
window.URL.revokeObjectURL(downloadUrl);
114+
115+
const metadata = {
116+
generatedDate: new Date().toLocaleString(),
117+
educatorName: educatorName,
118+
reportType: reportType,
119+
format: selectedFormat,
120+
fileName: link.download,
121+
};
122+
123+
toast.success(`${formattedReportType} report downloaded successfully!`, {
124+
position: 'top-right',
125+
autoClose: 3000,
126+
});
127+
128+
onDownloadSuccess?.(metadata);
129+
} catch (error) {
130+
console.error('Download error:', error);
131+
132+
let errorMessage = 'Failed to download report. Please try again.';
133+
134+
if (error.response?.status === 401) {
135+
errorMessage = 'Unauthorized. Please log in again.';
136+
} else if (error.response?.status === 403) {
137+
errorMessage = 'Access denied. You do not have permission to download reports.';
138+
} else if (error.response?.data) {
139+
const reader = new FileReader();
140+
reader.onload = () => {
141+
try {
142+
const errorData = JSON.parse(reader.result);
143+
errorMessage = errorData.error || errorData.message || errorMessage;
144+
} catch (e) {
145+
// Keep default error message
146+
}
147+
setError(errorMessage);
148+
toast.error(errorMessage, {
149+
position: 'top-right',
150+
autoClose: 3000,
151+
});
152+
};
153+
reader.readAsText(error.response.data);
154+
return; // Exit early since we're handling this asynchronously
155+
}
156+
157+
setError(errorMessage);
158+
toast.error(errorMessage, {
159+
position: 'top-right',
160+
autoClose: 3000,
161+
});
162+
163+
onDownloadError?.(error);
164+
} finally {
165+
setIsLoading(false);
166+
}
167+
}, [
168+
selectedFormat,
169+
reportType,
170+
userId,
171+
classId,
172+
startDate,
173+
endDate,
174+
educatorName,
175+
formattedReportType,
176+
authUser,
177+
onDownloadSuccess,
178+
onDownloadError,
179+
]);
180+
181+
const handleFormatChange = useCallback(e => {
182+
setSelectedFormat(e.target.value);
183+
setError(null);
184+
}, []);
185+
186+
return (
187+
<div
188+
className={`${styles.downloadSection} ${darkMode ? styles.darkMode : ''}`}
189+
role="region"
190+
aria-label="Report download section"
191+
>
192+
<div className={styles.downloadHeader}>
193+
<h3 className={styles.downloadTitle}>Export Report</h3>
194+
<p className={styles.downloadSubtitle}>Download your report in your preferred format</p>
195+
</div>
196+
197+
<div className={styles.downloadCard}>
198+
<div className={styles.formatSelection}>
199+
<label htmlFor="formatSelect" className={styles.formatLabel}>
200+
<span className={styles.labelIcon}>📄</span>
201+
Select Format
202+
</label>
203+
<div className={styles.selectWrapper}>
204+
<select
205+
id="formatSelect"
206+
value={selectedFormat}
207+
onChange={handleFormatChange}
208+
disabled={isLoading}
209+
className={`${styles.formatSelect} ${darkMode ? styles.darkModeSelect : ''}`}
210+
aria-label="Select report export format"
211+
>
212+
{EXPORT_FORMATS.map(format => (
213+
<option key={format.value} value={format.value}>
214+
{format.label}
215+
</option>
216+
))}
217+
</select>
218+
</div>
219+
</div>
220+
221+
<button
222+
onClick={handleDownload}
223+
disabled={isLoading}
224+
className={`${styles.downloadButton} ${isLoading ? styles.loading : ''} ${
225+
darkMode ? styles.darkModeButton : ''
226+
}`}
227+
aria-busy={isLoading}
228+
aria-label={
229+
isLoading
230+
? 'Generating report'
231+
: `Download ${formattedReportType} report as ${selectedFormat.toUpperCase()}`
232+
}
233+
type="button"
234+
>
235+
{isLoading ? (
236+
<>
237+
<span className={styles.spinner} aria-hidden="true"></span>
238+
<span>Generating Report...</span>
239+
</>
240+
) : (
241+
<>
242+
<svg
243+
className={styles.downloadIcon}
244+
aria-hidden="true"
245+
xmlns="http://www.w3.org/2000/svg"
246+
viewBox="0 0 24 24"
247+
fill="none"
248+
stroke="currentColor"
249+
strokeWidth="2"
250+
>
251+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
252+
<polyline points="7 10 12 15 17 10" />
253+
<line x1="12" y1="15" x2="12" y2="3" />
254+
</svg>
255+
<span>Download Report</span>
256+
</>
257+
)}
258+
</button>
259+
</div>
260+
261+
{error && (
262+
<div
263+
className={`${styles.errorMessage} ${darkMode ? styles.darkModeError : ''}`}
264+
role="alert"
265+
>
266+
<svg
267+
className={styles.errorIcon}
268+
xmlns="http://www.w3.org/2000/svg"
269+
viewBox="0 0 24 24"
270+
fill="none"
271+
stroke="currentColor"
272+
strokeWidth="2"
273+
>
274+
<circle cx="12" cy="12" r="10" />
275+
<line x1="12" y1="8" x2="12" y2="12" />
276+
<line x1="12" y1="16" x2="12.01" y2="16" />
277+
</svg>
278+
<span>{error}</span>
279+
</div>
280+
)}
281+
282+
<div className={`${styles.reportInfo} ${darkMode ? styles.darkModeInfo : ''}`}>
283+
<div className={styles.infoRow}>
284+
<span className={styles.infoLabel}>Report Type:</span>
285+
<span className={styles.infoValue}>{formattedReportType}</span>
286+
</div>
287+
<div className={styles.infoRow}>
288+
<span className={styles.infoLabel}>Generated By:</span>
289+
<span className={styles.infoValue}>{educatorName}</span>
290+
</div>
291+
{startDate && endDate && (
292+
<div className={styles.infoRow}>
293+
<span className={styles.infoLabel}>Date Range:</span>
294+
<span className={styles.infoValue}>
295+
{startDate} to {endDate}
296+
</span>
297+
</div>
298+
)}
299+
</div>
300+
</div>
301+
);
302+
};
303+
304+
export default ReportDownloadButton;

0 commit comments

Comments
 (0)