Merge pull request #13 from lemeow125/feature/pages

Feature/pages
This commit is contained in:
Keannu Bernasol 2023-12-04 19:17:51 +08:00 committed by GitHub
commit 70fb033658
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 5235 additions and 114 deletions

View file

@ -1,10 +1,10 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>CITC Equipment Tracker</title>
</head>
<body>
<div id="root"></div>

1173
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,21 @@
"preview": "vite preview"
},
"dependencies": {
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.16",
"@mui/material": "^5.14.17",
"@mui/styled-engine": "^5.14.17",
"@mui/styled-engine-sc": "^6.0.0-alpha.5",
"@reduxjs/toolkit": "^1.9.7",
"@tanstack/react-query": "^5.8.3",
"axios": "^1.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-redux": "^8.1.3",
"react-router-dom": "^6.18.0",
"react-toastify": "^9.1.3",
"reactjs-popup": "^2.0.6",
"styled-components": "^6.1.1"
},
"devDependencies": {
"@types/react": "^18.2.15",

View file

@ -4,39 +4,3 @@
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View file

@ -1,35 +1,118 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import LandingPage from "./Pages/LandingPage/LandingPage";
import { createHashRouter, RouterProvider } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Provider } from "react-redux";
import "./App.css";
import store from "./Components/Plugins/Redux/Store/Store";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import ErrorPage from "./Pages/ErrorPage/ErrorPage";
import DashboardPage from "./Pages/DashboardPage/DashboardPage";
import Revalidator from "./Components/Revalidator/Revalidator";
import ActivationPage from "./Pages/ActivationPage/ActivationPage";
import ResetPasswordPage from "./Pages/ResetPasswordPage/ResetPasswordPage";
import EquipmentInstancesListPage from "./Pages/EquipmentInstancesListPage/EquipmentInstancesListPage";
import EquipmentListPage from "./Pages/EquipmentListPage/EquipmentListPage";
import EquipmentLogsPage from "./Pages/EquipmentLogsPage/EquipmentLogsPage";
import EquipmentInstanceLogsPage from "./Pages/EquipmentInstanceLogsPage/EquipmentInstanceLogsPage";
function App() {
const [count, setCount] = useState(0)
return (
const queryClient = new QueryClient();
const router = createHashRouter([
{
path: "/",
element: (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
<Revalidator />
<LandingPage />
</>
)
}
),
errorElement: <ErrorPage />,
},
{
path: "/dashboard",
element: (
<>
<Revalidator />
<DashboardPage />
</>
),
errorElement: <ErrorPage />,
},
{
path: "/view/equipment_instances",
element: (
<>
<Revalidator />
<EquipmentInstancesListPage />
</>
),
errorElement: <ErrorPage />,
},
{
path: "/view/equipment_instances/logs",
element: (
<>
<Revalidator />
<EquipmentInstanceLogsPage />
</>
),
errorElement: <ErrorPage />,
},
{
path: "/view/equipments",
element: (
<>
<Revalidator />
<EquipmentListPage />
</>
),
errorElement: <ErrorPage />,
},
{
path: "/view/equipments/logs",
element: (
<>
<Revalidator />
<EquipmentLogsPage />
</>
),
errorElement: <ErrorPage />,
},
{
path: "/activation/:uid/:token",
element: (
<>
<ActivationPage />
</>
),
errorElement: <ErrorPage />,
},
{
path: "/reset_password_confirm/:uid/:token",
element: (
<>
<ResetPasswordPage />
</>
),
errorElement: <ErrorPage />,
},
]);
export default App
export default function App() {
return (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
<ToastContainer
position={"top-right"}
autoClose={1500}
closeOnClick
pauseOnHover
draggable
theme={"light"}
limit={3}
/>
</Provider>
);
}

342
src/Components/API/API.tsx Normal file
View file

