Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #283 : Dashboard #333

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed Dockefile
Empty file.
15 changes: 15 additions & 0 deletions public/locales/en/dashboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"user-info": {
"username": "Username",
"mail": "Email",
"role": "Role",
"inspectionNumber": "Number of inspections"
},
"list": {
"my-inspections": "My inspections",
"search": "Search"
},
"inspection": {
"unverified": "Inspection unverified"
}
}
15 changes: 15 additions & 0 deletions public/locales/fr/dashboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"user-info": {
"username": "Nom d'utilisateur",
"mail": "Email",
"role": "Rôle",
"inspectionNumber": "Nombre d'inspections"
},
"list": {
"my-inspections": "Mes inspections",
"search": "Rechercher"
},
"inspection": {
"unverified": "Inspection non vérifié"
}
}
23 changes: 23 additions & 0 deletions src/app/api/inspections/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,26 @@ export async function POST(request: Request) {
return handleApiError(error);
});
}

export async function GET(request: Request) {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return new Response(
JSON.stringify({ error: "Missing Authorization header" }),
{
status: 401,
},
);
}

return inspectionsApi
.getInspectionsInspectionsGet({
headers: { Authorization: authHeader },
})
.then((inspectionsResponse) => {
return Response.json(inspectionsResponse.data);
})
.catch((error) => {
return handleApiError(error);
});
}
65 changes: 65 additions & 0 deletions src/app/dashboard/__tests__/Dashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { screen, render } from "@testing-library/react";
import Dashboard from "@/app/dashboard/page";
import { Response } from "whatwg-fetch";
import axios from "axios";

// Mock useRouter:
jest.mock("next/navigation", () => ({
useRouter() {
return {
prefetch: () => null,
};
},
}));

const mockInspectList = [
{
company_info_id: "",
company_name: "test_company",
label_info_id: "",
manufacturer_info_id: "test_manufacturer",
picture_set_id: "",
sample_id: "",
id: "1",
product_name: "Fertilizer 1",
upload_date: new Date().toDateString(),
updated_at: new Date(new Date().getDate() + 5).toDateString(),
},
{
company_info_id: "",
company_name: "test_company",
label_info_id: "",
manufacturer_info_id: "test_manufacturer",
picture_set_id: "",
sample_id: "",
id: "2",
product_name: "Fertilizer 2",
upload_date: new Date().toDateString(),
updated_at: new Date().toDateString(),
},
];

axios.get = jest.fn((path: string | URL | Request) => {
if (path) {
return Promise.resolve(
new Response(mockInspectList, {
status: 200,
headers: new Headers({ "Content-Type": "application/json" }),
}),
);
} else {
return Promise.reject(
new Response("", {
status: 400,
headers: new Headers({ "Content-Type": "application/json" }),
}),
);
}
});

describe("Dashboard", () => {
it("renders the dashboard", async () => {
render(<Dashboard />);
expect(screen.getByTestId("user-info")).toBeInTheDocument();
});
});
128 changes: 128 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"use client";
import FertilizerList from "@/components/InspectionList/InspectionList";
import {
Grid2 as Grid,
InputAdornment,
TextField,
Typography,
} from "@mui/material";
import { Search, LocationOn } from "@mui/icons-material";
import { useEffect, useState } from "react";
import Cookies from "js-cookie";
import axios from "axios";
import theme from "@/app/theme";
import { useTranslation } from "react-i18next";

