icleafreportui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.env +7 -0
  2. package/README.md +70 -0
  3. package/package.json +55 -0
  4. package/public/favicon.ico +0 -0
  5. package/public/images/EResourcesImg.png +0 -0
  6. package/public/images/courseCardImg.png +0 -0
  7. package/public/images/courseInfo.png +0 -0
  8. package/public/images/exam-options.png +0 -0
  9. package/public/images/icleaf-11.png +0 -0
  10. package/public/images/icleaf_logo.png +0 -0
  11. package/public/images/template.png +0 -0
  12. package/public/images/unnamed.png +0 -0
  13. package/public/images/user.jpg +0 -0
  14. package/public/images/young-man-studying-library-using-laptop-1.png +0 -0
  15. package/public/index.html +45 -0
  16. package/public/logo192.png +0 -0
  17. package/public/logo512.png +0 -0
  18. package/public/manifest.json +25 -0
  19. package/public/robots.txt +3 -0
  20. package/src/App.css +24 -0
  21. package/src/App.js +37 -0
  22. package/src/Login.js +159 -0
  23. package/src/Reports/CourseReport.js +209 -0
  24. package/src/Reports/ExamPackReport.js +554 -0
  25. package/src/Reports/ExamReport.js +269 -0
  26. package/src/Reports/Report.js +271 -0
  27. package/src/api/client.jsx +42 -0
  28. package/src/components/Header.css +301 -0
  29. package/src/components/Header.jsx +192 -0
  30. package/src/components/Loader.jsx +23 -0
  31. package/src/components/imagePathUrl.jsx +11 -0
  32. package/src/components/sidebar.css +947 -0
  33. package/src/components/sidebar.jsx +81 -0
  34. package/src/context/TenantProvider.jsx +22 -0
  35. package/src/fonts/210000.jpg +0 -0
  36. package/src/fonts/210001.jpg +0 -0
  37. package/src/fonts/210003.jpg +0 -0
  38. package/src/fonts/210004.jpg +0 -0
  39. package/src/fonts/210006.jpg +0 -0
  40. package/src/fonts/210018.jpg +0 -0
  41. package/src/fonts/210019.jpg +0 -0
  42. package/src/fonts/210020.jpg +0 -0
  43. package/src/fonts/210279.jpg +0 -0
  44. package/src/fonts/210280.jpg +0 -0
  45. package/src/fonts/Gilroy-Black.ttf +0 -0
  46. package/src/fonts/Gilroy-BlackItalic.ttf +0 -0
  47. package/src/fonts/Gilroy-Bold.ttf +0 -0
  48. package/src/fonts/Gilroy-BoldItalic.ttf +0 -0
  49. package/src/fonts/Gilroy-ExtraBold.ttf +0 -0
  50. package/src/fonts/Gilroy-ExtraBoldItalic.ttf +0 -0
  51. package/src/fonts/Gilroy-Heavy.ttf +0 -0
  52. package/src/fonts/Gilroy-HeavyItalic.ttf +0 -0
  53. package/src/fonts/Gilroy-Light.ttf +0 -0
  54. package/src/fonts/Gilroy-LightItalic.ttf +0 -0
  55. package/src/fonts/Gilroy-Medium.ttf +0 -0
  56. package/src/fonts/Gilroy-MediumItalic.ttf +0 -0
  57. package/src/fonts/Gilroy-Regular.ttf +0 -0
  58. package/src/fonts/Gilroy-RegularItalic.ttf +0 -0
  59. package/src/fonts/Gilroy-SemiBold.ttf +0 -0
  60. package/src/fonts/Gilroy-SemiBoldItalic.ttf +0 -0
  61. package/src/fonts/Gilroy-Thin.ttf +0 -0
  62. package/src/fonts/Gilroy-ThinItalic.ttf +0 -0
  63. package/src/fonts/Gilroy-UltraLight.ttf +0 -0
  64. package/src/fonts/Gilroy-UltraLightItalic.ttf +0 -0
  65. package/src/fonts/Help - Guide Document.pdf +0 -0
  66. package/src/fonts/License.txt +144 -0
  67. package/src/fonts/More Free Fonts on fontshmonts.com.url +2 -0
  68. package/src/fonts/cover.jpg +0 -0
  69. package/src/index.css +13 -0
  70. package/src/index.js +21 -0
  71. package/src/login.css +809 -0
  72. package/src/logo.svg +1 -0
  73. package/src/package.js +10 -0
  74. package/src/reportWebVitals.js +13 -0
  75. package/src/setupTests.js +5 -0
  76. package/src/styles.css +2026 -0
  77. package/src/theme.css +107 -0