@ -0,0 +1,342 @@
/* eslint-disable react-refresh/only-export-components */
import axios from "axios";
import {
ActivationType,
EquipmentListType,
LoginType,
RegisterType,
ResetPasswordConfirmType,
EquipmentInstanceListType,
EquipmentType,
AddEquipmentType,
AddEquipmentInstanceType,
EquipmentInstanceType,
PatchEquipmentInstanceType,
PatchEquipmentType,
EquipmentLogListType,
EquipmentInstanceLogListType,
} from "../Types/Types";
const debug = false;
let backendURL;
if (debug) {
backendURL = "http://localhost:8000/";
} else {
backendURL = "https://equipment-tracker-backend.keannu1.duckdns.org/";
}
const instance = axios.create({
baseURL: backendURL,
});
// Token Handling
export async function getAccessToken() {
const accessToken = await localStorage.getItem("access_token");
return accessToken;
}
export async function getRefreshToken() {
const refreshToken = await localStorage.getItem("refresh_token");
return refreshToken;
}
export async function setAccessToken(access: string) {
await localStorage.setItem("access_token", access);
return true;
}
export async function setRefreshToken(refresh: string) {
await localStorage.setItem("refresh_token", refresh);
return true;
}
// Header Config Template for REST
export async function GetConfig() {
const accessToken = await getAccessToken();
return {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
}
export function ParseError(error: { response: { data: string } }) {
if (error.response && error.response.data) {
if (error.response.data.length > 50) {
return "Error truncated (too long)";
}
return JSON.stringify(error.response.data)
.replace(/[{}]/g, " ")
.replace(/\(/g, " ")
.replace(/\)/g, " ")
.replace(/"/g, " ")
.replace(/,/g, ",")
.replace(/\[/g, "")
.replace(/\]/g, "")
.replace(/\./g, "")
.replace(/non_field_errors/g, "")
.trim();
}
return "Unable to reach server";
}
// User APIs
export function RegisterAPI(info: RegisterType) {
return instance
.post("api/v1/accounts/users/", info)
.then(async (response) => {
console.log(response.data);
return [true, 0];
})
.catch((error) => {
console.log("Registration failed");
return [false, ParseError(error)];
});
}
export function LoginAPI(user: LoginType, remember_session: boolean) {
return instance
.post("api/v1/accounts/jwt/create/", user)
.then(async (response) => {
console.log(response.data);
setAccessToken(response.data.access);
if (remember_session) {
setRefreshToken(response.data.refresh);
}
console.log("Login Success");
return true;
})
.catch((error) => {
console.log("Login Failed", error.response.data);
return false;
});
}
export async function JWTRefreshAPI() {
const refresh = await getRefreshToken();
return instance
.post("api/v1/accounts/jwt/refresh/", {
refresh: refresh,
})
.then(async (response) => {
setAccessToken(response.data.access);
return true;
})
.catch(() => {
console.log("Error refreshing token");
return false;
});
}
export async function UserAPI() {
const config = await GetConfig();
return instance
.get("api/v1/accounts/users/me/", config)
.then((response) => {
return response.data;
})
.catch(() => {
console.log("Error retrieving user data");
});
}
export function ActivationAPI(activation: ActivationType) {
return instance
.post("api/v1/accounts/users/activation/", activation)
.then(() => {
console.log("Activation Success");
return true;
})
.catch(() => {
console.log("Activation failed");
return false;
});
}
export function ResetPasswordAPI(email: string) {
return instance
.post("api/v1/accounts/users/reset_password/", { email: email })
.then(() => {
console.log("Activation Success");
return true;
})
.catch(() => {
console.log("Activation failed");
return false;
});
}
export function ResetPasswordConfirmAPI(info: ResetPasswordConfirmType) {
return instance
.post("api/v1/accounts/users/reset_password_confirm/", info)
.then(() => {
console.log("Reset Success");
return true;
})
.catch(() => {
console.log("Reset failed");
return false;
});
}
// Equipment APIs
export async function EquipmentAPI(id: number) {
const config = await GetConfig();
return instance
.get(`api/v1/equipments/equipments/${id}/`, config)
.then((response) => {
return response.data as EquipmentType;
})
.catch(() => {
console.log("Error retrieving equipment");
});
}
export async function EquipmentUpdateAPI(
equipment: PatchEquipmentType,
id: number
) {
const config = await GetConfig();
return instance
.patch(`api/v1/equipments/equipments/${id}/`, equipment, config)
.then((response) => {
return [true, response.data as EquipmentType];
})
.catch((error) => {
console.log("Error updating equipment instance");
return [false, ParseError(error)];
});
}
export async function EquipmentRemoveAPI(id: number) {
const config = await GetConfig();
return instance
.delete(`api/v1/equipments/equipments/${id}/`, config)
.then((response) => {
return [true, response.data as EquipmentType];
})
.catch((error) => {
console.log("Error deleting equipment instance");
return [false, ParseError(error)];
});
}
export async function EquipmentsAPI() {
const config = await GetConfig();
return instance
.get("api/v1/equipments/equipments/", config)
.then((response) => {
return response.data as EquipmentListType;
})
.catch(() => {
console.log("Error retrieving equipments");
});
}
export async function EquipmentCreateAPI(equipment: AddEquipmentType) {
const config = await GetConfig();
return instance
.post("api/v1/equipments/equipments/", equipment, config)
.then((response) => {
return [true, response.data as EquipmentType];
})
.catch((error) => {
console.log("Error creating equipment");
return [false, ParseError(error)];
});
}
export async function EquipmentLogsAPI() {
const config = await GetConfig();
return instance
.get("api/v1/equipments/equipments/logs", config)
.then((response) => {
return response.data as EquipmentLogListType;
})
.catch(() => {
console.log("Error retrieving equipment logs");
});
}
// Equipment Instances APIs
export async function EquipmentInstanceLogsAPI() {
const config = await GetConfig();
return instance
.get("api/v1/equipments/equipment_instances/logs", config)
.then((response) => {
return response.data as EquipmentInstanceLogListType;
})
.catch(() => {
console.log("Error retrieving equipment logs");
});
}
export async function EquipmentInstanceAPI(id: number) {
const config = await GetConfig();
return instance
.get(`api/v1/equipments/equipment_instances/${id}/`, config)
.then((response) => {
return response.data as EquipmentInstanceType;
})
.catch(() => {
console.log("Error retrieving equipment");
});
}
export async function EquipmentInstanceUpdateAPI(
item: PatchEquipmentInstanceType,
id: number
) {
const config = await GetConfig();
return instance
.patch(`api/v1/equipments/equipment_instances/${id}/`, item, config)
.then((response) => {
return [true, response.data as EquipmentInstanceType];
})
.catch((error) => {
console.log("Error updating equipment instance");
return [false, ParseError(error)];
});
}
export async function EquipmentInstanceRemoveAPI(id: number) {
const config = await GetConfig();
return instance
.delete(`api/v1/equipments/equipment_instances/${id}/`, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
console.log("Error deleting equipment instance");
return [false, ParseError(error)];
});
}
export async function EquipmentInstancesAPI() {
const config = await GetConfig();
return instance
.get("api/v1/equipments/equipment_instances/", config)
.then((response) => {
return response.data as EquipmentInstanceListType;
})
.catch(() => {
console.log("Error retrieving equipments");
});
}
export async function EquipmentInstanceCreateAPI(
equipment_instance: AddEquipmentInstanceType
) {
const config = await GetConfig();
return instance
.post("api/v1/equipments/equipment_instances/", equipment_instance, config)
.then((response) => {
return [true, response.data as EquipmentInstanceType];
})
.catch((error) => {
console.log("Error creating equipment instance");
return [false, ParseError(error)];
});
}

View file

@ -0,0 +1,235 @@
import { useEffect, useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
import TextField from "@mui/material/TextField";
import AddToQueueIcon from "@mui/icons-material/AddToQueue";
import Button from "../Button/Button";
import { toast } from "react-toastify";
import { EquipmentInstanceCreateAPI, EquipmentsAPI } from "../API/API";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import Radio from "@mui/material/Radio";
import { useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { CircularProgress } from "@mui/material";
import React from "react";
export default function AddItemModal() {
const queryClient = useQueryClient();
const [item, setItem] = useState({
equipment: 0,
remarks: "",
status: "WORKING",
});
const [error, setError] = useState("");
const equipments = useQuery({
queryKey: ["equipments"],
queryFn: EquipmentsAPI,
});
useEffect(() => {
if (equipments.data) {
setItem({ ...item, equipment: equipments.data[0].id });
}
}, [equipments.data, item]);
if (equipments.isLoading) {
return (
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
);
}
return (
<>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
overflowY: "scroll",
},
}}
>
<AddToQueueIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>Add Item</p>
</div>
<div style={styles.flex_column}>
<FormControl style={{ marginTop: "8px" }}>
<FormLabel style={styles.text_dark} id="associated-equipment-group">
Select Associated SKU
</FormLabel>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
name="radio-buttons-group"
value={item.equipment}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({ ...item, equipment: Number(e.target.value) });
setError("");
}}
>
<div
style={{
...styles.flex_column,
...{ overflowY: "scroll", maxHeight: "8rem" },
}}
>
{equipments.data ? (
equipments.data.map((equipment) => (
<React.Fragment key={equipment.id}>
<FormControlLabel
value={equipment.id}
control={<Radio />}
label={equipment.name}
style={styles.text_dark}
/>
</React.Fragment>
))
) : (
<></>
)}
</div>
</RadioGroup>
</FormControl>
<FormControl style={{ marginTop: "8px" }}>
<FormLabel style={styles.text_dark} id="status-selection">
Item Status
</FormLabel>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
value={item.status}
defaultValue="WORKING"
name="radio-buttons-group"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({ ...item, status: e.target.value });
setError("");
}}
>
<div
style={{
...styles.flex_column,
...{ overflowY: "scroll", maxHeight: "8rem" },
}}
>
<FormControlLabel
value="WORKING"
control={<Radio />}
label="Working"
style={styles.text_dark}
/>
<FormControlLabel
value="BROKEN"
control={<Radio />}
label="Broken"
style={styles.text_dark}
/>
<FormControlLabel
value="MAINTENANCE"
control={<Radio />}
label="Under Maintenance"
style={styles.text_dark}
/>
<FormControlLabel
value="DECOMISSIONED"
control={<Radio />}
label="Decomissioned"
style={styles.text_dark}
/>
</div>
</RadioGroup>
</FormControl>
<TextField
id="outlined-helperText"
label="Remarks"
multiline
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({ ...item, remarks: e.target.value });
setError("");
}}
value={item.remarks}
placeholder={"Optionally add a brief description of the item"}
/>
</div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{error}</p>
<div
style={{
backgroundColor: colors.button_border,
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<Button
type={"dark"}
label={"Add Item"}
onClick={async () => {
let data;
if (item.remarks == "") {
data = await EquipmentInstanceCreateAPI({
equipment: item.equipment,
status: item.status,
});
} else {
data = await EquipmentInstanceCreateAPI(item);
}
if (data[0]) {
setError("Added successfully");
toast(
`New item added successfuly, ${
typeof data[1] == "object" ? "ID:" + data[1].id : ""
}`,
{
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
}
);
queryClient.invalidateQueries({
queryKey: ["equipment_instances"],
});
setItem({ ...item, status: "WORKING", remarks: "" });
} else {
setError(JSON.stringify(data[1]));
}
}}
/>
</>
);
}

View file

@ -0,0 +1,172 @@
import { useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
import TextField from "@mui/material/TextField";
import NoteAddIcon from "@mui/icons-material/NoteAdd";
import Button from "../Button/Button";
import { toast } from "react-toastify";
import { EquipmentCreateAPI } from "../API/API";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import Radio from "@mui/material/Radio";
import { useQueryClient } from "@tanstack/react-query";
export default function AddSKUModal() {
const queryClient = useQueryClient();
const [sku, setSKU] = useState({
name: "",
description: "",
category: "MISC",
});
const [error, setError] = useState("");
return (
<>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
overflowY: "scroll",
},
}}
>
<NoteAddIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>Add SKU</p>
</div>
<div style={styles.flex_column}>
<TextField
id="outlined-helperText"
label="SKU Name"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSKU({ ...sku, name: e.target.value });
setError("");
}}
value={sku.name}
placeholder={"Enter SKU name"}
/>
<TextField
id="outlined-helperText"
label="Description"
multiline
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSKU({ ...sku, description: e.target.value })
}
value={sku.description}
placeholder={"Give a brief description of the SKU"}
/>
<FormControl style={{ marginTop: "8px" }}>
<FormLabel
style={styles.text_dark}
id="demo-radio-buttons-group-label"
>
Category
</FormLabel>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
defaultValue="MISC"
name="radio-buttons-group"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSKU({ ...sku, category: e.target.value });
setError("");
}}
>
<div style={styles.flex_row}>
<div style={styles.flex_column}>
<FormControlLabel
value="PC"
control={<Radio />}
label="Workstation"
style={styles.text_dark}
/>
<FormControlLabel
value="NETWORKING"
control={<Radio />}
label="Networking"
style={styles.text_dark}
/>
<FormControlLabel
value="CCTV"
control={<Radio />}
label="CCTV"
style={styles.text_dark}
/>
</div>
<div style={styles.flex_column}>
<FormControlLabel
value="FURNITURE"
control={<Radio />}
label="Furniture"
style={styles.text_dark}
/>
<FormControlLabel
value="PERIPHERALS"
control={<Radio />}
label="Peripherals"
style={styles.text_dark}
/>
<FormControlLabel
value="MISC"
control={<Radio />}
label="Miscellaneous"
style={styles.text_dark}
/>
</div>
</div>
</RadioGroup>
</FormControl>
</div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{error}</p>
<div
style={{
backgroundColor: colors.button_border,
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<Button
type={"dark"}
label={"Add SKU"}
onClick={async () => {
const data = await EquipmentCreateAPI(sku);
if (data[0]) {
setError("Added successfully");
toast(
`New SKU added successfuly, ${
typeof data[1] == "object" ? "ID:" + data[1].id : ""
}`,
{
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
}
);
queryClient.invalidateQueries({ queryKey: ["equipments"] });
setSKU({ name: "", description: "", category: "MISC" });
} else {
setError(JSON.stringify(data[1]));
}
}}
/>
</>
);
}