const Dashboard = () => {
const { t } = useTranslation("dashboard");
const [search, setSearch] = useState("");
const [inspectList, setInspectList] = useState([]);

useEffect(() => {
const username = atob(Cookies.get("token") ?? "");
const password = "";
const authHeader = "Basic " + btoa(`${username}:${password}`);
axios
.get("/api/inspections", {
headers: {
Authorization: authHeader,
},
})
.then((response) => {
setInspectList(response.data);
});
}, []);

return (
<Grid container spacing={2} className={"p-5 h-[calc(100vh-65px)]"}>
<Grid size={{ xs: 12, sm: 4, md: 3 }}>
<Grid
data-testid={"user-info"}
container
className={"p-2 border-gray-200 border-2 rounded-md h-fit"}
>
<Grid size={12}>
<Typography component={"h2"} className={"!font-black "}>
{t("user-info.username")}
</Typography>
</Grid>
<Grid size={4}>
<b>{t("user-info.mail")}:</b>
</Grid>
<Grid size={8}>User email</Grid>
<Grid size={4}>
<b>{t("user-info.role")}:</b>
</Grid>
<Grid size={8}>User role</Grid>
<Grid size={4}>
<b>
<LocationOn />:
</b>
</Grid>
<Grid size={8}>User location</Grid>
</Grid>
<Grid
container
className={
"p-2 border-gray-200 border-2 rounded-md h-fit mt-2 w-11/12"
}
>
<Grid size={12}>
<Typography component={"h4"} className={"!font-semiboldl "}>
{t("user-info.inspectionNumber")}: {inspectList.length}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid
size={{ xs: 12, sm: 8, md: 9 }}
className={
"border-gray-200 border-2 rounded-md p-2 xs:pb-1 sm:pb-0 xs:h-[calc(100%-10.75rem)] sm:h-full min-h-60"
}
>
<Grid container spacing={2} className={"h-full"}>
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
<Typography component={"h2"} className={"!font-black"}>
{t("list.my-inspections")}
</Typography>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 8 }}>
<TextField
placeholder={t("list.search")}
variant={"filled"}
className={"rounded-md"}
sx={{
backgroundColor: theme.palette.background.paper,
}}
value={search}
onChange={(e) => setSearch(e.target.value)}
fullWidth
slotProps={{
htmlInput: {
className: "!p-0 after:!transition-none ",
},
input: {
className: "p-2",
startAdornment: (
<InputAdornment position={"start"} className={"!m-0"}>
<Search color="primary" />
</InputAdornment>
),
},
}}
/>
</Grid>
<hr className={"w-full"} />
<Grid
size={{ xs: 12 }}
className={"xs:h-[calc(100%-8rem)] sm:h-[85%]"}
>
<FertilizerList search={search} inspectionList={inspectList} />
</Grid>
</Grid>
</Grid>
</Grid>
);
};

export default Dashboard;
48 changes: 48 additions & 0 deletions src/components/InspectionList/InspectionElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Box, Card, Stack, Tooltip, Typography } from "@mui/material";
import ErrorIcon from "@mui/icons-material/Error";
import React from "react";
import InspectionPreview from "@/types/InspectionPreview";
import { useTranslation } from "react-i18next";

interface InspectionElementProps {
inspection: InspectionPreview;
key: string;
handleClick(): void;
}

const InspectionElement = ({
inspection,
handleClick,
}: InspectionElementProps) => {

const { t } = useTranslation("dashboard");

return (
<Card
className={
"p-2 xs:h-[75px] s:h-[100px] cursor-pointer hover:shadow-xl flex-shrink-0"
}
onClick={handleClick}
>
<Stack direction={"row"} className={"h-full items-center"}>
<Box
data-testid="image"
component="img"
alt={inspection.product_name || "Fertilizer picture"}
src={"/img/image.png"}
className={`max-h-full h-full`}
/>
<Typography component={"h2"} className={`!ml-2 w-full`}>
{inspection.product_name}
</Typography>
{inspection.updated_at === inspection.upload_date && (
<Tooltip title={t("inspection.unverified")}>
<ErrorIcon data-testid={"error-icon"} color="error"></ErrorIcon>
</Tooltip>
)}
</Stack>
</Card>
);
};

export default InspectionElement;
46 changes: 46 additions & 0 deletions src/components/InspectionList/InspectionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";
import { Stack } from "@mui/material";
import InspectionPreview from "@/types/InspectionPreview";
import InspectionElement from "@/components/InspectionList/InspectionElement";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

interface InspectionListProps {
search: string;
inspectionList: InspectionPreview[];
}

const InspectionList = ({ search, inspectionList }: InspectionListProps) => {
const [shownList, setShownList] = useState([] as InspectionPreview[]);

const router = useRouter();

const handleInspectionClick = (id: string) => {
router.push(`/label-data-validation/${id}`);
};

useEffect(() => {
setShownList(
inspectionList.filter((inspection) => {
return inspection
.product_name!.toLowerCase()
.includes(search.toLowerCase());
}),
);
}, [inspectionList, search]);
return (
<Stack spacing={2} className={"overflow-y-auto pl-0 pb-4 h-full"}>
{shownList.map((inspection) => {
return (
<InspectionElement
inspection={inspection}
key={inspection.id}
handleClick={() => handleInspectionClick(inspection.id)}
/>
);
})}
</Stack>
);
};

export default InspectionList;
Loading
Loading