admin usertest results

This commit is contained in:
David Katrinka 2025-12-31 21:30:29 +01:00
parent 4ee508f01b
commit b7313488a4
13 changed files with 455 additions and 49 deletions

View File

@ -7,6 +7,30 @@ import {
} from "../components/shared/types/TestTypes"; } from "../components/shared/types/TestTypes";
import axiosInstance from "./axiosInstance"; import axiosInstance from "./axiosInstance";
export type UserTestsResponse = {
data: UserTestType[];
links: {
first: string;
last: string;
prev: string | null;
next: string | null;
};
meta: {
current_page: number;
from: number | null;
last_page: number;
path: string;
per_page: number;
to: number | null;
total: number;
links: {
url: string | null;
label: string;
active: boolean;
}[];
};
};
export const startTest = async (data: StartTestPayload) => { export const startTest = async (data: StartTestPayload) => {
const res = await axiosInstance.post<{ data: UserTestType }>( const res = await axiosInstance.post<{ data: UserTestType }>(
"/api/user-tests", "/api/user-tests",
@ -64,3 +88,19 @@ export const getTestById = async (id: number) => {
const res = await axiosInstance.get<{ data: TestType }>(`/api/tests/${id}`); const res = await axiosInstance.get<{ data: TestType }>(`/api/tests/${id}`);
return res.data.data; return res.data.data;
}; };
export const getAllUserTests = async (
page = 1,
test_id?: number,
question_id?: number
) => {
const res = await axiosInstance.get<UserTestsResponse>("/api/user-tests", {
params: { page, test_id, question_id },
});
return res.data;
};
export const deleteUserTest = async (id: number) => {
const res = await axiosInstance.delete(`/api/user-tests/${id}`);
return res.data;
};

View File

@ -75,9 +75,9 @@ const LearningAnswers = ({
variant="contained" variant="contained"
color="primary" color="primary"
sx={{ mt: 2 }} sx={{ mt: 2 }}
onClick={() => setShowAnswer(true)} onClick={() => setShowAnswer(!showAnswer)}
> >
Reveal Answer {showAnswer ? "Hide Answer" : "Reveal Answer"}
</Button> </Button>
)} )}
</Box> </Box>

View File

@ -11,6 +11,7 @@ interface TestCardProps {
const TestCard = ({ test }: TestCardProps) => { const TestCard = ({ test }: TestCardProps) => {
let title: string | undefined = "User Test"; let title: string | undefined = "User Test";
if (test.test_id) { if (test.test_id) {
const { data } = useGetTestById(test.test_id); const { data } = useGetTestById(test.test_id);
title = data?.title; title = data?.title;

View File

@ -1,7 +1,8 @@
import { Box, Button, Chip, Typography } from "@mui/material"; import { Box, Button, Chip, Tooltip, Typography } from "@mui/material";
import CategoryIcon from "@mui/icons-material/Category"; import CategoryIcon from "@mui/icons-material/Category";
import type { TestType } from "../shared/types/TestTypes"; import type { TestType } from "../shared/types/TestTypes";
import { useStartTestById } from "../../hooks/Tests/useStartTestById"; import { useStartTestById } from "../../hooks/Tests/useStartTestById";
import { useNavigate } from "react-router-dom";
interface TestCardProps { interface TestCardProps {
test: TestType; test: TestType;
@ -9,6 +10,8 @@ interface TestCardProps {
const TestListCard = ({ test }: TestCardProps) => { const TestListCard = ({ test }: TestCardProps) => {
const { mutate: startTest } = useStartTestById(); const { mutate: startTest } = useStartTestById();
const navigate = useNavigate();
const title = test.title; const title = test.title;
let statusLabel = ""; let statusLabel = "";
@ -35,13 +38,28 @@ const TestListCard = ({ test }: TestCardProps) => {
flexDirection: { flexDirection: {
xs: "column", xs: "column",
sm: "column", sm: "column",
md: "row" md: "row",
}, },
}} }}
> >
<Box> <Box>
<Box sx={{display: "flex", gap: 1, flexDirection: {xs: "column", sm: "row"}}}> <Box
<Typography variant="h6">{title}</Typography> sx={{
display: "flex",
gap: 1,
flexDirection: { xs: "column", sm: "row" },
}}
>
<Tooltip title="View test details" placement="top" arrow>
<Typography
variant="h6"
color="primary"
sx={{ cursor: "pointer", color: "#000" }}
onClick={() => navigate(`/tests/view/${test.id}`)}
>
{title}
</Typography>
</Tooltip>
<Chip <Chip
icon={<CategoryIcon />} icon={<CategoryIcon />}
label={test.category.name} label={test.category.name}
@ -67,7 +85,6 @@ const TestListCard = ({ test }: TestCardProps) => {
</Box> </Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mt: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1, mt: 1 }}>
<Chip label={statusLabel} color={statusColor} /> <Chip label={statusLabel} color={statusColor} />
{test.is_available && ( {test.is_available && (
<Button <Button
@ -79,8 +96,6 @@ const TestListCard = ({ test }: TestCardProps) => {
</Button> </Button>
)} )}
</Box> </Box>
</Box> </Box>
); );
}; };

View File

@ -0,0 +1,69 @@
import { TableRow, TableCell, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import type { UserTestType } from "../../components/shared/types/TestTypes";
import { useGetTestById } from "../../hooks/Tests/useGetTestById";
import { formatDate } from "../../utils/functions";
type Props = {
userTest: UserTestType;
onDelete: (id: number) => void;
};
const UserTestRow = ({ userTest, onDelete }: Props) => {
const navigate = useNavigate();
const testId = userTest.test_id;
let title = "User Test";
let author = "—";
if (testId) {
const { data } = useGetTestById(testId);
title = data?.title ?? "User Test";
author = data?.author.username ?? "-";
}
return (
<TableRow>
<TableCell>{userTest.id}</TableCell>
<TableCell>{title}</TableCell>
<TableCell>{userTest.user?.username ?? "Unknown"}</TableCell>
<TableCell>{author}</TableCell>
<TableCell>
{userTest.score !== undefined ? userTest.score : "—"}
</TableCell>
<TableCell>{userTest.is_completed ? "Yes" : "No"}</TableCell>
<TableCell>
{formatDate(userTest.created_at)}
</TableCell>
<TableCell>{formatDate(userTest.closed_at)}</TableCell>
<TableCell>
<Button
size="small"
variant="outlined"
sx={{ mr: 1 }}
onClick={() => navigate(`/tests/${userTest.id}`)}
>
View
</Button>
<Button
size="small"
color="error"
variant="outlined"
onClick={() => onDelete(userTest.id)}
>
Delete
</Button>
</TableCell>
</TableRow>
);
};
export default UserTestRow;

View File

@ -0,0 +1,15 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteUserTest } from "../../api/testApi";
import { toast } from "react-toastify";
export const useDeleteUserTest = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteUserTest(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["all-user-tests"] });
toast.success("User Test deleted")
},
});
};