@@ -0,0 +1,269 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import axios from 'axios';
3
+ import { Autocomplete, FormControl, Button, Grid, Box, Typography, Paper, Snackbar, Alert, CircularProgress, TextField, Checkbox, FormControlLabel } from '@mui/material';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import Header from '../components/Header';
6
+ import Sidebar from '../components/sidebar';
7
+ import '../App.css';
8
+
9
+ function ExamReport() {
10
+ const [token, setToken] = useState(localStorage.getItem("token"));
11
+ const navigate = useNavigate();
12
+ const [subjects, setSubjects] = useState([]);
13
+ const [examPacks, setExamPacks] = useState([]);
14
+ const [exams, setExams] = useState([]);
15
+ const [users, setUsers] = useState([]);
16
+ const [selectedSubject, setSelectedSubject] = useState(null);
17
+ const [selectedExamPack, setSelectedExamPack] = useState(null);
18
+ const [selectedExam, setSelectedExam] = useState(null);
19
+ const [selectedUser, setSelectedUser] = useState(null);
20
+ const [alertMessage, setAlertMessage] = useState('');
21
+ const [openAlert, setOpenAlert] = useState(false);
22
+ const [loading, setLoading] = useState(false);
23
+
24
+ const baseURL = process.env.REACT_APP_BASE_URL;
25
+
26
+ useEffect(() => {
27
+ if (token == null) {
28
+ navigate("/login")
29
+ } else {
30
+ fetchAllSubjects();
31
+ }
32
+ }, []);
33
+
34
+ const fetchAllSubjects = async () => {
35
+ try {
36
+ const response = await axios({
37
+ method: "get",
38
+ url: `${baseURL}api/showAllSubjects`,
39
+ headers: { Authorization: `Bearer ${token}` },
40
+ });
41
+ setSubjects(response.data);
42
+ } catch (error) {
43
+ console.error('Error fetching subjects:', error);
44
+ }
45
+ };
46
+
47
+ const fetchExamPacks = async (subjectId) => {
48
+ try {
49
+ const response = await axios({
50
+ method: "get",
51
+ params: { subjectId },
52
+ url: `${baseURL}api/getAllExamPacks`,
53
+ headers: { Authorization: `Bearer ${token}` },
54
+ });
55
+ setExamPacks(response.data);
56
+ } catch (error) {
57
+ console.error('Error fetching exam packs:', error);
58
+ }
59
+ };
60
+
61
+ const fetchExams = async (examPackId) => {
62
+ try {
63
+ const response = await axios({
64
+ method: "get",
65
+ params: { examPackId },
66
+ url: `${baseURL}api/getAllExams`,
67
+ headers: { Authorization: `Bearer ${token}` },
68
+ });
69
+ setExams(response.data);
70
+ } catch (error) {
71
+ console.error('Error fetching exams:', error);
72
+ }
73
+ };
74
+
75
+ const fetchUsers = async (examId) => {
76
+ try {
77
+ const response = await axios({
78
+ method: "get",
79
+ params: { examId },
80
+ url: `${baseURL}api/getAllUsers`,
81
+ headers: { Authorization: `Bearer ${token}` },
82
+ });
83
+ setUsers(response.data);
84
+ } catch (error) {
85
+ console.error('Error fetching users:', error);
86
+ }
87
+ };
88
+
89
+ const handleSubjectChange = (event, newValue) => {
90
+ setSelectedSubject(newValue);
91
+ setSelectedExamPack(null);
92
+ setSelectedExam(null);
93
+ setSelectedUser(null);
94
+ setExamPacks([]);
95
+ setExams([]);
96
+ setUsers([]);
97
+ if (newValue) {
98
+ fetchExamPacks(newValue.subjectId);
99
+ }
100
+ };
101
+
102
+ const handleExamPackChange = (event, newValue) => {
103
+ setSelectedExamPack(newValue);
104
+ setSelectedExam(null);
105
+ setSelectedUser(null);
106
+ setExams([]);
107
+ setUsers([]);
108
+ if (newValue) {
109
+ fetchExams(newValue.id);
110
+ }
111
+ };
112
+
113
+ const handleExamChange = (event, newValue) => {
114
+ setSelectedExam(newValue);
115
+ setSelectedUser(null);
116
+ setUsers([]);
117
+ if (newValue) {
118
+ fetchUsers(newValue.id);
119
+ }
120
+ };
121
+
122
+ const handleUserChange = (event, newValue) => {
123
+ setSelectedUser(newValue);
124
+ };
125
+
126
+ const handleDownload = async () => {
127
+ setLoading(true);
128
+ try {
129
+ const url = `${baseURL}api/generateReportForExam?subjectId=${selectedSubject.subjectId}&examPackId=${selectedExamPack.id}&examId=${selectedExam.id}`;
130
+ const response = await axios.get(url, {
131
+ responseType: 'blob',
132
+ headers: {
133
+ Authorization: `Bearer ${token}`
134
+ }
135
+ });
136
+
137
+ const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
138
+ const blobUrl = URL.createObjectURL(blob);
139
+ const link = document.createElement('a');
140
+ link.href = blobUrl;
141
+ link.setAttribute('download', 'QuestionewiseExamReport.xlsx');
142
+ document.body.appendChild(link);
143
+ link.click();
144
+ link.parentNode.removeChild(link);
145
+ URL.revokeObjectURL(blobUrl);
146
+ } catch (error) {
147
+ console.error('Error downloading report:', error);
148
+ } finally {
149
+ setLoading(false);
150
+ }
151
+ };
152
+
153
+ return (
154
+ <div>
155
+ <Header />
156
+ <Sidebar />
157
+ <div style={{ padding: "100px 0px 0px 100px", marginLeft: "140px" }}>
158
+ <Box sx={{ maxWidth: 800, mx: 'auto', my: 4, p: 3, backgroundColor: '#f5f5f5', borderRadius: 2 }}>
159
+ <Paper elevation={6} sx={{ p: 5, textAlign: 'center', backgroundColor: '#fff' }}>
160
+ <Typography variant="h4" component="h1" gutterBottom sx={{ color: '#3f51b5', marginBottom: "20px" }}>
161
+ Exam Questions Report
162
+ </Typography>
163
+ <Grid container spacing={2} justifyContent="center">
164
+ <Grid item xs={12} sm={6}>
165
+ <FormControl fullWidth>
166
+ <Autocomplete
167
+ id="ddlSubject"
168
+ options={subjects}
169
+ getOptionLabel={(option) => option.subjectName}
170
+ value={selectedSubject}
171
+ onChange={handleSubjectChange}
172
+ renderInput={(params) => <TextField {...params} label="Subject" />}
173
+ />
174
+ </FormControl>
175
+ </Grid>
176
+
177
+ <Grid item xs={12} sm={6}>
178
+ <FormControl fullWidth>
179
+ <Autocomplete
180
+ id="ddlExamPack"
181
+ options={examPacks}
182
+ getOptionLabel={(option) => option.name}
183
+ value={selectedExamPack}
184
+ onChange={handleExamPackChange}
185
+ renderInput={(params) => <TextField {...params} label="Exam Pack" />}
186
+ />
187
+ </FormControl>
188
+ </Grid>
189
+
190
+ <Grid item xs={12} sm={6}>
191
+ <FormControl fullWidth>
192
+ <Autocomplete
193
+ id="ddlExam"
194
+ options={exams}
195
+ getOptionLabel={(option) => option.examName}
196
+ value={selectedExam}
197
+ onChange={handleExamChange}
198
+ renderInput={(params) => <TextField {...params} label="Exam" />}
199
+ />
200
+ </FormControl>
201
+ </Grid>
202
+
203
+ {/* <Grid item xs={12} sm={6}>
204
+ <FormControl fullWidth>
205
+ <Autocomplete
206
+ id="ddlUser"
207
+ options={users}
208
+ getOptionLabel={(option) => option.userName}
209
+ value={selectedUser}
210
+ onChange={handleUserChange}
211
+ renderInput={(params) => <TextField {...params} label="User" />}
212
+ />
213
+ </FormControl>
214
+ </Grid> */}
215
+ </Grid>
216
+
217
+ <Box sx={{ mt: 3, position: 'relative' }}>
218
+ {/* <FormControlLabel
219
+ control={
220
+ <Checkbox
221
+ checked={onlyWrongAnswers}
222
+ onChange={(e) => setOnlyWrongAnswers(e.target.checked)}
223
+ />
224
+ }
225
+ label="Only Wrong Answers"
226
+ /> */}
227
+ <Box sx={{ mt: 2, position: 'relative' }}>
228
+ <Button
229
+ variant="contained"
230
+ color="primary"
231
+ onClick={handleDownload}
232
+ disabled={!selectedSubject || !selectedExamPack || !selectedExam || loading}
233
+ sx={{ width: 'auto', backgroundColor: '#3f51b5', '&:hover': { backgroundColor: '#303f9f' } }}
234
+ >
235
+ {loading ? 'Downloading...' : 'Download Report'}
236
+ </Button>
237
+ {loading && (
238
+ <CircularProgress
239
+ size={24}
240
+ sx={{
241
+ position: 'absolute',
242
+ top: '50%',
243
+ left: '50%',
244
+ marginTop: '-12px',
245
+ marginLeft: '-12px',
246
+ }}
247
+ />
248
+ )}
249
+ </Box>
250
+ </Box>
251
+ </Paper>
252
+ </Box>
253
+
254
+ <Snackbar
255
+ open={openAlert}
256
+ autoHideDuration={6000}
257
+ onClose={() => setOpenAlert(false)}
258
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
259
+ >
260
+ <Alert onClose={() => setOpenAlert(false)} severity="info" sx={{ width: '100%' }}>
261
+ {alertMessage}
262
+ </Alert>
263
+ </Snackbar>
264
+ </div>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ export default ExamReport;
@@ -0,0 +1,271 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Autocomplete, FormControl, Button, Grid, Box, Typography, Paper, Snackbar, Alert, CircularProgress, TextField, Checkbox, FormControlLabel } from '@mui/material';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import Header from '../components/Header';
5
+ import Sidebar from '../components/sidebar';
6
+ import { useTenant } from '../context/TenantProvider'; // ADD THIS
7
+ import '../App.css';
8
+
9
+ function Report() {
10
+ const { apiClient } = useTenant(); // ADD THIS - gets API client with token
11
+ const navigate = useNavigate();
12
+ const [subjects, setSubjects] = useState([]);
13
+ const [examPacks, setExamPacks] = useState([]);
14
+ const [exams, setExams] = useState([]);
15
+ const [users, setUsers] = useState([]);
16
+ const [selectedSubject, setSelectedSubject] = useState(null);
17
+ const [selectedExamPack, setSelectedExamPack] = useState(null);
18
+ const [selectedExam, setSelectedExam] = useState(null);
19
+ const [selectedUser, setSelectedUser] = useState(null);
20
+ const [alertMessage, setAlertMessage] = useState('');
21
+ const [openAlert, setOpenAlert] = useState(false);
22
+ const [loading, setLoading] = useState(false);
23
+ const [onlyWrongAnswers, setOnlyWrongAnswers] = useState(false);
24
+
25
+ const baseURL = process.env.REACT_APP_BASE_URL;
26
+
27
+ useEffect(() => {
28
+ // REMOVE token check - no more JWT
29
+ fetchAllSubjects();
30
+ }, []);
31
+
32
+ const fetchAllSubjects = async () => {
33
+ try {
34
+ // OLD WAY with axios and JWT
35
+ // const response = await axios({
36
+ // method: "get",
37
+ // url: `${baseURL}api/showAllSubjects`,
38
+ // headers: { Authorization: `Bearer ${token}` },
39
+ // });
40
+
41
+ // NEW WAY with apiClient
42
+ const data = await apiClient.get('/api/showAllSubjects');
43
+ setSubjects(data);
44
+ } catch (error) {
45
+ console.error('Error fetching subjects:', error);
46
+ }
47
+ };
48
+
49
+ const fetchExamPacks = async (subjectId) => {
50
+ try {
51
+ // OLD WAY
52
+ // const response = await axios({
53
+ // method: "get",
54
+ // params: { subjectId },
55
+ // url: `${baseURL}api/getAllExamPacks`,
56
+ // headers: { Authorization: `Bearer ${token}` },
57
+ // });
58
+
59
+ // NEW WAY
60
+ const data = await apiClient.get(`/api/getAllExamPacks?subjectId=${subjectId}`);
61
+ setExamPacks(data);
62
+ } catch (error) {
63
+ console.error('Error fetching exam packs:', error);
64
+ }
65
+ };
66
+
67
+ const fetchExams = async (examPackId) => {
68
+ try {
69
+ const data = await apiClient.get(`/api/getAllExams?examPackId=${examPackId}`);
70
+ setExams(data);
71
+ } catch (error) {
72
+ console.error('Error fetching exams:', error);
73
+ }
74
+ };
75
+
76
+ const fetchUsers = async (examId) => {
77
+ try {
78
+ const data = await apiClient.get(`/api/getAllUsers?examId=${examId}`);
79
+ setUsers(data);
80
+ } catch (error) {
81
+ console.error('Error fetching users:', error);
82
+ }
83
+ };
84
+
85
+ const handleSubjectChange = (event, newValue) => {
86
+ setSelectedSubject(newValue);
87
+ setSelectedExamPack(null);
88
+ setSelectedExam(null);
89
+ setSelectedUser(null);
90
+ setExamPacks([]);
91
+ setExams([]);
92
+ setUsers([]);
93
+ if (newValue) {
94
+ fetchExamPacks(newValue.subjectId);
95
+ }
96
+ };
97
+
98
+ const handleExamPackChange = (event, newValue) => {
99
+ setSelectedExamPack(newValue);
100
+ setSelectedExam(null);
101
+ setSelectedUser(null);
102
+ setExams([]);
103
+ setUsers([]);
104
+ if (newValue) {
105
+ fetchExams(newValue.id);
106
+ }
107
+ };
108
+
109
+ const handleExamChange = (event, newValue) => {
110
+ setSelectedExam(newValue);
111
+ setSelectedUser(null);
112
+ setUsers([]);
113
+ if (newValue) {
114
+ fetchUsers(newValue.id);
115
+ }
116
+ };
117
+
118
+ const handleUserChange = (event, newValue) => {
119
+ setSelectedUser(newValue);
120
+ };
121
+
122
+ const handleDownload = async () => {
123
+ setLoading(true);
124
+ try {
125
+ // OLD WAY with axios
126
+ // const url = `${baseURL}api/generateReportForSingleUser?subjectId=${selectedSubject.subjectId}&examPackId=${selectedExamPack.id}&examId=${selectedExam.id}&userId=${selectedUser.id}&wrongAnswerFlag=${onlyWrongAnswers}`;
127
+ // const response = await axios.get(url, {
128
+ // responseType: 'blob',
129
+ // headers: { Authorization: `Bearer ${token}` }
130
+ // });
131
+
132
+ // NEW WAY - need blob response
133
+ const response = await fetch(`${baseURL}api/generateReportForSingleUser?subjectId=${selectedSubject.subjectId}&examPackId=${selectedExamPack.id}&examId=${selectedExam.id}&userId=${selectedUser.id}&wrongAnswerFlag=${onlyWrongAnswers}`, {
134
+ headers: {
135
+ 'X-Tenant-Token': apiClient.tenantToken
136
+ }
137
+ });
138
+
139
+ const blob = await response.blob();
140
+ const blobUrl = URL.createObjectURL(blob);
141
+ const link = document.createElement('a');
142
+ link.href = blobUrl;
143
+ link.setAttribute('download', 'Report.xlsx');
144
+ document.body.appendChild(link);
145
+ link.click();
146
+ link.parentNode.removeChild(link);
147
+ URL.revokeObjectURL(blobUrl);
148
+ } catch (error) {
149
+ console.error('Error downloading report:', error);
150
+ } finally {
151
+ setLoading(false);
152
+ }
153
+ };
154
+
155
+ return (
156
+ <div>
157
+ <Header />
158
+ <Sidebar />
159
+ <div style={{ padding: "100px 0px 0px 100px", marginLeft: "140px" }}>
160
+ <Box sx={{ maxWidth: 800, mx: 'auto', my: 4, p: 3, backgroundColor: '#f5f5f5', borderRadius: 2 }}>
161
+ <Paper elevation={6} sx={{ p: 5, textAlign: 'center', backgroundColor: '#fff' }}>
162
+ <Typography variant="h4" component="h1" gutterBottom sx={{ color: '#3f51b5', marginBottom: "20px" }}>
163
+ Exam Report
164
+ </Typography>
165
+ <Grid container spacing={2} justifyContent="center">
166
+ <Grid item xs={12} sm={6}>
167
+ <FormControl fullWidth>
168
+ <Autocomplete
169
+ id="ddlSubject"
170
+ options={subjects}
171
+ getOptionLabel={(option) => option.subjectName}
172
+ value={selectedSubject}
173
+ onChange={handleSubjectChange}
174
+ renderInput={(params) => <TextField {...params} label="Subject" />}
175
+ />
176
+ </FormControl>
177
+ </Grid>
178
+
179
+ <Grid item xs={12} sm={6}>
180
+ <FormControl fullWidth>
181
+ <Autocomplete
182
+ id="ddlExamPack"
183
+ options={examPacks}
184
+ getOptionLabel={(option) => option.name}
185
+ value={selectedExamPack}
186
+ onChange={handleExamPackChange}
187
+ renderInput={(params) => <TextField {...params} label="Exam Pack" />}
188
+ />
189
+ </FormControl>
190
+ </Grid>
191
+
192
+ <Grid item xs={12} sm={6}>
193
+ <FormControl fullWidth>
194
+ <Autocomplete
195
+ id="ddlExam"
196
+ options={exams}
197
+ getOptionLabel={(option) => option.examName}
198
+ value={selectedExam}
199
+ onChange={handleExamChange}
200
+ renderInput={(params) => <TextField {...params} label="Exam" />}
201
+ />
202
+ </FormControl>
203
+ </Grid>
204
+
205
+ <Grid item xs={12} sm={6}>
206
+ <FormControl fullWidth>
207
+ <Autocomplete
208
+ id="ddlUser"
209
+ options={users}
210
+ getOptionLabel={(option) => option.userName}
211
+ value={selectedUser}
212
+ onChange={handleUserChange}
213
+ renderInput={(params) => <TextField {...params} label="User" />}
214
+ />
215
+ </FormControl>
216
+ </Grid>
217
+ </Grid>
218
+
219
+ <Box sx={{ mt: 3, position: 'relative' }}>
220
+ <FormControlLabel
221
+ control={
222
+ <Checkbox
223
+ checked={onlyWrongAnswers}
224
+ onChange={(e) => setOnlyWrongAnswers(e.target.checked)}
225
+ />
226
+ }
227
+ label="Only Wrong Answers"
228
+ />
229
+ <Box sx={{ mt: 2, position: 'relative' }}>
230
+ <Button
231
+ variant="contained"
232
+ color="primary"
233
+ onClick={handleDownload}
234
+ disabled={!selectedSubject || !selectedExamPack || !selectedExam || !selectedUser || loading}
235
+ sx={{ width: 'auto', backgroundColor: '#3f51b5', '&:hover': { backgroundColor: '#303f9f' } }}
236
+ >
237
+ {loading ? 'Downloading...' : 'Download Report'}
238
+ </Button>
239
+ {loading && (
240
+ <CircularProgress
241
+ size={24}
242
+ sx={{
243
+ position: 'absolute',
244
+ top: '50%',
245
+ left: '50%',
246
+ marginTop: '-12px',
247
+ marginLeft: '-12px',
248
+ }}
249
+ />
250
+ )}
251
+ </Box>
252
+ </Box>
253
+ </Paper>
254
+ </Box>
255
+
256
+ <Snackbar
257
+ open={openAlert}
258
+ autoHideDuration={6000}
259
+ onClose={() => setOpenAlert(false)}
260
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
261
+ >
262
+ <Alert onClose={() => setOpenAlert(false)} severity="info" sx={{ width: '100%' }}>
263
+ {alertMessage}
264
+ </Alert>
265
+ </Snackbar>
266
+ </div>
267
+ </div>
268
+ );
269
+ }
270
+
271
+ export default Report;
@@ -0,0 +1,42 @@
1
+ class TenantApiClient {
2
+ constructor(config) {
3
+ this.baseURL = config.baseURL;
4
+ this.tenantToken = config.tenantToken;
5
+ }
6
+
7
+ async request(endpoint, options = {}) {
8
+ // Remove double slashes
9
+ const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
10
+ const url = `${this.baseURL}/${cleanEndpoint}`;
11
+ console.log('FULL URL BEING SENT:', url); // Add this
12
+
13
+ const response = await fetch(url, {
14
+ ...options,
15
+ headers: {
16
+ 'X-Tenant-Token': this.tenantToken,
17
+ 'Content-Type': 'application/json',
18
+ ...options.headers
19
+ }
20
+ });
21
+
22
+ if (response.status === 401) {
23
+ throw new Error('Invalid or missing tenant token');
24
+ }
25
+
26
+ return response.json();
27
+ }
28
+
29
+ // Add your API methods
30
+ async get(endpoint) {
31
+ return this.request(endpoint);
32
+ }
33
+
34
+ async post(endpoint, data) {
35
+ return this.request(endpoint, {
36
+ method: 'POST',
37
+ body: JSON.stringify(data)
38
+ });
39
+ }
40
+ }
41
+
42
+ export default TenantApiClient;