Skip to content

Commit

Permalink
Merge pull request #66 from jamesspearsv/feature/admin-reporting-ui
Browse files Browse the repository at this point in the history
Feature/admin reporting UI
  • Loading branch information
jamesspearsv authored Dec 18, 2024
2 parents be5d9a7 + 2e58117 commit 5c5b3d7
Show file tree
Hide file tree
Showing 29 changed files with 556 additions and 41 deletions.
18 changes: 12 additions & 6 deletions api/src/controllers/adminController.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ async function countTable(req, res, next) {

async function adminReportGet(req, res, next) {
try {
const { start, end, category } = req.query;
if (!start || !end || !category) {
const { startMonth, endMonth, category } = req.query;
if (!startMonth || !endMonth || !category) {
throw new BadRequestError("Start, end, and category must be provided");
}

Expand All @@ -137,15 +137,18 @@ async function adminReportGet(req, res, next) {
}

// query database for cumulative interactions counts during range
const total_interactions = await queries.countInteractionsAdmin(start, end);
const total_interactions = await queries.countInteractionsAdmin(
startMonth,
endMonth,
);
const total_detailed = await queries.countInteractionByCategoryAdmin(
start,
end,
startMonth,
endMonth,
category,
);

// parse range between start and end month
const range = parseMonthRange(start, end);
const range = parseMonthRange(startMonth, endMonth);
const monthly_details = [];
// query database for each month in range
for (const month of range) {
Expand All @@ -167,12 +170,15 @@ async function adminReportGet(req, res, next) {
monthly_details.push(monthObject);
}

const keys = Object.keys(monthly_details[0]);

res.json({
message: "ok",
range,
total_interactions,
total_detailed,
monthly_details,
keys,
});
} catch (error) {
return next(error);
Expand Down
1 change: 1 addition & 0 deletions client/src/Admin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function Admin() {
navItems={[
{ label: "Back to App", route: "/" },
{ label: "Dashboard", route: "/admin" },
{ label: "Reporting", route: "/admin/reporting" },
{ label: "Database", route: "/admin/database" },
]}
/>
Expand Down
14 changes: 10 additions & 4 deletions client/src/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import PropTypes from "prop-types";
import styles from "./Button.module.css";

/*
/**
* Custom button component
* Variant corresponds to styles in Button.module.css
* @param {string} [id] - prop to set `data-id` property of button
* @param {string} text - button text property
* @param {(e: Event) => void} [action] - button click callback function
* @param {'primary' | 'danger'} variant - styled button variant
* @param {'button' | 'submit'} type
* @param {Object} [style] - Optional React style object
* @returns {JSX.Element}
*/

function Button({ id, text, action, variant, type, style }) {
Expand All @@ -23,8 +29,8 @@ function Button({ id, text, action, variant, type, style }) {
Button.propTypes = {
id: PropTypes.string,
text: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
variant: PropTypes.string.isRequired,
type: PropTypes.oneOf(["button", "submit"]).isRequired,
variant: PropTypes.oneOf(["primary", "danger"]).isRequired,
action: PropTypes.func,
style: PropTypes.object,
};
Expand Down
7 changes: 6 additions & 1 deletion client/src/components/CardWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import styles from "./CardWrapper.module.css";
import PropTypes from "prop-types";

/* Wrapper to provide UI styling to elements */
/**
* Wrapper to provide UI styling to children
* @param {React.ReactNode} children
* @param {Object} [style] - optional React inline styles
* @returns {JSX.Element}
*/

function CardWrapper({ children, style }) {
return (
Expand Down
7 changes: 7 additions & 0 deletions client/src/components/CountReport.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import styles from "./CountReport.module.css";
* id, value, number_of_interactions
*/

/**
* Component used to display count summary on app report page
* @param {string} title - report title
* @param {{id: number, value: string, number_of_interaction: number}} count - report from database
* @returns {JSX.Element}
*/

function CountReport({ title, count }) {
return (
<div className={styles.report}>
Expand Down
21 changes: 17 additions & 4 deletions client/src/components/DateInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@ import styles from "./DateInput.module.css";

/* Custom date input with label and callback */

function DateInput({ label, value, handleChange }) {
const id = label.toLowerCase();
/**
* Custom date input with label and callback
* @param {string} id
* @param {string} label
* @param {'date' | 'month'} type
* @param {string} value
* @param {(e: Event) => void} handleChange
* @returns {JSX.Element}
*/

function DateInput({ id, label, type, value, handleChange }) {
return (
<div className={styles.date}>
<label htmlFor={id} className={styles.label}>{`${label} Date`}</label>
<label htmlFor={id} className={styles.label}>
{label}
</label>
<input
type="date"
type={type}
name={id}
id={id}
placeholder={type === "month" ? "YYYY-MM" : undefined}
value={value}
onChange={handleChange}
/>
Expand All @@ -23,7 +34,9 @@ function DateInput({ label, value, handleChange }) {
export default DateInput;

DateInput.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
type: PropTypes.oneOf(["date", "month"]).isRequired,
value: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
};
7 changes: 6 additions & 1 deletion client/src/components/ErrorComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import PropTypes from "prop-types";

/* Error message page */
/**
* Custom error component used throughout app
* @param {string} status - HTTP status code represented as a string
* @returns {JSX.Element}
* @constructor
*/

function ErrorComponent({ status }) {
return (
Expand Down
10 changes: 10 additions & 0 deletions client/src/components/Form.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import PropTypes from "prop-types";
import styles from "./Form.module.css";

/**
* Custom form component used to render and display form elements
* @param {(e: Event) => void} [onSubmit] - callback function to run when form is submitted
* @param {string} [title] - optional form title
* @param {Object} [style] - optional React inline styles
* @param {React.ReactNode} children
* @returns {JSX.Element}
* @constructor
*/

function Form({ onSubmit, title, style, children }) {
return (
<form onSubmit={onSubmit} className={styles.form} style={style}>
Expand Down
10 changes: 9 additions & 1 deletion client/src/components/Modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import PropTypes from "prop-types";
import styles from "./Modal.module.css";
import Button from "./Button";

/* Utility modal component */
/**
* Custom modal component
* @param {boolean} isOpen
* @param {(boolean) => void} setIsOpen
* @param {React.ReactNode} children
* @param {Object} [style]
* @returns {JSX.Element}
* @constructor
*/

function Modal({ isOpen, setIsOpen, children, style }) {
const modalRef = useRef(null);
Expand Down
7 changes: 6 additions & 1 deletion client/src/components/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { NavLink } from "react-router-dom";
import PropTypes from "prop-types";
import styles from "./Nav.module.css";

/* App navigation menu */
/**
* Add navigation menu component
* @param {Array<{label: string, route: string}>} navItems
* @returns {JSX.Element}
* @constructor
*/

function Nav({ navItems }) {
return (
Expand Down
23 changes: 18 additions & 5 deletions client/src/components/SelectInput.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import PropTypes from "prop-types";
import styles from "./SelectInput.module.css";

/* Custom select input with label, options, and callback */
/**
* Custom select input with id, label, options, and a callback
* @param {string} id
* @param {string} label
* @param {Array<{id: number | string, value: string}>}options
* @param {(e: Event) => void} handleChange
* @param {string} value
* @returns {JSX.Element}
* @constructor
*/

function SelectInput({ label, options, handleChange, value }) {
function SelectInput({ id, label, options, handleChange, value }) {
const style = {
display: "flex",
flexDirection: "column",
};

const id = label.toLowerCase();

return (
<div style={style} className={styles.customSelect}>
<label htmlFor={id} className={styles.label}>
Expand All @@ -29,8 +36,14 @@ function SelectInput({ label, options, handleChange, value }) {
}

SelectInput.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.string.isRequired,
}),
).isRequired,
handleChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
};
Expand Down
9 changes: 8 additions & 1 deletion client/src/components/TabSelector.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import PropTypes from "prop-types";
import styles from "./TabSelector.module.css";

/* Custom tab selector element */
/**
* Custom tab selector component
* @param {Arry<{id: string, label: string}>} tabs
* @param {(e: Event) => void} handleClick
* @param {string} activeTab
* @returns {JSX.Element}
* @constructor
*/

function TabSelector({ tabs, handleClick, activeTab }) {
return (
Expand Down
11 changes: 11 additions & 0 deletions client/src/components/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import Button from "../components/Button.jsx";
* Component to parse and render a given dataset in a table format
*/

/**
* Table component to parse and render a given dataset
* @param {Array<Object>} rows - dataset being rendered by table
* @param {Array<{key: string, label: value}>} columns - array representing columns in given dataset
* @param {Object} [style] - optional react styles object
* @param {boolean} [readonly] - boolean value to toggle readonly table mode
* @param {{text: string, action: (e: Event) => void}} [button] - optional button for editable tables
* @returns {JSX.Element}
* @constructor
*/

function Table({ rows, columns, style, readonly, button }) {
return (
<div className={styles.table} style={style}>
Expand Down
18 changes: 18 additions & 0 deletions client/src/lib/parseMonthInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Validates dates strings for YYYY-MM format
* @param {Array<string>} inputs
* @returns {Array<boolean>}
*/

export default function parseMonthInput(inputs) {
const regex = /[0-9]{4}-[0-9]{2}/;
const results = [];
for (const input of inputs) {
const match = input.match(regex);

if (!match || match[0] !== input) results.push(false);
else results.push(true);
}

return results;
}
8 changes: 6 additions & 2 deletions client/src/lib/response.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/*
Helper functions to validate API responses
/**
* Helper function to validate API responses
* @param {Response} res
* @param {Object} json
* @param {() => void} setAuth
* @returns {void}
*/

export function validateAdminResponse(res, json, setAuth) {
Expand Down
7 changes: 6 additions & 1 deletion client/src/pages-admin/Dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import { Navigate, useOutletContext } from "react-router-dom";
import DashboardStats from "./DashboardStats.jsx";
import DashboardTable from "./DashboardTable.jsx";

/**
* Dashboard page in admin app
* @returns {JSX.Element}
*/

function Dashboard() {
const { auth } = useOutletContext();

if (!auth) return <Navigate to={"/admin/login"} />;

return (
<div>
<div style={{ margin: "0 2.5rem" }}>
<DashboardStats />
<DashboardTable />
</div>
Expand Down
7 changes: 6 additions & 1 deletion client/src/pages-admin/Dashboard/DashboardStats.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import {
import { validateAdminResponse } from "../../lib/response.js";
import ErrorComponent from "../../components/ErrorComponent.jsx";

/**
* Component used to fetch and render database stats
* @returns {JSX.Element}
*/

function DashboardStats() {
const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#185c36"];
const { apihost, auth, setAuth } = useOutletContext();
Expand Down Expand Up @@ -117,7 +122,7 @@ function DashboardStats() {
<div>
{row.total_interactions} (
{(
(parseFloat(row.total_interactions) /
(parseInt(row.total_interactions) /
parseInt(stats.count_total)) *
100
).toFixed()}
Expand Down
6 changes: 5 additions & 1 deletion client/src/pages-admin/Dashboard/DashboardTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import CardWrapper from "../../components/CardWrapper.jsx";
import ErrorComponent from "../../components/ErrorComponent.jsx";
import { validateAdminResponse } from "../../lib/response.js";

/**
* Component used to fetch and render all rows in interactions table
* @returns {JSX.Element}
*/

function DashboardTable() {
const { apihost, auth, setAuth } = useOutletContext();
const [rows, setRows] = useState([]);
Expand Down Expand Up @@ -35,7 +40,6 @@ function DashboardTable() {
validateAdminResponse(res, json, setAuth);

const total = Math.ceil(json.total_rows / limit);
console.log(total);
setTotalPages(total);
} catch (error) {
console.error(error);
Expand Down
Loading

0 comments on commit 5c5b3d7

Please sign in to comment.