View file

@ -0,0 +1,65 @@
import React, { useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
export interface props {
onClick: React.MouseEventHandler<HTMLButtonElement>;
label?: string;
type: "light" | "dark";
children?: React.ReactNode;
}
export default function Button(props: props) {
const [clicked, setClicked] = useState(false);
return (
<div>
<button
onClick={props.onClick}
onMouseDown={() => {
if (!clicked) {
setClicked(!clicked);
}
}}
onMouseUp={() => setClicked(false)}
onMouseLeave={() => setClicked(false)}
style={{
borderRadius: 24,
minWidth: "128px",
maxWidth: "128px",
borderColor: colors.button_border,
borderStyle: "solid",
borderWidth: "2px",
paddingBottom: 0,
paddingTop: 0,
paddingRight: "4px",
paddingLeft: "4px",
marginBottom: "4px",
marginTop: "4px",
backgroundColor:
props.type == "light"
? clicked
? colors.button_dark
: colors.button_light
: clicked
? colors.button_light
: colors.button_dark,
}}
>
<p
style={{
...(props.type == "light"
? clicked
? styles.text_light
: styles.text_dark
: clicked
? styles.text_dark
: styles.text_light),
...styles.text_S,
}}
>
{props.label}
</p>
{props.children}
</button>
</div>
);
}

View file

@ -0,0 +1,110 @@
import styles, { colors } from "../../styles";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import HomeIcon from "@mui/icons-material/Home";
import LogoutIcon from "@mui/icons-material/Logout";
import { useQuery } from "@tanstack/react-query";
import { UserAPI, setAccessToken, setRefreshToken } from "../API/API";
import DrawerButton from "../DrawerButton/DrawerButton";
import { useDispatch } from "react-redux";
import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
export default function Drawer() {
const user = useQuery({ queryKey: ["user"], queryFn: UserAPI });
const dispatch = useDispatch();
const navigate = useNavigate();
return (
<div
style={{
width: "256px",
height: "100%",
padding: 16,
alignContent: "center",
justifyContent: "center",
backgroundColor: colors.header_color,
}}
>
<div style={styles.flex_row}>
<AccountCircleIcon
style={{
width: "48px",
height: "48px",
color: "white",
marginRight: "4px",
}}
/>
<p
style={{
...styles.text_light,
...styles.text_S,
...{ alignSelf: "center" },
}}
>
{user.data
? user.data.username
: user.isError
? "Error loading user"
: "Loading user..."}
</p>
</div>
<div
style={{
backgroundColor: "white",
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<DrawerButton
onClick={() => {
navigate("/dashboard");
}}
icon={
<HomeIcon
style={{
width: "48px",
height: "48px",
color: "white",
marginRight: "2px",
alignSelf: "center",
justifySelf: "center",
}}
/>
}
label={"Dashboard"}
/>
<DrawerButton
onClick={async () => {
navigate("/");
await dispatch(auth_toggle());
await setAccessToken("");
await setRefreshToken("");
toast("Logged out", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
}}
icon={
<LogoutIcon
style={{
width: "48px",
height: "48px",
color: "white",
marginRight: "2px",
alignSelf: "center",
justifySelf: "center",
}}
/>
}
label={"Log out"}
/>
</div>
);
}

View file

@ -0,0 +1,55 @@
import React, { useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
export interface props {
onClick: React.MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
icon?: React.ReactNode;
label: string;
}
export default function DrawerButton(props: props) {
const [clicked, setClicked] = useState(false);
return (
<div>
<button
onClick={props.onClick}
onMouseDown={() => {
if (!clicked) {
setClicked(!clicked);
}
}}
onMouseUp={() => setClicked(false)}
onMouseLeave={() => setClicked(false)}
style={{
borderRadius: 24,
minWidth: "128px",
maxWidth: "128px",
borderColor: colors.button_border,
borderStyle: "solid",
borderWidth: "2px",
paddingBottom: 0,
paddingTop: 0,
paddingRight: "4px",
paddingLeft: "4px",
marginBottom: "4px",
marginTop: "4px",
backgroundColor: clicked ? colors.button_light : colors.button_dark,
}}
>
<div style={styles.flex_row}>
{clicked ? <></> : props.icon}
<p
style={{
...(clicked ? styles.text_dark : styles.text_light),
...styles.text_M,
...{ marginLeft: "4px" },
}}
>
{props.label}
</p>
</div>
</button>
</div>
);
}

View file

@ -0,0 +1,298 @@
import { useEffect, useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
import TextField from "@mui/material/TextField";
import EditIcon from "@mui/icons-material/Edit";
import Button from "../Button/Button";
import { toast } from "react-toastify";
import {
EquipmentInstanceAPI,
EquipmentInstanceRemoveAPI,
EquipmentInstanceUpdateAPI,
} from "../API/API";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import Radio from "@mui/material/Radio";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { CircularProgress } from "@mui/material";
import React from "react";
export default function EditItemInstanceModal(props: {
id: number;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const queryClient = useQueryClient();
const [item, setItem] = useState({
remarks: "",
status: "",
});
const [error, setError] = useState("");
const equipment = useQuery({
queryKey: ["equipment_instance", props.id],
queryFn: () => EquipmentInstanceAPI(Number(props.id)),
});
useEffect(() => {
if (equipment.data) {
setItem({
...item,
remarks: equipment.data.remarks,
status: equipment.data.status,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [equipment.data]);
const update_mutation = useMutation({
mutationFn: async () => {
const data = await EquipmentInstanceUpdateAPI(item, props.id);
if (data[0] != true) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["equipment_instances"] });
queryClient.invalidateQueries({
queryKey: ["equipment_instance", props.id],
});
setError("Updated successfully");
toast(
`Item updated successfuly, ${
typeof data[1] == "object" ? "ID:" + data[1].id : ""
}`,
{
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
}
);
if (typeof data[1] == "object") {
setItem({
...item,
remarks: data[1].remarks,
status: data[1].status,
});
}
},
onError: (error) => {
setError(JSON.stringify(error));
},
});
const delete_mutation = useMutation({
mutationFn: async () => {
const data = await EquipmentInstanceRemoveAPI(props.id);
if (data[0] != true) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["equipment_instances"] });
queryClient.invalidateQueries({
queryKey: ["equipment_instance", props.id],
});
setError("Deleted successfully");
toast("Item deleted successfuly", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
props.setOpen(false);
if (typeof data[1] == "object") {
setItem({
...item,
remarks: data[1].remarks,
status: data[1].status,
});
}
},
onError: (error) => {
setError(JSON.stringify(error));
},
});
if (equipment.isLoading) {
return (
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
);
}
return (
<>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
overflowY: "scroll",
},
}}
>
<EditIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>Edit Item</p>
</div>
<div style={styles.flex_column}>
<FormControl style={{ marginTop: "8px" }}>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
verticalAlign: "center",
},
}}
>
<p
style={{
...styles.text_dark,
...styles.text_L,
...{ marginRight: "8px" },
}}
>
Associated SKU:
</p>
<p style={{ ...styles.text_dark, ...styles.text_M }}>
{equipment.data?.equipment_name}
{" (SKU #" + equipment.data?.equipment + ")"}
</p>
</div>
<FormLabel style={styles.text_dark} id="status-selection">
Item Status
</FormLabel>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
value={item.status}
defaultValue="WORKING"
name="radio-buttons-group"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({ ...item, status: e.target.value });
setError("");
}}
>
<div
style={{
...styles.flex_column,
...{ overflowY: "scroll", maxHeight: "8rem" },
}}
>
<FormControlLabel
value="WORKING"
control={<Radio />}
label="Working"
style={styles.text_dark}
/>
<FormControlLabel
value="BROKEN"
control={<Radio />}
label="Broken"
style={styles.text_dark}
/>
<FormControlLabel
value="MAINTENANCE"
control={<Radio />}
label="Under Maintenance"
style={styles.text_dark}
/>
<FormControlLabel
value="DECOMISSIONED"
control={<Radio />}
label="Decomissioned"
style={styles.text_dark}
/>
</div>
</RadioGroup>
</FormControl>
<TextField
id="outlined-helperText"
label="Remarks"
multiline
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({ ...item, remarks: e.target.value });
setError("");
}}
value={item.remarks}
placeholder={"Optionally add a brief description of the item"}
/>
</div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{error}</p>
<div
style={{
backgroundColor: colors.button_border,
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<div
style={{
...styles.flex_row,
...{ justifyContent: "center" },
}}
>
<Button
type={"dark"}
label={"Update Item"}
onClick={async () => {
await update_mutation.mutate();
}}
/>
<div style={{ margin: "8px" }}></div>
<Button
type={"light"}
label={"Delete Item"}
onClick={async () => {
await delete_mutation.mutate();
}}
/>
</div>
</>
);
}

