Skip to content

Commit f1662a2

Browse files
authored
Merge pull request #609 from EPantelaios/feature/CAT-1061
[CAT-1066] - Permit reporter role to click and view assessments
2 parents 2cf3509 + 37d02df commit f1662a2

File tree

5 files changed

+199
-29
lines changed

5 files changed

+199
-29
lines changed

src/api/services/reports.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { useMutation, useQuery } from "@tanstack/react-query";
1+
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
22
import { APIClient } from "../client";
3+
import type {
4+
ApiAdminAssessments,
5+
AssessmentDetailsResponse,
6+
AssessmentListResponse,
7+
Pagination,
8+
} from "@/types";
39

410
export interface ReportDefinition {
511
id: string;
@@ -126,3 +132,50 @@ export const useGetReportFilters = ({
126132
},
127133
enabled: !!token && !!reportDefinitionId && isRegistered,
128134
});
135+
136+
export const useGetReportAssessments = ({
137+
size,
138+
token,
139+
search,
140+
isRegistered,
141+
}: ApiAdminAssessments) =>
142+
useInfiniteQuery({
143+
queryKey: ["all-assessments", { size, search }],
144+
queryFn: async ({ pageParam = 1 }) => {
145+
const response = await APIClient(token).get<AssessmentListResponse>(
146+
`/v1/reports/assessments?size=${size}&page=${pageParam}${search !== "" ? "&search=" + search : ""}`,
147+
);
148+
return response.data;
149+
},
150+
initialPageParam: 1,
151+
getNextPageParam: (lastPage) => {
152+
const pageMeta = lastPage as Pagination;
153+
if (pageMeta.number_of_page < pageMeta.total_pages) {
154+
return pageMeta.number_of_page + 1;
155+
} else {
156+
return undefined;
157+
}
158+
},
159+
enabled: !!token && isRegistered,
160+
});
161+
162+
export function useGetReportAssessmentById({
163+
id,
164+
token,
165+
isRegistered,
166+
}: {
167+
id: string;
168+
token?: string;
169+
isRegistered?: boolean;
170+
}) {
171+
return useQuery({
172+
queryKey: ["assessment", id],
173+
queryFn: async () => {
174+
const url = `/v1/reports/assessments/${id}`;
175+
const response =
176+
await APIClient(token).get<AssessmentDetailsResponse>(url);
177+
return response.data;
178+
},
179+
enabled: !!token && isRegistered && id !== "",
180+
});
181+
}