View File

@ -0,0 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { getAllUserTests } from "../../api/testApi";
export const useGetAllUserTests = (
page: number,
test_id?: number,
question_id?: number
) =>
useQuery({
queryKey: ["all-user-tests", page],
queryFn: () => getAllUserTests(page, test_id, question_id),
});

View File

@ -0,0 +1,114 @@
import { useState } from "react";
import {
Box,
Button,
CircularProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
Pagination,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import Container from "../../components/shared/Container";
import { useGetAllUserTests } from "../../hooks/Tests/useGetAllUserTests";
import { useDeleteUserTest } from "../../hooks/Tests/useDeteleUserTest";
import UserTestRow from "../../components/UserTestRow/UserTestRow";
const UserTestsPage = () => {
const [currentPage, setCurrentPage] = useState(1);
const [deleteId, setDeleteId] = useState<number | null>(null);
const { data, isLoading, isError } = useGetAllUserTests(currentPage);
const deleteMutation = useDeleteUserTest();
const handleDelete = () => {
if (deleteId !== null) {
deleteMutation.mutate(deleteId, {
onSuccess: () => setDeleteId(null),
});
}
};
if (isLoading) return <CircularProgress />;
if (isError)
return <Typography color="error">Error loading user tests.</Typography>;
return (
<Container>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
<Typography variant="h4">User Tests</Typography>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Test</TableCell>
<TableCell>Test Taker</TableCell>
<TableCell>Test Author</TableCell>
<TableCell>Score</TableCell>
<TableCell>Completed</TableCell>
<TableCell>Created At</TableCell>
<TableCell>Closed At</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data?.data.map((ut) => (
<UserTestRow
key={ut.id}
userTest={ut}
onDelete={(id) => setDeleteId(id)}
/>
))}
</TableBody>
</Table>
</TableContainer>
<Pagination
color="primary"
shape="rounded"
count={data?.meta.last_page}
page={currentPage}
onChange={(_, value) => setCurrentPage(value)}
sx={{
mt: 3,
mb: 3,
display: "flex",
justifyContent: "center",
}}
/>
<Dialog open={deleteId !== null} onClose={() => setDeleteId(null)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
Are you sure you want to delete this user test?
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteId(null)}>Cancel</Button>
<Button
color="error"
onClick={handleDelete}
disabled={deleteMutation.isPending}
>
Delete
</Button>
</DialogActions>
</Dialog>
</Container>
);
};
export default UserTestsPage;