View file

@ -0,0 +1,300 @@
import { useEffect, useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
import TextField from "@mui/material/TextField";
import EditIcon from "@mui/icons-material/Edit";
import Button from "../Button/Button";
import { toast } from "react-toastify";
import {
EquipmentAPI,
EquipmentRemoveAPI,
EquipmentUpdateAPI,
} from "../API/API";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import Radio from "@mui/material/Radio";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { CircularProgress } from "@mui/material";
import React from "react";
export default function EditSKUModal(props: {
id: number;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const queryClient = useQueryClient();
const [item, setItem] = useState({
name: "",
description: "",
category: "",
});
const [error, setError] = useState("");
const equipment = useQuery({
queryKey: ["equipment", props.id],
queryFn: () => EquipmentAPI(Number(props.id)),
});
useEffect(() => {
if (equipment.data) {
setItem({
...item,
name: equipment.data.name,
description: equipment.data.description,
category: equipment.data.category,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [equipment.data]);
const update_mutation = useMutation({
mutationFn: async () => {
const data = await EquipmentUpdateAPI(item, props.id);
if (data[0] != true) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["equipments"] });
queryClient.invalidateQueries({
queryKey: ["equipment", props.id],
});
setError("Updated successfully");
toast(
`Item updated successfuly, ${
typeof data[1] == "object" ? "ID:" + data[1].id : ""
}`,
{
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
}
);
if (typeof data[1] == "object") {
setItem({
...item,
name: data[1].name,
description: data[1].description,
category: data[1].category,
});
}
},
onError: (error) => {
setError(JSON.stringify(error));
},
});
const delete_mutation = useMutation({
mutationFn: async () => {
const data = await EquipmentRemoveAPI(props.id);
if (data[0] != true) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["equipments"] });
queryClient.invalidateQueries({
queryKey: ["equipment", props.id],
});
setError("Deleted successfully");
toast("SKU deleted successfuly", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
props.setOpen(false);
if (typeof data[1] == "object") {
setItem({
...item,
name: data[1].name,
description: data[1].description,
category: data[1].category,
});
}
},
onError: (error) => {
setError(JSON.stringify(error));
},
});
if (equipment.isLoading) {
return (
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
);
}
return (
<>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
overflowY: "scroll",
},
}}
>
<EditIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>Edit SKU</p>
</div>
<div style={styles.flex_column}>
<FormControl style={{ marginTop: "8px" }}>
<TextField
id="outlined-helperText"
label="SKU Name"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({ ...item, name: e.target.value });
setError("");
}}
value={item.name}
placeholder={"Enter SKU name"}
/>
<TextField
id="outlined-helperText"
label="Description"
multiline
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setItem({ ...item, description: e.target.value })
}
value={item.description}
placeholder={"Give a brief description of the SKU"}
/>
<FormLabel
style={styles.text_dark}
id="demo-radio-buttons-group-label"
>
Category
</FormLabel>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
name="radio-buttons-group"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setItem({ ...item, category: e.target.value });
setError("");
}}
value={item.category}
>
<div style={styles.flex_row}>
<div style={styles.flex_column}>
<FormControlLabel
value="PC"
control={<Radio />}
label="Workstation"
style={styles.text_dark}
/>
<FormControlLabel
value="NETWORKING"
control={<Radio />}
label="Networking"
style={styles.text_dark}
/>
<FormControlLabel
value="CCTV"
control={<Radio />}
label="CCTV"
style={styles.text_dark}
/>
</div>
<div style={styles.flex_column}>
<FormControlLabel
value="FURNITURE"
control={<Radio />}
label="Furniture"
style={styles.text_dark}
/>
<FormControlLabel
value="PERIPHERALS"
control={<Radio />}
label="Peripherals"
style={styles.text_dark}
/>
<FormControlLabel
value="MISC"
control={<Radio />}
label="Miscellaneous"
style={styles.text_dark}
/>
</div>
</div>
</RadioGroup>
</FormControl>
</div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{error}</p>
<div
style={{
backgroundColor: colors.button_border,
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<div
style={{
...styles.flex_row,
...{ justifyContent: "center" },
}}
>
<Button
type={"dark"}
label={"Update Item"}
onClick={async () => {
await update_mutation.mutate();
}}
/>
<div style={{ margin: "8px" }}></div>
<Button
type={"light"}
label={"Delete Item"}
onClick={async () => {
await delete_mutation.mutate();
}}
/>
</div>
</>
);
}

View file

@ -0,0 +1,50 @@
import { useState } from "react";
import styles, { colors } from "../../styles";
import MenuIcon from "@mui/icons-material/Menu";
import SidebarModal from "../Drawer/Drawer";
import { Drawer } from "@mui/material";
export interface props {
label: string;
}
export default function Header(props: props) {
const [SidebarOpen, SetSidebarOpen] = useState(false);
return (
<div
style={{
position: "sticky",
top: 0,
zIndex: 1,
backgroundColor: colors.header_color,
display: "flex",
flexDirection: "row",
}}
>
<div
style={{
flex: 1,
alignSelf: "center",
}}
>
<MenuIcon
style={{
height: "64px",
width: "64px",
float: "left",
marginLeft: "8px",
}}
onClick={() => {
SetSidebarOpen(true);
}}
/>
</div>
<p style={{ ...styles.text_light, ...styles.text_L, ...{ flex: 1 } }}>
{props.label}
</p>
<div style={{ flex: 1 }} />
<Drawer open={SidebarOpen} onClose={() => SetSidebarOpen(false)}>
<SidebarModal />
</Drawer>
</div>
);
}

View file

@ -0,0 +1,145 @@
import { useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import IconButton from "@mui/material/IconButton";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import LoginIcon from "@mui/icons-material/Login";
import Checkbox from "@mui/material/Checkbox";
import Button from "../Button/Button";
import { useNavigate } from "react-router-dom";
import { LoginAPI } from "../API/API";
import { useDispatch } from "react-redux";
import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { toast } from "react-toastify";
export default function LoginModal() {
const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const [remember_session, setRememberSession] = useState(true);
const [error, setError] = useState("");
const [user, setUser] = useState({
username: "",
password: "",
});
const dispatch = useDispatch();
return (
<>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
overflowY: "scroll",
},
}}
>
<LoginIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>Welcome back!</p>
</div>
<div style={styles.flex_column}>
<TextField
id="outlined-helperText"
label="Username"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, username: e.target.value });
setError("");
}}
value={user.username}
placeholder={"Enter username"}
/>
<TextField
id="outlined-helperText"
type={showPassword ? "text" : "password"}
style={styles.input_form}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => {
setShowPassword(!showPassword);
setError("");
}}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
label="Password"
placeholder={"Enter password"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, password: e.target.value })
}
value={user.password}
/>
<div style={styles.flex_row}>
<div
style={{
...styles.flex_row,
...{ flex: 1, alignItems: "center" },
}}
>
<Checkbox
inputProps={{ "aria-label": "Checkbox demo" }}
defaultChecked
sx={{
color: colors.button_dark,
"&.Mui-checked": {
color: colors.button_dark,
},
}}
value={remember_session}
onChange={() => setRememberSession(!remember_session)}
/>
<p style={{ ...styles.text_dark, ...styles.text_S }}>Remember me</p>
</div>
</div>
</div>
<div
style={{
backgroundColor: colors.button_border,
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_S }}>{error}</p>
<Button
type={"dark"}
label={"Login"}
onClick={async () => {
const status = await LoginAPI(user, remember_session);
if (status === true) {
await dispatch(auth_toggle());
navigate("/dashboard");
toast("Logged in", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
} else {
setError("Invalid login");
}
}}
/>
</>
);
}

View file

@ -0,0 +1,19 @@
/* eslint-disable react-refresh/only-export-components */
import { createSlice } from "@reduxjs/toolkit";
export const AuthSlice = createSlice({
name: "auth",
initialState: {
value: false,
},
reducers: {
auth_toggle: (state) => {
state.value = !state.value;
},
},
});
// Action creators are generated for each case reducer function
export const { auth_toggle } = AuthSlice.actions;
export default AuthSlice.reducer;