src/pages/admin/reports/Reports.tsx

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useCallback, useState } from "react";
1+
import { useCallback, useState, useMemo } from "react";
2+
import { Link } from "react-router-dom";
23
import {
34
Container,
45
Row,
@@ -20,6 +21,9 @@ import type {
2021
ReportDefinition,
2122
ReportFilter,
2223
} from "@/api/services/reports";
24+
import type { AssessmentListItem } from "@/types";
25+
import { buildRoute } from "@/routes";
26+
import ROUTES from "@/routes";
2327
import styles from "./Reports.module.css";
2428

2529
interface ReportsProps {
@@ -33,6 +37,7 @@ interface ReportsProps {
3337
reportFilters: ReportFilter[];
3438
selectedFilters: Record<string, string[]>;
3539
appliedFilters: Record<string, string[]>;
40+
assessments: AssessmentListItem[];
3641
onReportDefinitionChange: (definitionId: string) => void;
3742
onFilterChange: (
3843
filterName: string,
@@ -55,6 +60,7 @@ function Reports({
5560
reportFilters,
5661
selectedFilters,
5762
appliedFilters,
63+
assessments,
5864
onReportDefinitionChange,
5965
onFilterChange,
6066
onApplyFilters,
@@ -63,6 +69,18 @@ function Reports({
6369
}: ReportsProps) {
6470
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
6571

72+
// Create assessment name-to-ID lookup map
73+
const assessmentNameToIdMap = useMemo(() => {
74+
const map = new Map<string, string>();
75+
assessments.forEach((assessment) => {
76+
map.set(assessment.name, assessment.id);
77+
});
78+
return map;
79+
}, [assessments]);
80+
81+
const isRowAssessment = reportData?.rows_dimension === "assessment";
82+
const isColumnAssessment = reportData?.columns_dimension === "assessment";
83+
6684
const getAppliedFilterCount = useCallback(() => {
6785
return Object.values(appliedFilters).reduce(
6886
(total, values) => total + values.length,
@@ -322,9 +340,24 @@ function Reports({
322340
</Button>
323341
)}
324342
</th>
325-
{reportData.columns.map((column, index) => (
326-
<th key={index}>{column}</th>
327-
))}
343+
{reportData.columns.map((column, index) => {
344+
const assessmentId = assessmentNameToIdMap.get(column);
345+
const isClickable = isColumnAssessment && assessmentId;
346+
347+
return isClickable ? (
348+
<th key={index}>
349+
<Link
350+
to={buildRoute(ROUTES.ASSESSMENTS.VIEW, {
351+
asmtId: assessmentId,
352+
})}
353+
>
354+
{column}
355+
</Link>
356+
</th>
357+
) : (
358+
<th key={index}>{column}</th>
359+
);
360+
})}
328361
</tr>
329362
</thead>
330363
<tbody>
@@ -348,22 +381,39 @@ function Reports({
348381
</td>
349382
</tr>
350383
) : (
351-
reportData.rows.map((row, rowIndex) => (
352-
<tr key={rowIndex}>
353-
<td className={styles["row-header"]}>{row}</td>
354-
{reportData.data[rowIndex]?.map(
355-
(cellValue, cellIndex) => (
356-
<td key={cellIndex}>
357-
<span
358-
className={`${styles["status-badge"]} ${getStatusClass(cellValue)}`}
384+
reportData.rows.map((row, rowIndex) => {
385+
const assessmentId = assessmentNameToIdMap.get(row);
386+
const isClickable = isRowAssessment && assessmentId;
387+
388+
return (
389+
<tr key={rowIndex}>
390+
{isClickable ? (
391+
<td className={styles["row-header"]}>
392+
<Link
393+
to={buildRoute(ROUTES.ASSESSMENTS.VIEW, {
394+
asmtId: assessmentId,
395+
})}
359396
>
360-
{getDisplayValue(cellValue)}
361-
</span>
397+
{row}
398+
</Link>
362399
</td>
363-
),
364-
)}
365-
</tr>
366-
))
400+
) : (
401+
<td className={styles["row-header"]}>{row}</td>
402+
)}
403+
{reportData.data[rowIndex]?.map(
404+
(cellValue, cellIndex) => (
405+
<td key={cellIndex}>
406+
<span
407+
className={`${styles["status-badge"]} ${getStatusClass(cellValue)}`}
408+
>
409+
{getDisplayValue(cellValue)}
410+
</span>
411+
</td>
412+
),
413+
)}
414+
</tr>
415+
);
416+
})
367417
)}
368418
</tbody>
369419
</Table>

src/pages/admin/reports/ReportsContainer.tsx

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
useGetReportFilters,
77
useExportReport,
88
type ReportResponse,
9+
useGetReportAssessments,
910
} from "@/api/services/reports";
1011
import toast from "react-hot-toast";
1112
import Reports from "./Reports";
13+
import type { AssessmentListItem } from "@/types";
1214

1315
function ReportsContainer() {
1416
const { keycloak, registered } = useContext(AuthContext)!;
@@ -23,6 +25,12 @@ function ReportsContainer() {
2325
const [appliedFilters, setAppliedFilters] = useState<
2426
Record<string, string[]>
2527
>({});
28+
const [shouldFetchAssessments, setShouldFetchAssessments] = useState(false);
29+
const [allAssessments, setAllAssessments] = useState<AssessmentListItem[]>(
30+
[],
31+
);
32+
const [isFetchingAllAssessments, setIsFetchingAllAssessments] =
33+
useState(false);
2634

2735
const {
2836
data: reportDefinitions,
@@ -39,9 +47,40 @@ function ReportsContainer() {
3947
isRegistered: registered,
4048
});
4149

50+
const {
51+
data: assessmentsData,
52+
fetchNextPage,
53+
hasNextPage,
54+
} = useGetReportAssessments({
55+
size: 5,
56+
token: keycloak?.token || "",
57+
search: "",
58+
isRegistered:
59+
registered && shouldFetchAssessments && isFetchingAllAssessments,
60+
});
61+
4262
const generateReportMutation = useGenerateReport(keycloak?.token || "");
4363
const exportReportMutation = useExportReport(keycloak?.token || "");
4464

65+
// Fetch all assessments with pagination
66+
useEffect(() => {
67+
if (!assessmentsData || !isFetchingAllAssessments) return;
68+
69+
let tmpAssessments: AssessmentListItem[] = [];
70+
if (assessmentsData?.pages) {
71+
assessmentsData.pages.map((page) => {
72+
tmpAssessments = [...tmpAssessments, ...page.content];
73+
});
74+
if (hasNextPage) {
75+
fetchNextPage();
76+
}
77+
}
78+
79+
setAllAssessments(tmpAssessments);
80+
setIsFetchingAllAssessments(false);
81+
// eslint-disable-next-line react-hooks/exhaustive-deps
82+
}, [assessmentsData, fetchNextPage, hasNextPage, isFetchingAllAssessments]);
83+
4584
const handleGenerateReportForDefinition = useCallback(
4685
async (definitionId: string, filters?: Record<string, string[]>) => {
4786
if (!isInitialLoad) {
@@ -66,6 +105,16 @@ function ReportsContainer() {
66105
});
67106
setReportData(result);
68107

108+
if (
109+
(result.rows_dimension === "assessment" ||
110+
result.columns_dimension === "assessment") &&
111+
!shouldFetchAssessments
112+
) {
113+
setAllAssessments([]);
114+
setShouldFetchAssessments(true);
115+
setIsFetchingAllAssessments(true);
116+
}
117+
69118
if (isInitialLoad) {
70119
setIsInitialLoad(false);
71120
}
@@ -76,7 +125,12 @@ function ReportsContainer() {
76125
setIsGenerating(false);
77126
}
78127
},
79-
[isInitialLoad, selectedFilters, generateReportMutation],
128+
[
129+
isInitialLoad,
130+
selectedFilters,
131+
generateReportMutation,
132+
shouldFetchAssessments,
133+
],
80134
);
81135

82136
useEffect(() => {
@@ -93,12 +147,8 @@ function ReportsContainer() {
93147
if (selectedReportDefinition && !reportData && isInitialLoad) {
94148
handleGenerateReportForDefinition(selectedReportDefinition);
95149
}
96-
}, [
97-
selectedReportDefinition,
98-
reportData,
99-
isInitialLoad,
100-
handleGenerateReportForDefinition,
101-
]);
150+
// eslint-disable-next-line react-hooks/exhaustive-deps
151+
}, [selectedReportDefinition, reportData, isInitialLoad]);
102152

103153
const handleReportDefinitionChange = (definitionId: string) => {
104154
setSelectedReportDefinition(definitionId);
@@ -202,6 +252,7 @@ function ReportsContainer() {
202252
reportFilters={reportFilters || []}
203253
selectedFilters={selectedFilters}
204254
appliedFilters={appliedFilters}
255+
assessments={allAssessments}
205256
onReportDefinitionChange={handleReportDefinitionChange}
206257
onFilterChange={handleFilterChange}
207258
onApplyFilters={handleApplyFilters}

src/pages/assessments/AssessmentView.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next";
1717
import { prettyPrintRanking } from "@/utils";
1818
import { AutoTestDetails } from "./components/tests/AutoTestDetails";
1919
import gatherStats from "./utils/gatherStats";
20+
import { useGetReportAssessmentById } from "@/api/services/reports";
2021

2122
/** AssessmentView page that displays the results of an assessment */
2223
const AssessmentView = ({ isPublic }: { isPublic: boolean }) => {
@@ -28,14 +29,29 @@ const AssessmentView = ({ isPublic }: { isPublic: boolean }) => {
2829

2930
const asmtNumID = asmtId !== undefined ? asmtId : "";
3031

32+
const hasFullAssessmentAccess =
33+
keycloak?.resourceAccess?.["backend-service"]?.roles?.some((role) =>
34+
["admin", "reporter"].includes(role),
35+
) ?? false;
36+
37+
const { data: reportAssessmentData } = useGetReportAssessmentById({
38+
id: asmtNumID,
39+
token: keycloak?.token || "",
40+
isRegistered: registered && hasFullAssessmentAccess,
41+
});
42+
3143
const { data: assessmentData } = useGetAssessment({
3244
id: asmtNumID,
3345
token: keycloak?.token || "",
34-
isRegistered: registered,
46+
isRegistered: registered && !hasFullAssessmentAccess,
3547
isPublic: isPublic,
3648
});
3749

38-
const assessment = assessmentData?.assessment_doc;
50+
const finalAssessmentData = hasFullAssessmentAccess
51+
? reportAssessmentData
52+
: assessmentData;
53+
54+
const assessment = finalAssessmentData?.assessment_doc;
3955
const stats = gatherStats(assessment);
4056

4157
return (

src/pages/assessments/AssessmentsList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ function AssessmentsList({ listPublic = false }: AssessmentListProps) {
236236

237237
const { data: adminSettings } = useGetAdminSettings({
238238
token: keycloak?.token || "",
239-
isRegistered: registered || false,
239+
isRegistered: (registered && userType?.toLowerCase() === "admin") || false,
240240
});
241241

242242
// Check if Zenodo publishing is enabled

0 commit comments

Comments
 (0)