View File

@ -0,0 +1,147 @@
import { useParams } from "react-router-dom";
import { useGetTestById } from "../../hooks/Tests/useGetTestById";
import Container from "../../components/shared/Container";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Pagination,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import LearningAnswers from "../../components/Answers/Answers";
import NotFoundPage from "../NotFoundPage/NotFoundPage";
import { useAuth } from "../../context/AuthContext";
import { useState } from "react";
import { useGetAllUserTests } from "../../hooks/Tests/useGetAllUserTests";
import UserTestRow from "../../components/UserTestRow/UserTestRow";
import { useDeleteUserTest } from "../../hooks/Tests/useDeteleUserTest";
export const SingleTestPage = () => {
const { id } = useParams();
const { user } = useAuth();
const [currentPage, setCurrentPage] = useState(1);
const [deleteId, setDeleteId] = useState<number | null>(null);
const deleteMutation = useDeleteUserTest();
const { data: tests } = useGetAllUserTests(currentPage, Number(id));
const handleDelete = () => {
if (deleteId !== null) {
deleteMutation.mutate(deleteId, {
onSuccess: () => setDeleteId(null),
});
}
};
const { data: test } = useGetTestById(Number(id));
if (!test) {
return <NotFoundPage />;
}
return (
<Container>
<Typography variant="h2" sx={{ mb: 3 }}>
{test?.title}
</Typography>
{test?.questions?.map((q) => (
<Box key={q.id} sx={{ mb: 4 }}>
<Typography variant="h6">{q.title}</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
{q.description}
</Typography>
<LearningAnswers
type={q.type as "single" | "multiple" | "text"}
variants={q.variants}
correctAnswers={q.correct_answers}
/>
</Box>
))}
{(user?.type == "admin" || user?.type == "creator") && (
<span>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
<Typography variant="h4">{test.title} results</Typography>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Test</TableCell>
<TableCell>Test Taker</TableCell>
<TableCell>Test Author</TableCell>
<TableCell>Score</TableCell>
<TableCell>Completed</TableCell>
<TableCell>Created At</TableCell>
<TableCell>Closed At</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tests?.data && tests.data.length > 0 ? (
tests.data.map((ut) => (
<UserTestRow
key={ut.id}
userTest={ut}
onDelete={(id) => setDeleteId(id)}
/>
))
) : (
<TableRow>
<TableCell colSpan={9} align="center" sx={{ py: 3 }}>
<Typography variant="body1" color="text.secondary">
No results found for this test.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<Pagination
color="primary"
shape="rounded"
count={tests?.meta.last_page}
page={currentPage}
onChange={(_, value) => setCurrentPage(value)}
sx={{
mt: 3,
mb: 3,
display: "flex",
justifyContent: "center",
}}
/>
<Dialog open={deleteId !== null} onClose={() => setDeleteId(null)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
Are you sure you want to delete this user test?
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteId(null)}>Cancel</Button>
<Button
color="error"
onClick={handleDelete}
disabled={deleteMutation.isPending}
>
Delete
</Button>
</DialogActions>
</Dialog>
</span>
)}
</Container>
);
};

View File

@ -8,9 +8,12 @@ import { useSubmitAnswer } from "../../hooks/Tests/useSubmitAnswer";
import { useCompleteTest } from "../../hooks/Tests/useCompleteTest"; import { useCompleteTest } from "../../hooks/Tests/useCompleteTest";
import LearningAnswers from "../../components/Answers/Answers"; import LearningAnswers from "../../components/Answers/Answers";
import { formatDate } from "../../utils/functions"; import { formatDate } from "../../utils/functions";
import NotFoundPage from "../NotFoundPage/NotFoundPage";
import { useAuth } from "../../context/AuthContext";
export const TestPage = () => { export const TestPage = () => {
const { id } = useParams(); const { id } = useParams();
const { user } = useAuth();
const { data: test, isLoading, error } = useUserTestById(Number(id)); const { data: test, isLoading, error } = useUserTestById(Number(id));
const [currentQuestion, setCurrentQuestion] = useState(1); const [currentQuestion, setCurrentQuestion] = useState(1);
const submitAnswerMutation = useSubmitAnswer(); const submitAnswerMutation = useSubmitAnswer();
@ -30,25 +33,11 @@ export const TestPage = () => {
</Container> </Container>
); );
if (error) if (error) return <NotFoundPage />;
return (
<Container>
<Typography variant="h6" color="error">
Oops! An error occurred: {error.message}
</Typography>
</Container>
);
if (!test) if (!test) return <NotFoundPage />;
return (
<Container>
<Typography variant="h6" color="textSecondary">
No test found.
</Typography>
</Container>
);
if (!test.is_available && !test.is_completed) if (!test.is_available && !test.is_completed && user?.type == 'user')
return ( return (
<Container> <Container>
<Typography variant="h6" color="textSecondary"> <Typography variant="h6" color="textSecondary">

View File

@ -12,7 +12,6 @@ import { useUserById } from "../../hooks/users/useUserById";
import { useCreateUser } from "../../hooks/users/useCreateUser"; import { useCreateUser } from "../../hooks/users/useCreateUser";
import { useUpdateUser } from "../../hooks/users/useUpdateUser"; import { useUpdateUser } from "../../hooks/users/useUpdateUser";
const UserForm = () => { const UserForm = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const isUpdate = Boolean(id); const isUpdate = Boolean(id);
@ -46,8 +45,6 @@ const UserForm = () => {
const payload = { username, email, type, ...(password && { password }) }; const payload = { username, email, type, ...(password && { password }) };
if (isUpdate && id) { if (isUpdate && id) {
console.log(payload);
console.log(id);
updateUserMutation.mutate( updateUserMutation.mutate(
{ id: Number(id), payload }, { id: Number(id), payload },
{ onSuccess: () => navigate("/dashboard/users") } { onSuccess: () => navigate("/dashboard/users") }
@ -103,6 +100,7 @@ const UserForm = () => {
<MenuItem value="user">User</MenuItem> <MenuItem value="user">User</MenuItem>
<MenuItem value="admin">Admin</MenuItem> <MenuItem value="admin">Admin</MenuItem>
<MenuItem value="creator">Creator</MenuItem> <MenuItem value="creator">Creator</MenuItem>
<MenuItem value="banned">Banned</MenuItem>
</TextField> </TextField>
<Button variant="contained" color="primary" type="submit"> <Button variant="contained" color="primary" type="submit">
{isUpdate ? "Update" : "Create"} {isUpdate ? "Update" : "Create"}

View File

@ -20,6 +20,8 @@ import CategoriesPage from "../pages/CategoriesPage/CategoriesPage";
import CategoryForm from "../pages/CategoryForm/CategoryForm"; import CategoryForm from "../pages/CategoryForm/CategoryForm";
import LogsPage from "../pages/LogsPage/LogsPage"; import LogsPage from "../pages/LogsPage/LogsPage";
import HitCountsPage from "../pages/HitcountsPage/HitcountsPage"; import HitCountsPage from "../pages/HitcountsPage/HitcountsPage";
import UserTestsPage from "../pages/AdminUserTestsPage/AdminUserTestsPage";
import { SingleTestPage } from "../pages/SingleTestPage/SingleTestPage";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -70,6 +72,10 @@ const router = createBrowserRouter([
</ProtectedRoute> </ProtectedRoute>
), ),
}, },
{path: "/tests/view/:id",
element: <SingleTestPage/>
},
{ {
path: "/tests", path: "/tests",
element: <TestsPage />, element: <TestsPage />,
@ -95,7 +101,7 @@ const router = createBrowserRouter([
}, },
{ path: "questions", element: <Container>questions</Container> }, { path: "questions", element: <Container>questions</Container> },
{ path: "tests", element: <Container>tests</Container> }, { path: "tests", element: <Container>tests</Container> },
{ path: "user-tests", element: <Container>User Tests</Container> }, { path: "user-tests", element:<UserTestsPage/> },
{ {
path: "categories", path: "categories",
children: [ children: [

View File

@ -12,7 +12,7 @@ export function arraysEqual(a: number[], b: number[]): boolean {
return true; return true;
} }
export const formatDate = (dateString: string | undefined) => { export const formatDate = (dateString: string | undefined | null) => {
if (!dateString) return "N/A"; if (!dateString) return "N/A";
const date = new Date(dateString); const date = new Date(dateString);