View file

@ -0,0 +1,15 @@
import { configureStore } from "@reduxjs/toolkit";
import AuthReducer from "../Slices/AuthSlice/AuthSlice";
const store = configureStore({
reducer: {
auth: AuthReducer,
},
});
export default store;
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

View file

@ -0,0 +1,193 @@
import { useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import IconButton from "@mui/material/IconButton";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import { AppRegistration } from "@mui/icons-material";
import Button from "../Button/Button";
import { useNavigate } from "react-router-dom";
import { RegisterAPI } from "../API/API";
import { toast } from "react-toastify";
export default function RegisterModal() {
const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const [user, setUser] = useState({
first_name: "",
last_name: "",
username: "",
email: "",
password: "",
confirm_password: "",
});
const [error, setError] = useState("");
return (
<>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
overflowY: "scroll",
},
}}
>
<AppRegistration
style={{
height: 64,
width: 64,
fill: colors.font_dark,
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>Get Started</p>
</div>
<div style={styles.flex_column}>
<TextField
id="outlined-helperText"
label="First Name"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, first_name: e.target.value });
setError("");
}}
value={user.first_name}
placeholder={"Enter your first name"}
/>
<TextField
id="outlined-helperText"
label="Last Name"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, last_name: e.target.value })
}
value={user.last_name}
placeholder={"Enter your last name"}
/>
<TextField
id="outlined-helperText"
label="Email"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, email: e.target.value })
}
value={user.email}
placeholder={"Enter your email"}
/>
<TextField
id="outlined-helperText"
label="Username"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, username: e.target.value });
setError("");
}}
value={user.username}
placeholder={"Enter username"}
/>
<TextField
id="outlined-helperText"
type={showPassword ? "text" : "password"}
style={styles.input_form}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
label="Password"
placeholder={"Enter password"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, password: e.target.value })
}
value={user.password}
/>
<TextField
id="outlined-helperText"
type={showPassword ? "text" : "password"}
style={styles.input_form}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
label="Confirm Password"
placeholder={"Re-enter password"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, confirm_password: e.target.value });
setError("");
}}
value={user.confirm_password}
/>
</div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{error}</p>
<div
style={{
backgroundColor: colors.button_border,
marginTop: "16px",
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<Button
type={"dark"}
label={"Register"}
onClick={async () => {
if (user.password !== user.confirm_password) {
setError("Passwords do not match");
} else {
const status = await RegisterAPI(user);
if (status[0]) {
setError(
"Registration successful. Please activate your account using the email provided"
);
toast("Registration successful", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
setTimeout(() => {
navigate(0);
}, 3000);
setUser({
first_name: "",
last_name: "",
username: "",
email: "",
password: "",
confirm_password: "",
});
} else {
setError(JSON.stringify(status[1]));
}
}
}}
/>
</>
);
}

View file

@ -0,0 +1,91 @@
import { useState } from "react";
import styles from "../../styles";
import { colors } from "../../styles";
import TextField from "@mui/material/TextField";
import NewReleasesIcon from "@mui/icons-material/NewReleases";
import Button from "../Button/Button";
import { useNavigate } from "react-router-dom";
import { ResetPasswordAPI } from "../API/API";
import { useDispatch } from "react-redux";
import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { toast } from "react-toastify";
export default function ResetPasswordModal() {
const navigate = useNavigate();
const [error, setError] = useState("");
const [email, setEmail] = useState("");
const dispatch = useDispatch();
return (
<>
<div
style={{
...styles.flex_row,
...{
alignItems: "center",
justifyContent: "center",
overflowY: "scroll",
},
}}
>
<NewReleasesIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>Forgot Password</p>
</div>
<p style={{ ...styles.text_dark, ...styles.text_S }}>
Enter your email to request a password reset
</p>
<div style={styles.flex_column}>
<TextField
id="outlined-helperText"
label="Email"
style={styles.input_form}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
setError("");
}}
value={email}
placeholder={"Enter email associated with account"}
/>
<p style={{ ...styles.text_dark, ...styles.text_S }}>{error}</p>
<div
style={{
backgroundColor: colors.button_border,
width: "100%",
height: "2px",
marginBottom: 8,
}}
/>
<Button
type={"dark"}
label={"Confirm"}
onClick={async () => {
const status = await ResetPasswordAPI(email);
if (status === true) {
await dispatch(auth_toggle());
navigate("/");
toast("Reset request sent", {
position: "top-right",
autoClose: 6000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
setError(
"Password reset request sent. Please follow your email for further instructions"
);
} else {
setError("Invalid email specified");
}
}}
/>
</div>
</>
);
}

View file

@ -0,0 +1,60 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useLocation } from "react-router-dom";
import { JWTRefreshAPI, setAccessToken, setRefreshToken } from "../API/API";
import { auth_toggle } from "../Plugins/Redux/Slices/AuthSlice/AuthSlice";
import { RootState } from "../Plugins/Redux/Store/Store";
import { toast } from "react-toastify";
export default function Revalidator() {
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
const authenticated = useSelector((state: RootState) => state.auth.value);
const [rechecked, setRechecked] = useState(false);
useEffect(() => {
if (!authenticated && rechecked) {
if (location.pathname !== "/") {
navigate("/");
toast("Please log in to continue", {
position: "bottom-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
}
}
}, [authenticated, location.pathname, navigate, rechecked]);
useEffect(() => {
if (!authenticated) {
JWTRefreshAPI().then(async (response) => {
if (response) {
await dispatch(auth_toggle());
toast("User session restored", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
} else {
await setRefreshToken("");
await setAccessToken("");
}
setRechecked(true);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <></>;
}

View file

@ -0,0 +1,98 @@
export type RegisterType = {
email: string;
username: string;
password: string;
first_name: string;
last_name: string;
};
export type LoginType = {
username: string;
password: string;
};
export type ActivationType = {
uid: string;
token: string;
};
export type ResetPasswordConfirmType = {
uid: string;
token: string;
new_password: string;
};
export type AddEquipmentType = {
name: string;
description: string;
category?: string;
};
export type PatchEquipmentType = {
name: string;
description: string;
category?: string;
};
export type EquipmentType = {
id: number;
name: string;
description: string;
last_updated: string;
last_updated_by: string;
date_added: string;
category: string;
};
export type EquipmentListType = Array<EquipmentType>;
export type EquipmentLogType = {
history_id: number;
id: number;
name: string;
category: string;
description: string;
history_date: string;
history_user: string;
};
export type EquipmentLogListType = Array<EquipmentLogType>;
export type AddEquipmentInstanceType = {
equipment: number;
status: string;
remarks?: string;
};
export type PatchEquipmentInstanceType = {
status: string;
remarks?: string;
};
export type EquipmentInstanceType = {
id: number;
equipment: string;
equipment_name: string;
status: string;
remarks: string;
last_updated: string;
last_updated_by: string;
date_added: string;
category: string;
};
export type EquipmentInstanceListType = Array<EquipmentInstanceType>;
export type EquipmentInstanceLogType = {
history_id: number;
id: number;
equipment: number;
equipment_name: string;
category: string;
status: string;
remarks: string;
history_date: string;
history_user: string;
};
export type EquipmentInstanceLogListType = Array<EquipmentInstanceLogType>;

View file

@ -0,0 +1,98 @@
import { useNavigate, useParams } from "react-router-dom";
import styles, { colors } from "../../styles";
import { ActivationAPI } from "../../Components/API/API";
import { useEffect, useState } from "react";
import { CircularProgress } from "@mui/material";
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import { toast } from "react-toastify";
export default function ActivationPage() {
const { uid, token } = useParams();
const [feedback, setFeedback] = useState("");
const [error, setError] = useState(false);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
if (uid && token && feedback == "") {
ActivationAPI({ uid, token }).then((response) => {
if (response) {
setFeedback("Activation successful");
toast("Activation successful", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
toast("Please login to continue", {
position: "top-right",
autoClose: 6000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
setTimeout(() => {
navigate("/");
});
} else {
setFeedback("Invalid activation link");
setError(true);
}
});
}
if (!uid || !token) {
setFeedback("Missing uid or token");
}
setLoading(false);
}, [uid, token, feedback, navigate]);
return (
<div style={styles.background}>
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
alignSelf: "center",
justifyContent: "center",
height: "100%",
},
}}
>
{loading ? (
<CircularProgress style={{ height: "128px", width: "128px" }} />
) : (
<></>
)}
{error && !loading ? (
<ErrorOutlineIcon
style={{ height: "128px", width: "128px", color: colors.red }}
/>
) : (
<CheckCircleOutlineIcon
style={{ height: "128px", width: "128px", color: colors.green }}
/>
)}
<p style={{ ...styles.text_dark, ...styles.text_L }}>{feedback}</p>
<div
style={{
backgroundColor: colors.header_color,
marginTop: "16px",
width: "30%",
height: "4px",
marginBottom: 8,
}}
/>
<p style={{ ...styles.text_dark, ...styles.text_L }}>
Activating your CITC Equipment Tracker Account
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,616 @@
import Header from "../../Components/Header/Header";
import styles from "../../styles";
import { useQueries } from "@tanstack/react-query";
import { EquipmentsAPI, EquipmentInstancesAPI } from "../../Components/API/API";
import { Button, CircularProgress } from "@mui/material";
import ComputerIcon from "@mui/icons-material/Computer";
import RouterIcon from "@mui/icons-material/Router";
import CameraOutdoorIcon from "@mui/icons-material/CameraOutdoor";
import ChairIcon from "@mui/icons-material/Chair";
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import AddToQueueIcon from "@mui/icons-material/AddToQueue";
import NoteAddIcon from "@mui/icons-material/NoteAdd";
import NoteIcon from "@mui/icons-material/Note";
import ManageSearchIcon from "@mui/icons-material/ManageSearch";
import { colors } from "../../styles";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import AddSKUModal from "../../Components/AddSKUModal/AddSKUModal";
import Popup from "reactjs-popup";
import AddItemModal from "../../Components/AddItemModal/AddItemModal";
export default function Dashboard() {
const navigate = useNavigate();
const queries = useQueries({
queries: [
{
queryKey: ["equipments"],
queryFn: EquipmentsAPI,
},
{
queryKey: ["equipment_instances"],
queryFn: EquipmentInstancesAPI,
},
],
});
const isLoading = queries.some((result) => result.isLoading);
const [addSKUmodalOpen, SetAddSKUModalOpen] = useState(false);
const [additemmodalOpen, SetAddItemModalOpen] = useState(false);
if (isLoading) {
return (
<div style={styles.background}>
<Header label={"Dashboard"} />
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
</div>
);
}
return (
<div style={styles.background}>
<Header label={"Dashboard"} />
<div style={styles.flex_column}>
<div
style={{
...styles.flex_row,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<div
style={{
paddingLeft: "16px",
paddingRight: "16px",
margin: "16px",
borderRadius: 16,
backgroundColor: "#a6a6a6",
alignSelf: "center",
justifyContent: "center",
width: "16rem",
}}
>
<p
style={{
...styles.text_dark,
...styles.text_M,
...{ float: "left", position: "absolute" },
}}
>
SKUs in Database
</p>
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
{queries[0].data ? queries[0].data.length : 0}
</p>
</div>
<div
style={{
paddingLeft: "16px",
paddingRight: "16px",
margin: "16px",
borderRadius: 16,
backgroundColor: "#a6a6a6",
alignSelf: "center",
justifyContent: "center",
width: "16rem",
}}
>
<p
style={{
...styles.text_dark,
...styles.text_M,
...{ float: "left", position: "absolute" },
}}
>
Items in Database
</p>
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
{queries[1].data ? queries[1].data.length : 0}
</p>
</div>
</div>
<div
style={{
...styles.flex_row,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<div
style={{
paddingLeft: "16px",
paddingRight: "16px",
margin: "16px",
borderRadius: 16,
backgroundColor: "#a6a6a6",
alignSelf: "center",
justifyContent: "center",
width: "16rem",
}}
>
<p
style={{
...styles.text_dark,
...styles.text_M,
...{ float: "left", position: "absolute" },
}}
>
Functional Items
</p>
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
{queries[1].data
? queries[1].data.filter(
(equipment) => equipment.status == "WORKING"
).length
: 0}
</p>
</div>
<div
style={{
paddingLeft: "16px",
paddingRight: "16px",
margin: "16px",
borderRadius: 16,
backgroundColor: "#a6a6a6",
alignSelf: "center",
justifyContent: "center",
width: "16rem",
}}
>
<p
style={{
...styles.text_dark,
...styles.text_M,
...{ float: "left", position: "absolute" },
}}
>
Broken Items
</p>
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
{queries[1].data
? queries[1].data.filter(
(equipment) => equipment.status == "BROKEN"
).length
: 0}
</p>
</div>
</div>
</div>
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Equipments
</p>
<div
style={{
...styles.flex_row,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
onClick={() => {
navigate("/view/equipment_instances");
}}
>
<FormatListBulletedIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
View All
</p>
</Button>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
onClick={() => {
SetAddItemModalOpen(true);
}}
>
<AddToQueueIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
Add Item
</p>
</Button>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
onClick={() => {
SetAddSKUModalOpen(true);
}}
>
<NoteAddIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
Add SKU
</p>
</Button>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
onClick={() => {
navigate("/view/equipments");
}}
>
<NoteIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
View SKUs
</p>
</Button>
</div>
<div
style={{
...styles.flex_row,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<ComputerIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
Workstations
</p>
</Button>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<RouterIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
Networking
</p>
</Button>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<CameraOutdoorIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
CCTVs
</p>
</Button>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<ChairIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
Furniture
</p>
</Button>
</div>
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Logs
</p>
<div
style={{
...styles.flex_row,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
onClick={() => {
navigate("/view/equipments/logs");
}}
>
<ManageSearchIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
SKU Logs
</p>
</Button>
<Button
style={{
...styles.flex_column,
...{
alignSelf: "center",
justifyContent: "center",
flexWrap: "wrap",
},
}}
onClick={() => {
navigate("/view/equipment_instances/logs");
}}
>
<ManageSearchIcon
style={{
height: 64,
width: 64,
fill: colors.font_dark,
marginLeft: "1rem",
marginRight: "1rem",
}}
/>
<p
style={{
...styles.text_dark,
...styles.text_M,
}}
>
Item Logs
</p>
</Button>
</div>
<Popup
open={addSKUmodalOpen}
onClose={() => SetAddSKUModalOpen(false)}
modal
position={"top center"}
contentStyle={{
width: "32rem",
borderRadius: 16,
borderColor: "grey",
borderStyle: "solid",
borderWidth: 1,
padding: 16,
alignContent: "center",
justifyContent: "center",
textAlign: "center",
}}
>
<AddSKUModal />
</Popup>
<Popup
open={additemmodalOpen}
onClose={() => SetAddItemModalOpen(false)}
modal
position={"top center"}
contentStyle={{
width: "32rem",
borderRadius: 16,
borderColor: "grey",
borderStyle: "solid",
borderWidth: 1,
padding: 16,
alignContent: "center",
justifyContent: "center",
textAlign: "center",
}}
>
<AddItemModal />
</Popup>
</div>
);
}

View file

@ -0,0 +1,140 @@
import { useQuery } from "@tanstack/react-query";
import Header from "../../Components/Header/Header";
import styles from "../../styles";
import { EquipmentInstanceLogsAPI } from "../../Components/API/API";
import { CircularProgress } from "@mui/material";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { colors } from "../../styles";
export default function EquipmentInstanceLogsPage() {
const equipment_instance_logs = useQuery({
queryKey: ["equipment_instance_logs"],
queryFn: EquipmentInstanceLogsAPI,
});
if (equipment_instance_logs.isLoading) {
return (
<div style={styles.background}>
<Header label={"Dashboard"} />
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
</div>
);
}
return (
<div style={styles.background}>
<Header label={"Item History"} />
<div
style={{
...styles.flex_column,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
minHeight: "100%",
minWidth: "100%",
flexWrap: "wrap",
}}
>
<div style={{ width: "90%", overflowY: "scroll", marginTop: "2rem" }}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: "32rem" }} size="medium">
<TableHead>
<TableRow style={{ backgroundColor: colors.header_color }}>
<TableCell align="center" style={styles.text_light}>
Transaction ID
</TableCell>
<TableCell align="center" style={styles.text_light}>
Item ID
</TableCell>
<TableCell align="center" style={styles.text_light}>
SKU
</TableCell>
<TableCell align="center" style={styles.text_light}>
Remarks
</TableCell>
<TableCell align="center" style={styles.text_light}>
Status
</TableCell>
<TableCell align="center" style={styles.text_light}>
Date Modified
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{equipment_instance_logs.data ? (
equipment_instance_logs.data.map((equipment_instance_log) => (
<TableRow
key={equipment_instance_log.history_id}
sx={{
"&:last-child td, &:last-child th": { border: 0 },
}}
>
<TableCell align="center" component="th" scope="row">
{equipment_instance_log.history_id}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment_instance_log.id}
</TableCell>
<TableCell align="center" component="th" scope="row">
{`SKU #${equipment_instance_log.equipment} - ${equipment_instance_log.equipment_name}`}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment_instance_log.remarks}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment_instance_log.status}
</TableCell>
<TableCell align="right">
<div
style={{
...styles.flex_column,
...{ alignItems: "center" },
}}
>
<div>{equipment_instance_log.history_date}</div>
<div>
{equipment_instance_log.history_user
? "by " + equipment_instance_log.history_user
: ""}
</div>
</div>
</TableCell>
</TableRow>
))
) : (
<></>
)}
</TableBody>
</Table>
</TableContainer>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,148 @@
import { useQuery } from "@tanstack/react-query";
import Header from "../../Components/Header/Header";
import styles from "../../styles";
import { EquipmentInstancesAPI } from "../../Components/API/API";
import { CircularProgress } from "@mui/material";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { colors } from "../../styles";
import EditItemModal from "../../Components/EditItemInstanceModal/EditItemInstanceModal";
import { useState } from "react";
import Popup from "reactjs-popup";
export default function EquipmentInstancesListPage() {
const [editmodalOpen, SetEditModalOpen] = useState(false);
const [selectedItem, SetSelectedItem] = useState(0);
const equipment_instances = useQuery({
queryKey: ["equipment_instances"],
queryFn: EquipmentInstancesAPI,
});
if (equipment_instances.isLoading) {
return (
<div style={styles.background}>
<Header label={"Dashboard"} />
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
</div>
);
}
return (
<div style={styles.background}>
<Header label={"Items List"} />
<div
style={{
...styles.flex_column,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
minHeight: "100%",
minWidth: "100%",
flexWrap: "wrap",
}}
>
<div style={{ width: "90%", overflowY: "scroll", marginTop: "2rem" }}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: "32rem" }} size="medium">
<TableHead>
<TableRow style={{ backgroundColor: colors.header_color }}>
<TableCell style={styles.text_light}>ID</TableCell>
<TableCell align="center" style={styles.text_light}>
Name
</TableCell>
<TableCell align="center" style={styles.text_light}>
Status
</TableCell>
<TableCell align="center" style={styles.text_light}>
Category
</TableCell>
<TableCell align="center" style={styles.text_light}>
Last Modified
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{equipment_instances.data ? (
equipment_instances.data.map((equipment) => (
<TableRow
key={equipment.id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
onClick={() => {
SetSelectedItem(equipment.id);
SetEditModalOpen(true);
}}
>
<TableCell align="center" component="th" scope="row">
{equipment.id}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment.equipment_name}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment.status}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment.category}
</TableCell>
<TableCell align="right">
<div
style={{
...styles.flex_column,
...{ alignItems: "center" },
}}
>
<div>{equipment.last_updated}</div>
<div>
{equipment.last_updated_by
? "by " + equipment.last_updated_by
: ""}
</div>
</div>
</TableCell>
</TableRow>
))
) : (
<></>
)}
</TableBody>
</Table>
</TableContainer>
</div>
</div>
<Popup
open={editmodalOpen}
onClose={() => SetEditModalOpen(false)}
modal
position={"top center"}
contentStyle={styles.popup_center}
>
<EditItemModal id={selectedItem} setOpen={SetEditModalOpen} />
</Popup>
</div>
);
}

View file

@ -0,0 +1,148 @@
import { useQuery } from "@tanstack/react-query";
import Header from "../../Components/Header/Header";
import styles from "../../styles";
import { EquipmentsAPI } from "../../Components/API/API";
import { CircularProgress } from "@mui/material";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { colors } from "../../styles";
import EditSKUModal from "../../Components/EditSKUModal/EditSKUModal";
import Popup from "reactjs-popup";
import { useState } from "react";
export default function EquipmentListPage() {
const [editmodalOpen, SetEditModalOpen] = useState(false);
const [selectedItem, SetSelectedItem] = useState(0);
const equipments = useQuery({
queryKey: ["equipments"],
queryFn: EquipmentsAPI,
});
if (equipments.isLoading) {
return (
<div style={styles.background}>
<Header label={"Dashboard"} />
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
</div>
);
}
return (
<div style={styles.background}>
<Header label={"SKU List"} />
<div
style={{
...styles.flex_column,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
minHeight: "100%",
minWidth: "100%",
flexWrap: "wrap",
}}
>
<div style={{ width: "90%", overflowY: "scroll", marginTop: "2rem" }}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: "32rem" }} size="medium">
<TableHead>
<TableRow style={{ backgroundColor: colors.header_color }}>
<TableCell style={styles.text_light}>ID</TableCell>
<TableCell align="center" style={styles.text_light}>
Name
</TableCell>
<TableCell align="center" style={styles.text_light}>
Description
</TableCell>
<TableCell align="center" style={styles.text_light}>
Category
</TableCell>
<TableCell align="center" style={styles.text_light}>
Last Modified
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{equipments.data ? (
equipments.data.map((equipment) => (
<TableRow
key={equipment.id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
onClick={() => {
SetSelectedItem(equipment.id);
SetEditModalOpen(true);
}}
>
<TableCell align="center" component="th" scope="row">
{equipment.id}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment.name}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment.description}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment.category}
</TableCell>
<TableCell align="right">
<div
style={{
...styles.flex_column,
...{ alignItems: "center" },
}}
>
<div>{equipment.last_updated}</div>
<div>
{equipment.last_updated_by
? "by " + equipment.last_updated_by
: ""}
</div>
</div>
</TableCell>
</TableRow>
))
) : (
<></>
)}
</TableBody>
</Table>
</TableContainer>
</div>
</div>
<Popup
open={editmodalOpen}
onClose={() => SetEditModalOpen(false)}
modal
position={"top center"}
contentStyle={styles.popup_center}
>
<EditSKUModal id={selectedItem} setOpen={SetEditModalOpen} />
</Popup>
</div>
);
}

View file

@ -0,0 +1,138 @@
import { useQuery } from "@tanstack/react-query";
import Header from "../../Components/Header/Header";
import styles from "../../styles";
import { EquipmentLogsAPI } from "../../Components/API/API";
import { CircularProgress } from "@mui/material";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { colors } from "../../styles";
export default function EquipmentLogsPage() {
const equipment_logs = useQuery({
queryKey: ["equipment_logs"],
queryFn: EquipmentLogsAPI,
});
if (equipment_logs.isLoading) {
return (
<div style={styles.background}>
<Header label={"Dashboard"} />
<div
style={{
...styles.flex_column,
...{
alignItems: "center",
justifyContent: "center",
paddingTop: "64px",
},
}}
>
<CircularProgress style={{ height: "128px", width: "128px" }} />
<p
style={{
...styles.text_dark,
...styles.text_L,
}}
>
Loading
</p>
</div>
</div>
);
}
return (
<div style={styles.background}>
<Header label={"SKU History"} />
<div
style={{
...styles.flex_column,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
minHeight: "100%",
minWidth: "100%",
flexWrap: "wrap",
}}
>
<div style={{ width: "90%", overflowY: "scroll", marginTop: "2rem" }}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: "32rem" }} size="medium">
<TableHead>
<TableRow style={{ backgroundColor: colors.header_color }}>
<TableCell align="center" style={styles.text_light}>
Transaction ID
</TableCell>
<TableCell align="center" style={styles.text_light}>
SKU ID
</TableCell>
<TableCell align="center" style={styles.text_light}>
Name
</TableCell>
<TableCell align="center" style={styles.text_light}>
Description
</TableCell>
<TableCell align="center" style={styles.text_light}>
Category
</TableCell>
<TableCell align="center" style={styles.text_light}>
Date Modified
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{equipment_logs.data ? (
equipment_logs.data.map((equipment_log) => (
<TableRow
key={equipment_log.history_id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell align="center" component="th" scope="row">
{equipment_log.history_id}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment_log.id}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment_log.name}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment_log.description}
</TableCell>
<TableCell align="center" component="th" scope="row">
{equipment_log.category}
</TableCell>
<TableCell align="right">
<div
style={{
...styles.flex_column,
...{ alignItems: "center" },
}}
>
<div>{equipment_log.history_date}</div>
<div>
{equipment_log.history_user
? "by " + equipment_log.history_user
: ""}
</div>
</div>
</TableCell>
</TableRow>
))
) : (
<></>
)}
</TableBody>
</Table>
</TableContainer>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,3 @@
export default function ErrorPage() {
return <div>{"ErrorPage"}</div>;
}

View file

@ -0,0 +1,161 @@
import Button from "../../Components/Button/Button";
import styles from "../../styles";
import citc_logo from "../../assets/citc_logo.jpg";
import Popup from "reactjs-popup";
import "reactjs-popup/dist/index.css";
import { useEffect, useState } from "react";
import LoginModal from "../../Components/LoginModal/LoginModal";
import RegisterModal from "../../Components/RegisterModal/RegisterModal";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { RootState } from "../../Components/Plugins/Redux/Store/Store";
import ResetPasswordModal from "../../Components/ResetPasswordModal/ResetPasswordModal";
export default function LandingPage() {
const [loginmodalOpen, SetloginmodalOpen] = useState(false);
const [registermodalOpen, SetRegisterModalOpen] = useState(false);
const [resetmodalOpen, SetResetModalOpen] = useState(false);
const authenticated = useSelector((state: RootState) => state.auth.value);
const navigate = useNavigate();
useEffect(() => {
if (authenticated) {
navigate("/dashboard");
console.log("Already logged in. Redirecting to dashboard page");
}
}, [authenticated, navigate]);
return (
<div style={styles.background}>
<div
style={{
...styles.flex_row,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
minHeight: "100%",
minWidth: "100%",
flexWrap: "wrap",
}}
>
<div style={{ height: "auto", flex: 1, flexWrap: "wrap" }}>
<img style={{ width: "16rem", height: "auto" }} src={citc_logo} />
</div>
<div
style={{
height: "auto",
flex: 1,
flexWrap: "wrap",
}}
>
<div
style={{
minWidth: "30vw",
borderRadius: 4,
borderColor: "grey",
borderStyle: "solid",
borderWidth: 1,
padding: 16,
margin: 64,
paddingBottom: "16vh",
paddingTop: "16vh",
}}
>
<p style={{ ...styles.text_dark, ...styles.text_L }}>
CITC EQUIPMENT
<br />
TRACKER
</p>
<div style={{ ...styles.flex_column }}>
<Button
type={"light"}
label={"Login"}
onClick={() => {
SetloginmodalOpen(true);
SetRegisterModalOpen(false);
SetResetModalOpen(false);
}}
/>
<Button
type={"dark"}
label={"Register"}
onClick={() => {
SetRegisterModalOpen(true);
SetloginmodalOpen(false);
SetResetModalOpen(false);
}}
/>
<Button
type={"light"}
label={"Forgot Password"}
onClick={() => {
SetResetModalOpen(true);
SetRegisterModalOpen(false);
SetloginmodalOpen(false);
}}
/>
<Popup
open={loginmodalOpen}
onClose={() => SetloginmodalOpen(false)}
modal
position={"top center"}
contentStyle={{
width: "512px",
borderRadius: 16,
borderColor: "grey",
borderStyle: "solid",
borderWidth: 1,
padding: 16,
alignContent: "center",
justifyContent: "center",
textAlign: "center",
}}
>
<LoginModal />
</Popup>
<Popup
open={registermodalOpen}
onClose={() => SetRegisterModalOpen(false)}
modal
position={"top center"}
contentStyle={{
width: "512px",
borderRadius: 16,
borderColor: "grey",
borderStyle: "solid",
borderWidth: 1,
padding: 16,
alignContent: "center",
justifyContent: "center",
textAlign: "center",
}}
>
<RegisterModal />
</Popup>
<Popup
open={resetmodalOpen}
onClose={() => SetResetModalOpen(false)}
modal
position={"top center"}
contentStyle={{
width: "512px",
borderRadius: 16,
borderColor: "grey",
borderStyle: "solid",
borderWidth: 1,
padding: 16,
alignContent: "center",
justifyContent: "center",
textAlign: "center",
}}
>
<ResetPasswordModal />
</Popup>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,148 @@
import { useNavigate, useParams } from "react-router-dom";
import styles, { colors } from "../../styles";
import { ResetPasswordConfirmAPI } from "../../Components/API/API";
import { useState } from "react";
import { toast } from "react-toastify";
import { VisibilityOff, Visibility } from "@mui/icons-material";
import { TextField, InputAdornment, IconButton } from "@mui/material";
import Button from "../../Components/Button/Button";
export default function ResetPasswordPage() {
const { uid, token } = useParams();
const [feedback, setFeedback] = useState("");
const [user, setUser] = useState({
password: "",
confirm_password: "",
});
const [showPassword, setShowPassword] = useState(false);
const navigate = useNavigate();
return (
<div style={styles.background}>
<div
style={{
...styles.flex_column,
...{
justifyContent: "center",
verticalAlign: "center",
height: "100%",
},
}}
>
<p style={{ ...styles.text_dark, ...styles.text_L }}>
Confirm Password Reset
</p>
<TextField
id="outlined-helperText"
type={showPassword ? "text" : "password"}
style={styles.input_form}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => {
setShowPassword(!showPassword);
setFeedback("");
}}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
label="New Password"
placeholder={"Enter new password"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, password: e.target.value })
}
value={user.password}
/>
<TextField
id="outlined-helperText"
type={showPassword ? "text" : "password"}
style={styles.input_form}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => {
setShowPassword(!showPassword);
setFeedback("");
}}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
label="Confirm Password"
placeholder={"Re-enter password"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUser({ ...user, confirm_password: e.target.value })
}
value={user.confirm_password}
/>
<div style={{ justifyContent: "center", display: "flex" }}>
<div
style={{
backgroundColor: colors.header_color,
marginTop: "16px",
width: "80%",
height: "4px",
marginBottom: 8,
}}
/>
</div>
<p style={{ ...styles.text_dark, ...styles.text_M }}>{feedback}</p>
<Button
type={"dark"}
label={"Confirm"}
onClick={() => {
if (uid && token && feedback == "") {
ResetPasswordConfirmAPI({
uid,
token,
new_password: user.password,
}).then((response) => {
if (response) {
setFeedback("Reset successful");
toast("Reset successful", {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
toast("Please login to continue", {
position: "top-right",
autoClose: 6000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
setTimeout(() => {
navigate("/");
});
} else {
setFeedback("Invalid token specified for password reset");
}
});
}
if (!uid || !token) {
setFeedback("Missing token for password reset");
}
}}
/>
</div>
</div>
);
}

BIN
src/assets/citc_logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View file

@ -1,10 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

92
src/styles.tsx Normal file
View file

@ -0,0 +1,92 @@
export const colors = {
background: "#FFFFFF",
header_color: "#141762",
font_dark: "#141762",
font_light: "#FFFFFF",
button_dark: "#141762",
button_light: "#FFFFFF",
button_border: "#141762",
red: "#a44141",
orange: "#c57331",
green: "#80b28a",
};
const styles: { [key: string]: React.CSSProperties } = {
background: {
backgroundColor: colors.background,
position: "fixed",
top: 0,
left: 0,
height: "100%",
width: "100%",
minHeight: "100%",
minWidth: "100%",
overflowY: "scroll",
},
text_dark: {
color: colors.font_dark,
fontWeight: "bold",
},
text_light: {
color: colors.font_light,
fontWeight: "bold",
},
text_red: {
color: colors.red,
fontWeight: "bold",
},
text_orange: {
color: colors.orange,
fontWeight: "bold",
},
text_green: {
color: colors.green,
fontWeight: "bold",
},
text_XL: {
fontSize: "clamp(2rem, 3rem, 8rem)",
},
text_L: {
fontSize: "clamp(1.5rem, 2rem, 6rem)",
},
text_M: {
fontSize: "clamp(1rem, 1rem, 4rem)",
},
text_S: {
fontSize: "clamp(0.6rem, 0.8rem, 1rem)",
},
text_XS: {
fontSize: "clamp(0.5rem, 0.6rem, 0.8rem)",
},
flex_row: {
display: "flex",
flexDirection: "row",
},
flex_column: {
display: "flex",
flexDirection: "column",
},
input_form: {
color: colors.font_dark,
fontWeight: "bold",
fontSize: "clamp(1vw, 1rem, 2vw)",
background: "none",
borderRadius: 8,
maxWidth: "128px",
minWidth: "100%",
marginTop: 16,
},
popup_center: {
width: "32rem",
borderRadius: 16,
borderColor: "grey",
borderStyle: "solid",
borderWidth: 1,
padding: 16,
alignContent: "center",
justifyContent: "center",
textAlign: "center",
overflowY: "scroll",
},
};
export default styles;

View file

@ -18,7 +18,11 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"paths":{
"@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]

17
woodpecker.yml Normal file
View file

@ -0,0 +1,17 @@
pipeline:
- name: build
image: node:14
commands:
- npm install
- npm run build
- name: copy
image: alpine
environment:
- SSH_KEY:
from_secret: ssh_key
commands:
- apk add --no-cache openssh-client
- echo "$SSH_KEY" | tr -d '\r' > /root/.ssh/id_rsa
- chmod 600 /root/.ssh/id_rsa
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > /root/.ssh/config
- scp -r dist/* username@10.0.10.4:/mnt/sda1/projects/equipment_tracker_frontend