From be0c3c8d028b2dfdbd3d6d20e2372ccf9f16ca45 Mon Sep 17 00:00:00 2001 From: James Spears Date: Tue, 3 Dec 2024 15:13:59 -0500 Subject: [PATCH 01/28] Add /admin/report endpoint & configure count queries for admin reporting Signed-off-by: James Spears --- api/package-lock.json | 19 +++++++++++++++- api/package.json | 3 ++- api/src/controllers/adminController.js | 30 ++++++++++++++++++++++++++ api/src/db/queries.js | 17 ++++++++++++++- api/src/routes/adminRoutes.js | 2 ++ http-requests/admin.http | 8 +++++-- 6 files changed, 74 insertions(+), 5 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index fe89805..ac76d88 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -18,7 +18,8 @@ }, "devDependencies": { "concurrently": "^9.0.1", - "nodemon": "^3.1.7" + "nodemon": "^3.1.7", + "prettier": "^3.4.1" } }, "node_modules/accepts": { @@ -1535,6 +1536,22 @@ "node": ">=10" } }, + "node_modules/prettier": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", + "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/api/package.json b/api/package.json index 3ed6c2a..6f9570f 100644 --- a/api/package.json +++ b/api/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "concurrently": "^9.0.1", - "nodemon": "^3.1.7" + "nodemon": "^3.1.7", + "prettier": "^3.4.1" } } diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 565f04e..1f13397 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -1,6 +1,7 @@ /* Middleware to handle actions requiring admin authorization */ const queries = require("../db/queries"); const { BadRequestError } = require("../lib/errorsClasses"); +const { countInteractionsAdmin } = require("../db/queries"); // Get all rows from a given table async function tableGet(req, res, next) { @@ -123,6 +124,34 @@ async function countTable(req, res, next) { } } +async function reportGet(req, res, next) { + try { + const { start, end } = req.query; + if (!start || !end) throw new BadRequestError("No start or end provided"); + + // search database for counts of total interaction and counts of categories + const [total_interactions, type_count, location_count, format_count] = + await Promise.all([ + countInteractionsAdmin(start, end), + queries.countInteractionByCategoryAdmin(start, end, "type"), + queries.countInteractionByCategoryAdmin(start, end, "location"), + queries.countInteractionByCategoryAdmin(start, end, "format"), + ]); + + // todo: add queries for counts grouped by month + + res.json({ + message: "ok", + total_interactions, + type_count, + location_count, + format_count, + }); + } catch (error) { + return next(error); + } +} + module.exports = { rowGetById, tableGet, @@ -131,4 +160,5 @@ module.exports = { statsGet, interactionsGet, countTable, + reportGet, }; diff --git a/api/src/db/queries.js b/api/src/db/queries.js index 6039ede..4d195cc 100644 --- a/api/src/db/queries.js +++ b/api/src/db/queries.js @@ -218,6 +218,20 @@ async function countRowsInTable(table) { } } +async function countInteractionsAdmin(start, end) { + const rows = await db("interactions").count('* as total_interactions').whereRaw("strftime('%Y-%m', date) BETWEEN" + + " :start AND :end", {start, end}).first(); + + return rows.total_interactions; +} + +async function countInteractionByCategoryAdmin(start, end, category) { + const table = `${category}s` + + const rows = await db(table).select(`${table}.id`, `${table}.value`).count(`interactions.${category}_id as number_of_interactions`).leftJoin('interactions', `${table}.id`, `interactions.${category}_id`).whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", {start, end}).groupBy(`${table}.id`) + return rows; +} + module.exports = { selectInteractions, selectAllFromTable, @@ -233,5 +247,6 @@ module.exports = { insertRow, countAllInteractionByGroup, countRowsInTable, - // countAllInteractions, + countInteractionsAdmin, + countInteractionByCategoryAdmin }; diff --git a/api/src/routes/adminRoutes.js b/api/src/routes/adminRoutes.js index 43fff51..00af856 100644 --- a/api/src/routes/adminRoutes.js +++ b/api/src/routes/adminRoutes.js @@ -33,4 +33,6 @@ router.get("/stats", adminController.statsGet); router.get("/interactions", adminController.interactionsGet); router.get("/count/:table", adminController.countTable); +router.get('/report', adminController.reportGet) + module.exports = router; diff --git a/http-requests/admin.http b/http-requests/admin.http index 2b7d1f1..46d045c 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -24,6 +24,10 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aS GET http://localhost:3001/admin/interactions?page=1&limit=20 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew -### GET /count/:table +### GET /admin/count/:table GET http://localhost:3001/admin/count/interactions -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew \ No newline at end of file +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew + +### GET /admin/report +GET http://localhost:3001/admin/report?start=2024-10&end=2024-11 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From 0e4e122e67935f5c906ed0d604f7e67fe595cb08 Mon Sep 17 00:00:00 2001 From: James Spears Date: Tue, 3 Dec 2024 21:56:44 -0500 Subject: [PATCH 02/28] Code comment and clean up Signed-off-by: James Spears --- api/src/controllers/adminController.js | 18 ++++++++++++++---- api/src/routes/adminRoutes.js | 5 ++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 1f13397..2d311e1 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -124,7 +124,8 @@ async function countTable(req, res, next) { } } -async function reportGet(req, res, next) { +// fetch cumulative count of interaction between a given range of months +async function reportTotalGet(req, res, next) { try { const { start, end } = req.query; if (!start || !end) throw new BadRequestError("No start or end provided"); @@ -138,8 +139,6 @@ async function reportGet(req, res, next) { queries.countInteractionByCategoryAdmin(start, end, "format"), ]); - // todo: add queries for counts grouped by month - res.json({ message: "ok", total_interactions, @@ -152,6 +151,16 @@ async function reportGet(req, res, next) { } } +// todo: build controller to query database for counts grouped by month +async function reportMonthlyGet(req, res, next) { + /* + query each database table (types, formats, and locations) and count interactions per row for each month + i.e. count the number of interactions in 2024-10 with the type_id of 1 + */ + + return null; +} + module.exports = { rowGetById, tableGet, @@ -160,5 +169,6 @@ module.exports = { statsGet, interactionsGet, countTable, - reportGet, + reportTotalGet, + reportMonthlyGet, }; diff --git a/api/src/routes/adminRoutes.js b/api/src/routes/adminRoutes.js index 00af856..f0529a1 100644 --- a/api/src/routes/adminRoutes.js +++ b/api/src/routes/adminRoutes.js @@ -29,10 +29,13 @@ router.get("/formats/:id", adminController.updateRowById); router.post("/formats", adminController.addNewRow); router.get("/formats", adminController.tableGet); +// todo: rename to a more semantic route and controller name. (i.e /admin/summary/count?) router.get("/stats", adminController.statsGet); +// todo: rename to a more semantic route and controller name. (i.e /admin/summary/interactions?) router.get("/interactions", adminController.interactionsGet); router.get("/count/:table", adminController.countTable); -router.get('/report', adminController.reportGet) +router.get("/report/total", adminController.reportTotalGet); +router.get("/report/monthly", adminController.reportMonthlyGet); module.exports = router; From 0c658a0090b9fc60566d97ce68fedec6feca7e90 Mon Sep 17 00:00:00 2001 From: jamesspearsv <58987727+jamesspearsv@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:24:02 -0500 Subject: [PATCH 03/28] Improve error details in app.js error middleware Signed-off-by: jamesspearsv <58987727+jamesspearsv@users.noreply.github.com> --- api/src/app.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/app.js b/api/src/app.js index 8b33886..7e53a76 100644 --- a/api/src/app.js +++ b/api/src/app.js @@ -42,7 +42,10 @@ app.use((error, req, res, next) => { console.error(error); res .status(error.statusCode || 500) - .json({ message: error.message || "Server Error", details: error.details }); + .json({ + message: error.message || "Server Error", + details: error.details || "None", + }); }); app.listen(PORT, () => { From ccdfdb110f1327227491de8eef92b4a73afa20ca Mon Sep 17 00:00:00 2001 From: James Spears Date: Wed, 4 Dec 2024 21:01:19 -0500 Subject: [PATCH 04/28] Begin monthly category count query and controller Signed-off-by: James Spears --- api/src/controllers/adminController.js | 10 ++++-- api/src/db/queries.js | 46 +++++++++++++++++++++++--- http-requests/admin.http | 6 +++- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 2d311e1..879de7d 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -108,7 +108,7 @@ async function interactionsGet(req, res, next) { queries.selectInteractions(limit, offset), ]); - res.json({ message: "ok", total_rows, rows }); + return res.json({ message: "ok", total_rows, rows }); } catch (error) { return next(error); } @@ -139,7 +139,7 @@ async function reportTotalGet(req, res, next) { queries.countInteractionByCategoryAdmin(start, end, "format"), ]); - res.json({ + return res.json({ message: "ok", total_interactions, type_count, @@ -158,7 +158,11 @@ async function reportMonthlyGet(req, res, next) { i.e. count the number of interactions in 2024-10 with the type_id of 1 */ - return null; + const { month } = req.query; + + const rows = await queries.countInteractionsByCategoryByMonth(month, "type"); + + return res.json({ message: "ok", rows }); } module.exports = { diff --git a/api/src/db/queries.js b/api/src/db/queries.js index 4d195cc..586d3bd 100644 --- a/api/src/db/queries.js +++ b/api/src/db/queries.js @@ -219,19 +219,54 @@ async function countRowsInTable(table) { } async function countInteractionsAdmin(start, end) { - const rows = await db("interactions").count('* as total_interactions').whereRaw("strftime('%Y-%m', date) BETWEEN" + - " :start AND :end", {start, end}).first(); + const rows = await db("interactions") + .count("* as total_interactions") + .whereRaw("strftime('%Y-%m', date) BETWEEN" + " :start AND :end", { + start, + end, + }) + .first(); return rows.total_interactions; } async function countInteractionByCategoryAdmin(start, end, category) { - const table = `${category}s` + const table = `${category}s`; - const rows = await db(table).select(`${table}.id`, `${table}.value`).count(`interactions.${category}_id as number_of_interactions`).leftJoin('interactions', `${table}.id`, `interactions.${category}_id`).whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", {start, end}).groupBy(`${table}.id`) + const rows = await db(table) + .select(`${table}.id`, `${table}.value`) + .count("interactions.id as number_of_interactions") + .leftJoin("interactions", `${table}.id`, `interactions.${category}_id`) + .whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", { start, end }) + .groupBy(`${table}.id`); return rows; } +async function countInteractionsByCategoryByMonth(month, category) { + try { + const table = `${category}s`; + + // TODO: refactor to use knex.raw + + const rows = await db(table) + .select(`${table}.id`, `${table}.value`) + .count("interactions.id as" + " number_of_interactions") + .leftJoin("interactions", function () { + this.on(`${table}.id`, "=", `interactions.${category}_id`).andOn( + `strftime('%Y-%m, interactions.date)`, + "=", + month, + ); + }) + .groupBy(`${table}.id`); + + return rows; + } catch (error) { + console.error(error); + throw new DatabaseError(error.message); + } +} + module.exports = { selectInteractions, selectAllFromTable, @@ -248,5 +283,6 @@ module.exports = { countAllInteractionByGroup, countRowsInTable, countInteractionsAdmin, - countInteractionByCategoryAdmin + countInteractionByCategoryAdmin, + countInteractionsByCategoryByMonth, }; diff --git a/http-requests/admin.http b/http-requests/admin.http index 46d045c..42e8ff7 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -29,5 +29,9 @@ GET http://localhost:3001/admin/count/interactions Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew ### GET /admin/report -GET http://localhost:3001/admin/report?start=2024-10&end=2024-11 +GET http://localhost:3001/admin/report/total?start=2024-10&end=2024-11 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew + +### Get monthly count of a single category +GET http://localhost:3001/admin/report/monthly?month=2024-12 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From 6d260383df01ac601067bf7c8b017168c4e228a5 Mon Sep 17 00:00:00 2001 From: jamesspearsv <58987727+jamesspearsv@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:57:48 -0500 Subject: [PATCH 05/28] Fix db query to count interactions by month by category Signed-off-by: jamesspearsv <58987727+jamesspearsv@users.noreply.github.com> --- api/src/controllers/adminController.js | 24 +++++++++++++++++++++++- api/src/db/queries.js | 21 ++++++++------------- api/src/routes/adminRoutes.js | 1 + http-requests/admin.http | 6 +++++- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 879de7d..61ec53a 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -124,6 +124,24 @@ async function countTable(req, res, next) { } } +async function adminReportGet(req, res, next) { + /** + * TODO: Refactor /admin/report endpoint + * This endpoint should return a structured object for the given table during the given range in the db + * For example: + * { + * total: [], + * monthly: [month: [],month: [] month: []] + * } + **/ + try { + const { start, end, category } = req.query; + res.json({ message: "wip", query: { start, end, category } }); + } catch (error) { + return next(error); + } +} + // fetch cumulative count of interaction between a given range of months async function reportTotalGet(req, res, next) { try { @@ -160,7 +178,10 @@ async function reportMonthlyGet(req, res, next) { const { month } = req.query; - const rows = await queries.countInteractionsByCategoryByMonth(month, "type"); + const rows = await queries.countInteractionsByCategoryByMonth( + month, + "format", + ); return res.json({ message: "ok", rows }); } @@ -175,4 +196,5 @@ module.exports = { countTable, reportTotalGet, reportMonthlyGet, + adminReportGet, }; diff --git a/api/src/db/queries.js b/api/src/db/queries.js index 586d3bd..749976b 100644 --- a/api/src/db/queries.js +++ b/api/src/db/queries.js @@ -246,21 +246,16 @@ async function countInteractionsByCategoryByMonth(month, category) { try { const table = `${category}s`; - // TODO: refactor to use knex.raw - - const rows = await db(table) + return await db(table) .select(`${table}.id`, `${table}.value`) - .count("interactions.id as" + " number_of_interactions") - .leftJoin("interactions", function () { - this.on(`${table}.id`, "=", `interactions.${category}_id`).andOn( - `strftime('%Y-%m, interactions.date)`, - "=", - month, - ); - }) + .count("interactions.id as number_of_interactions") + .leftJoin( + db.raw( + "interactions on ??=?? AND strftime('%Y-%m', interactions.date)=?", + [`interactions.${category}_id`, `${table}.id`, month], + ), + ) .groupBy(`${table}.id`); - - return rows; } catch (error) { console.error(error); throw new DatabaseError(error.message); diff --git a/api/src/routes/adminRoutes.js b/api/src/routes/adminRoutes.js index f0529a1..5833583 100644 --- a/api/src/routes/adminRoutes.js +++ b/api/src/routes/adminRoutes.js @@ -37,5 +37,6 @@ router.get("/count/:table", adminController.countTable); router.get("/report/total", adminController.reportTotalGet); router.get("/report/monthly", adminController.reportMonthlyGet); +router.get("/report", adminController.adminReportGet); module.exports = router; diff --git a/http-requests/admin.http b/http-requests/admin.http index 42e8ff7..235a0cf 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -33,5 +33,9 @@ GET http://localhost:3001/admin/report/total?start=2024-10&end=2024-11 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew ### Get monthly count of a single category -GET http://localhost:3001/admin/report/monthly?month=2024-12 +GET http://localhost:3001/admin/report/monthly?month=2024-11 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew + +### Get monthly count of a single category +GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=format Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From 756818316a7975f668a35ee1e752b058ec341541 Mon Sep 17 00:00:00 2001 From: James Spears Date: Thu, 5 Dec 2024 19:24:49 -0500 Subject: [PATCH 06/28] Remove testing endpoints Signed-off-by: James Spears --- api/src/controllers/adminController.js | 60 ++++++-------------------- api/src/routes/adminRoutes.js | 2 - http-requests/admin.http | 12 +----- 3 files changed, 16 insertions(+), 58 deletions(-) diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 61ec53a..cc20d9a 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -130,62 +130,32 @@ async function adminReportGet(req, res, next) { * This endpoint should return a structured object for the given table during the given range in the db * For example: * { + * total_interactions: number * total: [], * monthly: [month: [],month: [] month: []] * } **/ try { const { start, end, category } = req.query; - res.json({ message: "wip", query: { start, end, category } }); - } catch (error) { - return next(error); - } -} - -// fetch cumulative count of interaction between a given range of months -async function reportTotalGet(req, res, next) { - try { - const { start, end } = req.query; - if (!start || !end) throw new BadRequestError("No start or end provided"); - - // search database for counts of total interaction and counts of categories - const [total_interactions, type_count, location_count, format_count] = - await Promise.all([ - countInteractionsAdmin(start, end), - queries.countInteractionByCategoryAdmin(start, end, "type"), - queries.countInteractionByCategoryAdmin(start, end, "location"), - queries.countInteractionByCategoryAdmin(start, end, "format"), - ]); - return res.json({ - message: "ok", - total_interactions, - type_count, - location_count, - format_count, - }); + // query database for cumulative interactions counts during range + const total = await queries.countInteractionsAdmin(start, end); + const total_detailed = await queries.countInteractionByCategoryAdmin( + start, + end, + category, + ); + const count_by_month = await queries.countInteractionsByCategoryByMonth( + start, + category, + ); + + res.json({ message: "wip", total, total_detailed, count_by_month }); } catch (error) { return next(error); } } -// todo: build controller to query database for counts grouped by month -async function reportMonthlyGet(req, res, next) { - /* - query each database table (types, formats, and locations) and count interactions per row for each month - i.e. count the number of interactions in 2024-10 with the type_id of 1 - */ - - const { month } = req.query; - - const rows = await queries.countInteractionsByCategoryByMonth( - month, - "format", - ); - - return res.json({ message: "ok", rows }); -} - module.exports = { rowGetById, tableGet, @@ -194,7 +164,5 @@ module.exports = { statsGet, interactionsGet, countTable, - reportTotalGet, - reportMonthlyGet, adminReportGet, }; diff --git a/api/src/routes/adminRoutes.js b/api/src/routes/adminRoutes.js index 5833583..21e4408 100644 --- a/api/src/routes/adminRoutes.js +++ b/api/src/routes/adminRoutes.js @@ -35,8 +35,6 @@ router.get("/stats", adminController.statsGet); router.get("/interactions", adminController.interactionsGet); router.get("/count/:table", adminController.countTable); -router.get("/report/total", adminController.reportTotalGet); -router.get("/report/monthly", adminController.reportMonthlyGet); router.get("/report", adminController.adminReportGet); module.exports = router; diff --git a/http-requests/admin.http b/http-requests/admin.http index 235a0cf..f6004cb 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -28,14 +28,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aS GET http://localhost:3001/admin/count/interactions Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew -### GET /admin/report -GET http://localhost:3001/admin/report/total?start=2024-10&end=2024-11 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew - -### Get monthly count of a single category -GET http://localhost:3001/admin/report/monthly?month=2024-11 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew - -### Get monthly count of a single category -GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=format +### Admin report +GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=type Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From 86970e3bb2acaa90736a5a28bc2dcddb03f01f89 Mon Sep 17 00:00:00 2001 From: James Spears Date: Thu, 5 Dec 2024 19:25:29 -0500 Subject: [PATCH 07/28] Update count interaction by category query in queries.js Signed-off-by: James Spears --- api/src/db/queries.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/api/src/db/queries.js b/api/src/db/queries.js index 749976b..9ecabeb 100644 --- a/api/src/db/queries.js +++ b/api/src/db/queries.js @@ -221,7 +221,7 @@ async function countRowsInTable(table) { async function countInteractionsAdmin(start, end) { const rows = await db("interactions") .count("* as total_interactions") - .whereRaw("strftime('%Y-%m', date) BETWEEN" + " :start AND :end", { + .whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", { start, end, }) @@ -233,12 +233,24 @@ async function countInteractionsAdmin(start, end) { async function countInteractionByCategoryAdmin(start, end, category) { const table = `${category}s`; - const rows = await db(table) + const row = await db(table) .select(`${table}.id`, `${table}.value`) .count("interactions.id as number_of_interactions") .leftJoin("interactions", `${table}.id`, `interactions.${category}_id`) .whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", { start, end }) .groupBy(`${table}.id`); + + const rows = await db(table) + .select(`${table}.id`, `${table}.value`) + .count("interactions.id as number_of_interactions") + .leftJoin( + db.raw( + "interactions ON ??=?? AND strftime('%Y-%m', interactions.date) BETWEEN ? AND ?", + [`${table}.id`, `interactions.${category}_id`, start, end], + ), + ) + .groupBy(`${table}.id`); + return rows; } @@ -251,7 +263,7 @@ async function countInteractionsByCategoryByMonth(month, category) { .count("interactions.id as number_of_interactions") .leftJoin( db.raw( - "interactions on ??=?? AND strftime('%Y-%m', interactions.date)=?", + "interactions ON ??=?? AND strftime('%Y-%m', interactions.date)=?", [`interactions.${category}_id`, `${table}.id`, month], ), ) From 2f930fb1863adaee26f51b4636119495ced934d4 Mon Sep 17 00:00:00 2001 From: James Spears Date: Thu, 5 Dec 2024 20:53:13 -0500 Subject: [PATCH 08/28] Write parseMonthRange lib function Signed-off-by: James Spears --- api/src/lib/parseMonthRange.js | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 api/src/lib/parseMonthRange.js diff --git a/api/src/lib/parseMonthRange.js b/api/src/lib/parseMonthRange.js new file mode 100644 index 0000000..fa39d96 --- /dev/null +++ b/api/src/lib/parseMonthRange.js @@ -0,0 +1,36 @@ +function parseMonthRange(startMonth, endMonth) { + // Parse inputs to extract year and month + const [startYear, startMonthNum] = startMonth.split("-").map(Number); + const [endYear, endMonthNum] = endMonth.split("-").map(Number); + + const dateMonthsArray = []; + let currentYear = startYear; + let currentMonth = startMonthNum; + + // Validate inputs + if ( + startYear > endYear || + (startYear === endYear && startMonthNum > endMonthNum) + ) { + throw new Error("Start month must be before or equal to the end month."); + } + + while ( + currentYear < endYear || + (currentYear === endYear && currentMonth <= endMonthNum) + ) { + const formattedMonth = String(currentMonth).padStart(2, "0"); // Ensure 2-digit month + dateMonthsArray.push(`${currentYear}-${formattedMonth}`); + + // Increment month and adjust year if needed + currentMonth++; + if (currentMonth > 12) { + currentMonth = 1; + currentYear++; + } + } + + return dateMonthsArray; +} + +module.exports = parseMonthRange; From 96e9fef4fd6f19bb61cebf85fea72361485e823f Mon Sep 17 00:00:00 2001 From: James Spears Date: Thu, 5 Dec 2024 21:06:54 -0500 Subject: [PATCH 09/28] Implement monthly count ability in admin report middleware Signed-off-by: James Spears --- api/src/controllers/adminController.js | 29 +++++++++++++++++++++----- http-requests/admin.http | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index cc20d9a..947b6fa 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -2,6 +2,7 @@ const queries = require("../db/queries"); const { BadRequestError } = require("../lib/errorsClasses"); const { countInteractionsAdmin } = require("../db/queries"); +const parseMonthRange = require("../lib/parseMonthRange"); // Get all rows from a given table async function tableGet(req, res, next) { @@ -137,6 +138,7 @@ async function adminReportGet(req, res, next) { **/ try { const { start, end, category } = req.query; + const range = parseMonthRange(start, end); // query database for cumulative interactions counts during range const total = await queries.countInteractionsAdmin(start, end); @@ -145,12 +147,29 @@ async function adminReportGet(req, res, next) { end, category, ); - const count_by_month = await queries.countInteractionsByCategoryByMonth( - start, - category, - ); - res.json({ message: "wip", total, total_detailed, count_by_month }); + const montly_count = []; + for (const month of range) { + const rows = await queries.countInteractionsByCategoryByMonth( + month, + category, + ); + + const monthObject = { + month, + }; + + for (const row of rows) { + monthObject[row.value] = row.number_of_interactions; + } + + console.log(rows); + montly_count.push(monthObject); + } + + // console.log(montly_count[0]["Directional"]); + + res.json({ message: "ok", range, total, total_detailed, montly_count }); } catch (error) { return next(error); } diff --git a/http-requests/admin.http b/http-requests/admin.http index f6004cb..24c3c2c 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -29,5 +29,5 @@ GET http://localhost:3001/admin/count/interactions Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew ### Admin report -GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=type +GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=format Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From 92bc8e280665ebf2945d6d00c0b01b36b498442b Mon Sep 17 00:00:00 2001 From: James Spears Date: Fri, 6 Dec 2024 21:05:50 -0500 Subject: [PATCH 10/28] Split queries module into appQueries and adminQueries Signed-off-by: James Spears --- api/src/controllers/adminController.js | 8 +- api/src/controllers/appController.js | 2 +- api/src/db/queries.js | 295 ------------------------- api/src/db/queries/adminQueries.js | 158 +++++++++++++ api/src/db/queries/appQueries.js | 143 ++++++++++++ 5 files changed, 305 insertions(+), 301 deletions(-) delete mode 100644 api/src/db/queries.js create mode 100644 api/src/db/queries/adminQueries.js create mode 100644 api/src/db/queries/appQueries.js diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 947b6fa..28a120e 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -1,14 +1,14 @@ /* Middleware to handle actions requiring admin authorization */ -const queries = require("../db/queries"); +const queries = require("../db/queries/adminQueries"); +const { selectAllFromTable } = require("../db/queries/appQueries"); const { BadRequestError } = require("../lib/errorsClasses"); -const { countInteractionsAdmin } = require("../db/queries"); const parseMonthRange = require("../lib/parseMonthRange"); // Get all rows from a given table async function tableGet(req, res, next) { try { table = req.table; - const rows = await queries.selectAllFromTable(table); + const rows = await selectAllFromTable(table); return res.json({ message: "ok", rows }); } catch (error) { @@ -167,8 +167,6 @@ async function adminReportGet(req, res, next) { montly_count.push(monthObject); } - // console.log(montly_count[0]["Directional"]); - res.json({ message: "ok", range, total, total_detailed, montly_count }); } catch (error) { return next(error); diff --git a/api/src/controllers/appController.js b/api/src/controllers/appController.js index 99e444e..5e86718 100644 --- a/api/src/controllers/appController.js +++ b/api/src/controllers/appController.js @@ -2,7 +2,7 @@ Middleware to handle actions not requiring admin authorization */ -const queries = require("../db/queries"); +const queries = require("../db/queries/appQueries"); const { BadRequestError } = require("../lib/errorsClasses"); // get all rows from types, locations, and formats tables diff --git a/api/src/db/queries.js b/api/src/db/queries.js deleted file mode 100644 index 9ecabeb..0000000 --- a/api/src/db/queries.js +++ /dev/null @@ -1,295 +0,0 @@ -const db = require("./connection"); -const { getDateToday } = require("../lib/dates"); -const { DatabaseError } = require("../lib/errorsClasses"); - -async function selectInteractions(limit, offset) { - try { - return await db("interactions") - .select( - "interactions.id", - "types.value as type", - "locations.value as location", - "formats.value as format", - "date", - ) - .join("types", "interactions.type_id", "=", "types.id") - .join("locations", "interactions.location_id", "=", "locations.id") - .join("formats", "interactions.format_id", "=", "formats.id") - .limit(limit) - .offset(offset); - } catch (error) { - throw DatabaseError(error.message); - } -} - -// Select all columns from given table -async function selectAllFromTable(table) { - try { - return await db(table).select("*"); - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Insert interaction into interactions table -async function insertInteraction(type, location, format) { - try { - await db("interactions").insert({ - type_id: type, - location_id: location, - format_id: format, - date: getDateToday(), - }); - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Return boolean value if id exists in a given table -async function checkIfExists(table, id) { - const row = await db(table).where("id", id).first(); - return !!row; -} - -// select interaction filtered by a given date range and location id -async function selectInteractionsInRange(start, end, location_id) { - try { - return await db("interactions") - .select( - "interactions.id", - "types.value as type", - "locations.value as location", - "formats.value as format", - "date", - ) - .join("types", "interactions.type_id", "=", "types.id") - .join("locations", "interactions.location_id", "=", "locations.id") - .join("formats", "interactions.format_id", "=", "formats.id") - .whereBetween("date", [start, end]) - .andWhere("interactions.location_id", location_id); - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Count total number of interactions in a given date range at a given location -async function countInteractionsInRange(start, end, location_id) { - try { - const count = await db("interactions") - .select("*") - .count("interactions.id as number_of_interactions") - .join("locations", "interactions.location_id", "=", "locations.id") - .whereBetween("date", [start, end]) - .andWhere("interactions.location_id", location_id) - .first(); - - return count.number_of_interactions; - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Count number of interaction in a given date range grouped by category (i.e. type or format) -async function countInteractionsByCategory(start, end, location_id, category) { - try { - return await db(`${category}s`) - .select(`${category}s.id`, `${category}s.value`) - .count("interactions.id as number_of_interactions") - .leftJoin("interactions", function () { - this.on(`interactions.${category}_id`, "=", `${category}s.id`) - .andOn( - "interactions.location_id", - "=", - db.raw(":location_id", { location_id }), - ) - .andOnBetween("interactions.date", [start, end]); - }) - .groupBy(`${category}s.id`); - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Count number of interaction per day in a given date range and at a given location -async function countInteractionsByDay(start, end, location) { - try { - return await db.raw( - `WITH RECURSIVE date_range AS ( - SELECT DATE(:start) AS date - UNION ALL - SELECT DATE(date, '+1 day') - FROM date_range - WHERE date < DATE(:end) - ) - SELECT dr.date, - COUNT(i.id) AS number_of_interactions - FROM date_range dr - LEFT JOIN interactions i - ON dr.date = i.date - AND i.location_id = :location - GROUP BY dr.date - ORDER BY dr.date ASC; - `, - { start, end, location }, - ); - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Count total number interaction in the current month -async function countInteractionsThisMonth() { - try { - const row = await db("interactions") - .select("*") - .count("interactions.id as number_of_interactions") - .whereRaw("strftime('%Y-%m', date) = strftime('%Y-%m', 'now')") - .first(); - return row.number_of_interactions; - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Select row by id from a given table -async function selectRowFromTable(table, id) { - try { - return await db(table).select("*").where("id", id).first(); - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Update row by id from a given table -async function updateRowFromTable(table, id, values) { - try { - const rows = await db(table).where("id", id).update(values, ["*"]); - return rows[0]; - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Insert new row into specified table -async function insertRow(table, row) { - try { - const rows = await db(table).insert(row, ["*"]); - return rows[0]; - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Count all rows in interactions table -async function countAllInteractions() { - try { - const count = await db("interactions") - .count("interactions.id as total") - .first(); - - return count.total; - } catch (error) { - throw new DatabaseError(error.message); - } -} - -// Count rows in interaction grouped by a given group -async function countAllInteractionByGroup(group) { - try { - return await db(`${group}s`) - .select(`${group}s.id`, `${group}s.value`) - .count("interactions.id as total_interactions") - .leftJoin("interactions", `${group}s.id`, "=", `interactions.${group}_id`) - .groupBy(`${group}s.id`); - } catch (error) { - throw new DatabaseError(error.message); - } -} - -async function countRowsInTable(table) { - try { - const rows = await db.raw(`SELECT COUNT(id) AS total_rows FROM :table:`, { - table, - }); - - return rows[0].total_rows; - } catch (error) { - throw new DatabaseError(error.message); - } -} - -async function countInteractionsAdmin(start, end) { - const rows = await db("interactions") - .count("* as total_interactions") - .whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", { - start, - end, - }) - .first(); - - return rows.total_interactions; -} - -async function countInteractionByCategoryAdmin(start, end, category) { - const table = `${category}s`; - - const row = await db(table) - .select(`${table}.id`, `${table}.value`) - .count("interactions.id as number_of_interactions") - .leftJoin("interactions", `${table}.id`, `interactions.${category}_id`) - .whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", { start, end }) - .groupBy(`${table}.id`); - - const rows = await db(table) - .select(`${table}.id`, `${table}.value`) - .count("interactions.id as number_of_interactions") - .leftJoin( - db.raw( - "interactions ON ??=?? AND strftime('%Y-%m', interactions.date) BETWEEN ? AND ?", - [`${table}.id`, `interactions.${category}_id`, start, end], - ), - ) - .groupBy(`${table}.id`); - - return rows; -} - -async function countInteractionsByCategoryByMonth(month, category) { - try { - const table = `${category}s`; - - return await db(table) - .select(`${table}.id`, `${table}.value`) - .count("interactions.id as number_of_interactions") - .leftJoin( - db.raw( - "interactions ON ??=?? AND strftime('%Y-%m', interactions.date)=?", - [`interactions.${category}_id`, `${table}.id`, month], - ), - ) - .groupBy(`${table}.id`); - } catch (error) { - console.error(error); - throw new DatabaseError(error.message); - } -} - -module.exports = { - selectInteractions, - selectAllFromTable, - insertInteraction, - checkIfExists, - selectInteractionsInRange, - countInteractionsInRange, - countInteractionsByCategory, - countInteractionsByDay, - countInteractionsThisMonth, - selectRowFromTable, - updateRowFromTable, - insertRow, - countAllInteractionByGroup, - countRowsInTable, - countInteractionsAdmin, - countInteractionByCategoryAdmin, - countInteractionsByCategoryByMonth, -}; diff --git a/api/src/db/queries/adminQueries.js b/api/src/db/queries/adminQueries.js new file mode 100644 index 0000000..7c0e2d5 --- /dev/null +++ b/api/src/db/queries/adminQueries.js @@ -0,0 +1,158 @@ +const db = require("../connection"); +const { DatabaseError } = require("../../lib/errorsClasses"); + +// select paginated subsection of interactions table +async function selectInteractions(limit, offset) { + try { + return await db("interactions") + .select( + "interactions.id", + "types.value as type", + "locations.value as location", + "formats.value as format", + "date", + ) + .join("types", "interactions.type_id", "=", "types.id") + .join("locations", "interactions.location_id", "=", "locations.id") + .join("formats", "interactions.format_id", "=", "formats.id") + .limit(limit) + .offset(offset); + } catch (error) { + throw DatabaseError(error.message); + } +} + +// Select row by id from a given table +async function selectRowFromTable(table, id) { + try { + return await db(table).select("*").where("id", id).first(); + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Update row by id from a given table +async function updateRowFromTable(table, id, values) { + try { + const rows = await db(table).where("id", id).update(values, ["*"]); + return rows[0]; + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Insert new row into specified table +async function insertRow(table, row) { + try { + const rows = await db(table).insert(row, ["*"]); + return rows[0]; + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Count all rows in interactions table +async function countAllInteractions() { + try { + const count = await db("interactions") + .count("interactions.id as total") + .first(); + + return count.total; + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Count rows in interaction grouped by a given group +async function countAllInteractionByGroup(group) { + try { + return await db(`${group}s`) + .select(`${group}s.id`, `${group}s.value`) + .count("interactions.id as total_interactions") + .leftJoin("interactions", `${group}s.id`, "=", `interactions.${group}_id`) + .groupBy(`${group}s.id`); + } catch (error) { + throw new DatabaseError(error.message); + } +} + +async function countRowsInTable(table) { + try { + const rows = await db.raw(`SELECT COUNT(id) AS total_rows FROM :table:`, { + table, + }); + + return rows[0].total_rows; + } catch (error) { + throw new DatabaseError(error.message); + } +} + +async function countInteractionsAdmin(start, end) { + const rows = await db("interactions") + .count("* as total_interactions") + .whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", { + start, + end, + }) + .first(); + + return rows.total_interactions; +} + +async function countInteractionByCategoryAdmin(start, end, category) { + const table = `${category}s`; + + const row = await db(table) + .select(`${table}.id`, `${table}.value`) + .count("interactions.id as number_of_interactions") + .leftJoin("interactions", `${table}.id`, `interactions.${category}_id`) + .whereRaw("strftime('%Y-%m', date) BETWEEN :start AND :end", { start, end }) + .groupBy(`${table}.id`); + + const rows = await db(table) + .select(`${table}.id`, `${table}.value`) + .count("interactions.id as number_of_interactions") + .leftJoin( + db.raw( + "interactions ON ??=?? AND strftime('%Y-%m', interactions.date) BETWEEN ? AND ?", + [`${table}.id`, `interactions.${category}_id`, start, end], + ), + ) + .groupBy(`${table}.id`); + + return rows; +} + +async function countInteractionsByCategoryByMonth(month, category) { + try { + const table = `${category}s`; + + return await db(table) + .select(`${table}.id`, `${table}.value`) + .count("interactions.id as number_of_interactions") + .leftJoin( + db.raw( + "interactions ON ??=?? AND strftime('%Y-%m', interactions.date)=?", + [`interactions.${category}_id`, `${table}.id`, month], + ), + ) + .groupBy(`${table}.id`); + } catch (error) { + console.error(error); + throw new DatabaseError(error.message); + } +} + +module.exports = { + selectInteractions, + selectRowFromTable, + updateRowFromTable, + insertRow, + countAllInteractionByGroup, + countRowsInTable, + countInteractionsAdmin, + countInteractionByCategoryAdmin, + countInteractionsByCategoryByMonth, +}; diff --git a/api/src/db/queries/appQueries.js b/api/src/db/queries/appQueries.js new file mode 100644 index 0000000..377a88d --- /dev/null +++ b/api/src/db/queries/appQueries.js @@ -0,0 +1,143 @@ +const db = require("../connection"); +const { getDateToday } = require("../../lib/dates"); +const { DatabaseError } = require("../../lib/errorsClasses"); + +// Select all columns from given table +async function selectAllFromTable(table) { + try { + return await db(table).select("*"); + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Insert interaction into interactions table +async function insertInteraction(type, location, format) { + try { + await db("interactions").insert({ + type_id: type, + location_id: location, + format_id: format, + date: getDateToday(), + }); + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Return boolean value if id exists in a given table +async function checkIfExists(table, id) { + const row = await db(table).where("id", id).first(); + return !!row; +} + +// select interaction filtered by a given date range and location id +async function selectInteractionsInRange(start, end, location_id) { + try { + return await db("interactions") + .select( + "interactions.id", + "types.value as type", + "locations.value as location", + "formats.value as format", + "date", + ) + .join("types", "interactions.type_id", "=", "types.id") + .join("locations", "interactions.location_id", "=", "locations.id") + .join("formats", "interactions.format_id", "=", "formats.id") + .whereBetween("date", [start, end]) + .andWhere("interactions.location_id", location_id); + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Count total number of interactions in a given date range at a given location +async function countInteractionsInRange(start, end, location_id) { + try { + const count = await db("interactions") + .select("*") + .count("interactions.id as number_of_interactions") + .join("locations", "interactions.location_id", "=", "locations.id") + .whereBetween("date", [start, end]) + .andWhere("interactions.location_id", location_id) + .first(); + + return count.number_of_interactions; + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Count number of interaction in a given date range grouped by category (i.e. type or format) +async function countInteractionsByCategory(start, end, location_id, category) { + try { + return await db(`${category}s`) + .select(`${category}s.id`, `${category}s.value`) + .count("interactions.id as number_of_interactions") + .leftJoin("interactions", function () { + this.on(`interactions.${category}_id`, "=", `${category}s.id`) + .andOn( + "interactions.location_id", + "=", + db.raw(":location_id", { location_id }), + ) + .andOnBetween("interactions.date", [start, end]); + }) + .groupBy(`${category}s.id`); + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Count number of interaction per day in a given date range and at a given location +async function countInteractionsByDay(start, end, location) { + try { + return await db.raw( + `WITH RECURSIVE date_range AS ( + SELECT DATE(:start) AS date + UNION ALL + SELECT DATE(date, '+1 day') + FROM date_range + WHERE date < DATE(:end) + ) + SELECT dr.date, + COUNT(i.id) AS number_of_interactions + FROM date_range dr + LEFT JOIN interactions i + ON dr.date = i.date + AND i.location_id = :location + GROUP BY dr.date + ORDER BY dr.date ASC; + `, + { start, end, location }, + ); + } catch (error) { + throw new DatabaseError(error.message); + } +} + +// Count total number interaction in the current month +async function countInteractionsThisMonth() { + try { + const row = await db("interactions") + .select("*") + .count("interactions.id as number_of_interactions") + .whereRaw("strftime('%Y-%m', date) = strftime('%Y-%m', 'now')") + .first(); + return row.number_of_interactions; + } catch (error) { + throw new DatabaseError(error.message); + } +} + +module.exports = { + selectAllFromTable, + insertInteraction, + checkIfExists, + selectInteractionsInRange, + countInteractionsInRange, + countInteractionsByCategory, + countInteractionsByDay, + countInteractionsThisMonth, +}; From 5e4c580c3f4918b1d5f8c7a5998f13a04e801881 Mon Sep 17 00:00:00 2001 From: James Spears Date: Fri, 6 Dec 2024 21:41:52 -0500 Subject: [PATCH 11/28] Improve error handling and other minor code improvements Signed-off-by: James Spears --- api/src/controllers/adminController.js | 38 +++++++++++++++----------- api/src/db/queries/adminQueries.js | 1 - api/src/lib/parseMonthRange.js | 6 +++- http-requests/admin.http | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 28a120e..9d4cd0a 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -126,29 +126,28 @@ async function countTable(req, res, next) { } async function adminReportGet(req, res, next) { - /** - * TODO: Refactor /admin/report endpoint - * This endpoint should return a structured object for the given table during the given range in the db - * For example: - * { - * total_interactions: number - * total: [], - * monthly: [month: [],month: [] month: []] - * } - **/ try { const { start, end, category } = req.query; - const range = parseMonthRange(start, end); + if (!start || !end || !category) { + throw new BadRequestError("Start, end, and category must be provided"); + } + + if (!["type", "location", "format"].includes(category)) { + throw new BadRequestError("Invalid category provided"); + } // query database for cumulative interactions counts during range - const total = await queries.countInteractionsAdmin(start, end); + const total_interactions = await queries.countInteractionsAdmin(start, end); const total_detailed = await queries.countInteractionByCategoryAdmin( start, end, category, ); - const montly_count = []; + // parse range between start and end month + const range = parseMonthRange(start, end); + const monthly_details = []; + // query database for each month in range for (const month of range) { const rows = await queries.countInteractionsByCategoryByMonth( month, @@ -159,15 +158,22 @@ async function adminReportGet(req, res, next) { month, }; + // push data from each row to new formatted object for (const row of rows) { monthObject[row.value] = row.number_of_interactions; } - console.log(rows); - montly_count.push(monthObject); + // push month object to monthly_details array + monthly_details.push(monthObject); } - res.json({ message: "ok", range, total, total_detailed, montly_count }); + res.json({ + message: "ok", + range, + total_interactions, + total_detailed, + monthly_details, + }); } catch (error) { return next(error); } diff --git a/api/src/db/queries/adminQueries.js b/api/src/db/queries/adminQueries.js index 7c0e2d5..c2b4cb5 100644 --- a/api/src/db/queries/adminQueries.js +++ b/api/src/db/queries/adminQueries.js @@ -140,7 +140,6 @@ async function countInteractionsByCategoryByMonth(month, category) { ) .groupBy(`${table}.id`); } catch (error) { - console.error(error); throw new DatabaseError(error.message); } } diff --git a/api/src/lib/parseMonthRange.js b/api/src/lib/parseMonthRange.js index fa39d96..bf918a9 100644 --- a/api/src/lib/parseMonthRange.js +++ b/api/src/lib/parseMonthRange.js @@ -1,3 +1,5 @@ +const { BadRequestError } = require("./errorsClasses"); + function parseMonthRange(startMonth, endMonth) { // Parse inputs to extract year and month const [startYear, startMonthNum] = startMonth.split("-").map(Number); @@ -12,7 +14,9 @@ function parseMonthRange(startMonth, endMonth) { startYear > endYear || (startYear === endYear && startMonthNum > endMonthNum) ) { - throw new Error("Start month must be before or equal to the end month."); + throw new BadRequestError( + "Start month must be before or equal to the end month.", + ); } while ( diff --git a/http-requests/admin.http b/http-requests/admin.http index 24c3c2c..975a30d 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -29,5 +29,5 @@ GET http://localhost:3001/admin/count/interactions Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew ### Admin report -GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=format +GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=locations Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From be5d9a7344282dd52c4cc0f9ce2ea8842e5adfd7 Mon Sep 17 00:00:00 2001 From: James Spears Date: Fri, 6 Dec 2024 21:47:56 -0500 Subject: [PATCH 12/28] Fix admin database modal title bug Signed-off-by: James Spears --- client/src/pages-admin/Database/DatabaseModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages-admin/Database/DatabaseModal.jsx b/client/src/pages-admin/Database/DatabaseModal.jsx index 48bd69f..eb10450 100644 --- a/client/src/pages-admin/Database/DatabaseModal.jsx +++ b/client/src/pages-admin/Database/DatabaseModal.jsx @@ -133,7 +133,7 @@ function DatabaseModal({ table, rowId, modalOpen, setModalOpen, setRefresh }) { style={{ width: "fit-content" }} >
Date: Tue, 10 Dec 2024 13:33:54 -0500 Subject: [PATCH 13/28] Add admin reporting route to admin subapp. Signed-off-by: jamesspearsv <58987727+jamesspearsv@users.noreply.github.com> --- client/src/Admin.jsx | 1 + client/src/pages-admin/Reporting/AdminReporting.jsx | 12 ++++++++++++ client/src/pages-admin/Reporting/ReportingParams.jsx | 5 +++++ client/src/routes.jsx | 2 ++ 4 files changed, 20 insertions(+) create mode 100644 client/src/pages-admin/Reporting/AdminReporting.jsx create mode 100644 client/src/pages-admin/Reporting/ReportingParams.jsx diff --git a/client/src/Admin.jsx b/client/src/Admin.jsx index 3eec2ab..219a6cf 100644 --- a/client/src/Admin.jsx +++ b/client/src/Admin.jsx @@ -60,6 +60,7 @@ function Admin() { { label: "Back to App", route: "/" }, { label: "Dashboard", route: "/admin" }, { label: "Database", route: "/admin/database" }, + { label: "Reporting", route: "/admin/reporting" }, ]} /> {auth && ( diff --git a/client/src/pages-admin/Reporting/AdminReporting.jsx b/client/src/pages-admin/Reporting/AdminReporting.jsx new file mode 100644 index 0000000..e6b3025 --- /dev/null +++ b/client/src/pages-admin/Reporting/AdminReporting.jsx @@ -0,0 +1,12 @@ +import ReportingParams from "./ReportingParams.jsx"; + +function AdminReporting() { + return ( +
+ +

Admin Reporting

+
+ ); +} + +export default AdminReporting; diff --git a/client/src/pages-admin/Reporting/ReportingParams.jsx b/client/src/pages-admin/Reporting/ReportingParams.jsx new file mode 100644 index 0000000..dbe4eeb --- /dev/null +++ b/client/src/pages-admin/Reporting/ReportingParams.jsx @@ -0,0 +1,5 @@ +function ReportingParams() { + return "Component"; +} + +export default ReportingParams; diff --git a/client/src/routes.jsx b/client/src/routes.jsx index 591ac77..8f0a492 100644 --- a/client/src/routes.jsx +++ b/client/src/routes.jsx @@ -7,6 +7,7 @@ import Admin from "./Admin.jsx"; import Login from "./pages-admin/Login.jsx"; import Dashboard from "./pages-admin/Dashboard/Dashboard.jsx"; import Database from "./pages-admin/Database/Database.jsx"; +import AdminReporting from "./pages-admin/Reporting/AdminReporting.jsx"; const routes = [ { @@ -26,6 +27,7 @@ const routes = [ { index: true, element: }, { path: "login", element: }, { path: "database", element: }, + { path: "reporting", element: }, ], }, ]; From 8c9f9e1804646ed757d610cbfa96469ae21e7862 Mon Sep 17 00:00:00 2001 From: jamesspearsv <58987727+jamesspearsv@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:18:43 -0500 Subject: [PATCH 14/28] Build basic reporting params form Signed-off-by: jamesspearsv <58987727+jamesspearsv@users.noreply.github.com> --- client/src/components/SelectInput.jsx | 7 +- .../pages-admin/Reporting/AdminReporting.jsx | 12 ---- .../src/pages-admin/Reporting/Reporting.jsx | 23 +++++++ .../pages-admin/Reporting/ReportingParams.jsx | 66 ++++++++++++++++++- client/src/routes.jsx | 4 +- http-requests/admin.http | 2 +- 6 files changed, 97 insertions(+), 17 deletions(-) delete mode 100644 client/src/pages-admin/Reporting/AdminReporting.jsx create mode 100644 client/src/pages-admin/Reporting/Reporting.jsx diff --git a/client/src/components/SelectInput.jsx b/client/src/components/SelectInput.jsx index 10e5db8..f69b720 100644 --- a/client/src/components/SelectInput.jsx +++ b/client/src/components/SelectInput.jsx @@ -30,7 +30,12 @@ function SelectInput({ label, options, handleChange, value }) { SelectInput.propTypes = { label: PropTypes.string.isRequired, - options: PropTypes.array.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }), + ).isRequired, handleChange: PropTypes.func.isRequired, value: PropTypes.string.isRequired, }; diff --git a/client/src/pages-admin/Reporting/AdminReporting.jsx b/client/src/pages-admin/Reporting/AdminReporting.jsx deleted file mode 100644 index e6b3025..0000000 --- a/client/src/pages-admin/Reporting/AdminReporting.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import ReportingParams from "./ReportingParams.jsx"; - -function AdminReporting() { - return ( -
- -

Admin Reporting

-
- ); -} - -export default AdminReporting; diff --git a/client/src/pages-admin/Reporting/Reporting.jsx b/client/src/pages-admin/Reporting/Reporting.jsx new file mode 100644 index 0000000..2e98e0f --- /dev/null +++ b/client/src/pages-admin/Reporting/Reporting.jsx @@ -0,0 +1,23 @@ +import ReportingParams from "./ReportingParams.jsx"; +import { Navigate, useOutletContext } from "react-router-dom"; + +function Reporting() { + const { auth } = useOutletContext(); + + if (!auth) return ; + + return ( +
+ +
+ ); +} + +export default Reporting; diff --git a/client/src/pages-admin/Reporting/ReportingParams.jsx b/client/src/pages-admin/Reporting/ReportingParams.jsx index dbe4eeb..52a41fd 100644 --- a/client/src/pages-admin/Reporting/ReportingParams.jsx +++ b/client/src/pages-admin/Reporting/ReportingParams.jsx @@ -1,5 +1,69 @@ +import PropTypes from "prop-types"; +import CardWrapper from "../../components/CardWrapper.jsx"; +import Form from "../../components/Form.jsx"; +import SelectInput from "../../components/SelectInput.jsx"; +import { useState } from "react"; + +const categories = [ + { id: "type", value: "Types" }, + { id: "format", value: "Formats" }, + { id: "location", value: "Locations" }, +]; + function ReportingParams() { - return "Component"; + // todo : raise state to parent component + const [params, setParams] = useState({ + startMonth: null, + endMonth: null, + category: "", + }); + + function handleCagetoryChange(e) { + setParams((params) => ({ ...params, category: e.target.value })); + } + + // todo: finish updater function + function handleMonthChange(e) { + return; + } + + return ( +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ ); } export default ReportingParams; diff --git a/client/src/routes.jsx b/client/src/routes.jsx index 8f0a492..539105f 100644 --- a/client/src/routes.jsx +++ b/client/src/routes.jsx @@ -7,7 +7,7 @@ import Admin from "./Admin.jsx"; import Login from "./pages-admin/Login.jsx"; import Dashboard from "./pages-admin/Dashboard/Dashboard.jsx"; import Database from "./pages-admin/Database/Database.jsx"; -import AdminReporting from "./pages-admin/Reporting/AdminReporting.jsx"; +import Reporting from "./pages-admin/Reporting/Reporting.jsx"; const routes = [ { @@ -27,7 +27,7 @@ const routes = [ { index: true, element: }, { path: "login", element: }, { path: "database", element: }, - { path: "reporting", element: }, + { path: "reporting", element: }, ], }, ]; diff --git a/http-requests/admin.http b/http-requests/admin.http index 975a30d..7718cfe 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -29,5 +29,5 @@ GET http://localhost:3001/admin/count/interactions Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew ### Admin report -GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=locations +GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=location Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From bfbbe809c2ceae732c6485b2c0d2dd635aaf00d3 Mon Sep 17 00:00:00 2001 From: James Spears Date: Wed, 11 Dec 2024 07:48:03 -0500 Subject: [PATCH 15/28] Complete basic admin reporting state management Signed-off-by: James Spears --- api/src/controllers/adminController.js | 15 ++++--- .../pages-admin/Dashboard/DashboardTable.jsx | 1 - .../src/pages-admin/Reporting/Reporting.jsx | 25 +++++++++++- .../pages-admin/Reporting/ReportingParams.jsx | 39 ++++++++++--------- http-requests/admin.http | 2 +- 5 files changed, 55 insertions(+), 27 deletions(-) diff --git a/api/src/controllers/adminController.js b/api/src/controllers/adminController.js index 9d4cd0a..050a0bd 100644 --- a/api/src/controllers/adminController.js +++ b/api/src/controllers/adminController.js @@ -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"); } @@ -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) { diff --git a/client/src/pages-admin/Dashboard/DashboardTable.jsx b/client/src/pages-admin/Dashboard/DashboardTable.jsx index 718d1d1..c73639c 100644 --- a/client/src/pages-admin/Dashboard/DashboardTable.jsx +++ b/client/src/pages-admin/Dashboard/DashboardTable.jsx @@ -35,7 +35,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); diff --git a/client/src/pages-admin/Reporting/Reporting.jsx b/client/src/pages-admin/Reporting/Reporting.jsx index 2e98e0f..49f6a4a 100644 --- a/client/src/pages-admin/Reporting/Reporting.jsx +++ b/client/src/pages-admin/Reporting/Reporting.jsx @@ -1,8 +1,26 @@ import ReportingParams from "./ReportingParams.jsx"; import { Navigate, useOutletContext } from "react-router-dom"; +import { useState } from "react"; + +const defaultParams = { + startMonth: "", + endMonth: "", + category: "", +}; function Reporting() { const { auth } = useOutletContext(); + const [params, setParams] = useState(defaultParams); + + function updateParams(param, value) { + setParams((prevParams) => { + return { ...prevParams, [param]: value }; + }); + } + + function resetParams() { + setParams(defaultParams); + } if (!auth) return ; @@ -15,7 +33,12 @@ function Reporting() { alignItems: "center", }} > - + +
+

startMonth: {params.startMonth}

+

endMonth: {params.endMonth}

+

category: {params.category}

+
); } diff --git a/client/src/pages-admin/Reporting/ReportingParams.jsx b/client/src/pages-admin/Reporting/ReportingParams.jsx index 52a41fd..5bd46a8 100644 --- a/client/src/pages-admin/Reporting/ReportingParams.jsx +++ b/client/src/pages-admin/Reporting/ReportingParams.jsx @@ -1,8 +1,6 @@ import PropTypes from "prop-types"; -import CardWrapper from "../../components/CardWrapper.jsx"; import Form from "../../components/Form.jsx"; import SelectInput from "../../components/SelectInput.jsx"; -import { useState } from "react"; const categories = [ { id: "type", value: "Types" }, @@ -10,21 +8,15 @@ const categories = [ { id: "location", value: "Locations" }, ]; -function ReportingParams() { - // todo : raise state to parent component - const [params, setParams] = useState({ - startMonth: null, - endMonth: null, - category: "", - }); +function ReportingParams({ params, updateParams }) { + function handleFormChange(e) { + console.log("id", e.target.id); + console.log("value", e.target.value); - function handleCagetoryChange(e) { - setParams((params) => ({ ...params, category: e.target.value })); - } + const param = e.target.id; + const value = e.target.value; - // todo: finish updater function - function handleMonthChange(e) { - return; + updateParams(param, value); } return ( @@ -36,10 +28,11 @@ function ReportingParams() { Starting Month
@@ -47,17 +40,18 @@ function ReportingParams() { Ending Month
@@ -66,4 +60,13 @@ function ReportingParams() { ); } +ReportingParams.propTypes = { + params: PropTypes.shape({ + startMonth: PropTypes.string.isRequired, + endMonth: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, + }), + updateParams: PropTypes.func.isRequired, +}; + export default ReportingParams; diff --git a/http-requests/admin.http b/http-requests/admin.http index 7718cfe..3d8f232 100644 --- a/http-requests/admin.http +++ b/http-requests/admin.http @@ -29,5 +29,5 @@ GET http://localhost:3001/admin/count/interactions Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew ### Admin report -GET http://localhost:3001/admin/report?start=2024-11&end=2024-12&category=location +GET http://localhost:3001/admin/report?startMonth=2024-11&endMonth=2024-12&category=location Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.Mzc3MWRlYTkwYTc4NjllNzdjMTc.z8ZJjS4aSbhn6NnoW65WIWZWRzxxVUsIUEzwInvHnew From a891fed111b420cb347934b4dc9842b0a89efa80 Mon Sep 17 00:00:00 2001 From: James Spears Date: Wed, 11 Dec 2024 09:18:47 -0500 Subject: [PATCH 16/28] Set up ReportingDisplay.jsx and add data fetching effect Signed-off-by: James Spears --- .../src/pages-admin/Reporting/Reporting.jsx | 2 + .../Reporting/ReportingDisplay.jsx | 79 +++++++++++++++++++ .../pages-admin/Reporting/ReportingParams.jsx | 6 +- 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 client/src/pages-admin/Reporting/ReportingDisplay.jsx diff --git a/client/src/pages-admin/Reporting/Reporting.jsx b/client/src/pages-admin/Reporting/Reporting.jsx index 49f6a4a..b098f9a 100644 --- a/client/src/pages-admin/Reporting/Reporting.jsx +++ b/client/src/pages-admin/Reporting/Reporting.jsx @@ -1,6 +1,7 @@ import ReportingParams from "./ReportingParams.jsx"; import { Navigate, useOutletContext } from "react-router-dom"; import { useState } from "react"; +import ReportingDisplay from "./ReportingDisplay.jsx"; const defaultParams = { startMonth: "", @@ -39,6 +40,7 @@ function Reporting() {

endMonth: {params.endMonth}

category: {params.category}

+ ); } diff --git a/client/src/pages-admin/Reporting/ReportingDisplay.jsx b/client/src/pages-admin/Reporting/ReportingDisplay.jsx new file mode 100644 index 0000000..d3d3eb0 --- /dev/null +++ b/client/src/pages-admin/Reporting/ReportingDisplay.jsx @@ -0,0 +1,79 @@ +import PropTypes from "prop-types"; +import { useState, useEffect } from "react"; +import { useOutletContext } from "react-router-dom"; +import { validateAdminResponse } from "../../lib/response.js"; +import { toast } from "react-hot-toast"; + +function ReportingDisplay({ params }) { + const { auth, apihost, setAuth } = useOutletContext(); + const [report, setReport] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const regex = /[0-9]{4}-[0-9]{2}/; + + useEffect(() => { + // check that params form is complete + const { startMonth, endMonth, category } = params; + + // todo : write matching function to validate month inputs in unsupported browsers + const match = startMonth.match(regex); + if (!match || match[0] !== startMonth) console.error("no match"); + console.log(match); + + if (!startMonth || !endMonth || !category) return; + + // perform async fetch operation + (async () => { + try { + // define fetch url and options + const url = `${apihost}/admin/report?startMonth=${startMonth}&endMonth=${endMonth}&category=${category}`; + const options = { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `${auth.token_type} ${auth.token}`, + }, + }; + + const res = await fetch(url, options); + const json = await res.json(); + + // validate API response + validateAdminResponse(res, json, setAuth); + + setReport(json); + setLoading(false); + setError(null); + } catch (error) { + console.error(error); + toast.error(error.message); + setLoading(false); + setError({ message: error.message }); + } + + // clean up state + return () => { + setReport(null); + setLoading(true); + setError(null); + }; + })(); + }, [params]); + + // if params are incomplete or error has occurred + if (loading) return
Set your report parameters above
; + if (error) return
Error loading report -- {error.message}
; + + return "Reporting!"; +} + +ReportingDisplay.propTypes = { + params: PropTypes.shape({ + startMonth: PropTypes.string.isRequired, + endMonth: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, + }), +}; + +export default ReportingDisplay; diff --git a/client/src/pages-admin/Reporting/ReportingParams.jsx b/client/src/pages-admin/Reporting/ReportingParams.jsx index 5bd46a8..e13641e 100644 --- a/client/src/pages-admin/Reporting/ReportingParams.jsx +++ b/client/src/pages-admin/Reporting/ReportingParams.jsx @@ -10,12 +10,8 @@ const categories = [ function ReportingParams({ params, updateParams }) { function handleFormChange(e) { - console.log("id", e.target.id); - console.log("value", e.target.value); - const param = e.target.id; const value = e.target.value; - updateParams(param, value); } @@ -33,6 +29,7 @@ function ReportingParams({ params, updateParams }) { name="startMonth" value={params.startMonth} onChange={handleFormChange} + placeholder={"YYYY-MM"} />
@@ -44,6 +41,7 @@ function ReportingParams({ params, updateParams }) { type="month" value={params.endMonth} onChange={handleFormChange} + placeholder={"YYYY-MM"} />
From 415011d9be0a51111096112423867d6f8796e85d Mon Sep 17 00:00:00 2001 From: James Spears Date: Wed, 11 Dec 2024 21:46:19 -0500 Subject: [PATCH 17/28] Write parseMonthInput utility function Signed-off-by: James Spears --- client/src/lib/parseMonthInput.js | 18 +++++++++++++ .../Reporting/ReportingDisplay.jsx | 27 ++++++++++--------- 2 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 client/src/lib/parseMonthInput.js diff --git a/client/src/lib/parseMonthInput.js b/client/src/lib/parseMonthInput.js new file mode 100644 index 0000000..5e2f783 --- /dev/null +++ b/client/src/lib/parseMonthInput.js @@ -0,0 +1,18 @@ +/** + * Validates dates strings for YYYY-MM format + * @param {string[]} inputs + * @returns {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; +} diff --git a/client/src/pages-admin/Reporting/ReportingDisplay.jsx b/client/src/pages-admin/Reporting/ReportingDisplay.jsx index d3d3eb0..2faf3b1 100644 --- a/client/src/pages-admin/Reporting/ReportingDisplay.jsx +++ b/client/src/pages-admin/Reporting/ReportingDisplay.jsx @@ -1,8 +1,14 @@ +/** + * + * + */ + import PropTypes from "prop-types"; import { useState, useEffect } from "react"; import { useOutletContext } from "react-router-dom"; import { validateAdminResponse } from "../../lib/response.js"; import { toast } from "react-hot-toast"; +import parseMonthInput from "../../lib/parseMonthInput.js"; function ReportingDisplay({ params }) { const { auth, apihost, setAuth } = useOutletContext(); @@ -10,22 +16,19 @@ function ReportingDisplay({ params }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const regex = /[0-9]{4}-[0-9]{2}/; - useEffect(() => { - // check that params form is complete - const { startMonth, endMonth, category } = params; - - // todo : write matching function to validate month inputs in unsupported browsers - const match = startMonth.match(regex); - if (!match || match[0] !== startMonth) console.error("no match"); - console.log(match); - - if (!startMonth || !endMonth || !category) return; - // perform async fetch operation (async () => { try { + // check that params form is complete + const { startMonth, endMonth, category } = params; + if (!startMonth || !endMonth || !category) return; + + // todo : write matching function to validate month inputs in unsupported browsers + const results = parseMonthInput([startMonth, endMonth]); + if (results.includes(false)) + throw new Error("Months must be in YYYY-MM format"); + // define fetch url and options const url = `${apihost}/admin/report?startMonth=${startMonth}&endMonth=${endMonth}&category=${category}`; const options = { From c99df0e2b40c2376ea1aa1c0f120fb80c7691874 Mon Sep 17 00:00:00 2001 From: James Spears Date: Wed, 11 Dec 2024 22:54:40 -0500 Subject: [PATCH 18/28] Improve client documentation using jsdoc Signed-off-by: James Spears --- client/src/components/Button.jsx | 10 ++++++++-- client/src/components/CardWrapper.jsx | 7 ++++++- client/src/components/CountReport.jsx | 7 +++++++ client/src/components/DateInput.jsx | 2 ++ client/src/lib/response.js | 8 ++++++-- client/src/pages-admin/Dashboard/Dashboard.jsx | 5 +++++ .../src/pages-admin/Dashboard/DashboardStats.jsx | 5 +++++ .../src/pages-admin/Dashboard/DashboardTable.jsx | 5 +++++ client/src/pages-admin/Database/Database.jsx | 9 +++++---- client/src/pages-admin/Database/DatabaseModal.jsx | 10 ++++++++++ client/src/pages-admin/Database/DatabaseTable.jsx | 14 +++++++++----- client/src/pages-admin/Reporting/Reporting.jsx | 14 +++++++++----- .../src/pages-admin/Reporting/ReportingDisplay.jsx | 11 ++++++----- .../src/pages-admin/Reporting/ReportingParams.jsx | 7 +++++++ client/src/pages/Home.jsx | 5 +++++ client/src/pages/Record.jsx | 5 +++++ client/src/pages/Report.jsx | 5 +++++ 17 files changed, 105 insertions(+), 24 deletions(-) diff --git a/client/src/components/Button.jsx b/client/src/components/Button.jsx index 49d9adb..66c1b48 100644 --- a/client/src/components/Button.jsx +++ b/client/src/components/Button.jsx @@ -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 {() => 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 }) { diff --git a/client/src/components/CardWrapper.jsx b/client/src/components/CardWrapper.jsx index 75e19ac..41fc56e 100644 --- a/client/src/components/CardWrapper.jsx +++ b/client/src/components/CardWrapper.jsx @@ -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 {JSX.ElementS} children + * @param {Objest} style - optional React inline styles + * @returns {JSX.Element} + */ function CardWrapper({ children, style }) { return ( diff --git a/client/src/components/CountReport.jsx b/client/src/components/CountReport.jsx index 714669a..597d067 100644 --- a/client/src/components/CountReport.jsx +++ b/client/src/components/CountReport.jsx @@ -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 (
diff --git a/client/src/components/DateInput.jsx b/client/src/components/DateInput.jsx index d7943aa..a54175d 100644 --- a/client/src/components/DateInput.jsx +++ b/client/src/components/DateInput.jsx @@ -3,6 +3,8 @@ import styles from "./DateInput.module.css"; /* Custom date input with label and callback */ +// todo: begin here with jsdoc task + function DateInput({ label, value, handleChange }) { const id = label.toLowerCase(); diff --git a/client/src/lib/response.js b/client/src/lib/response.js index f60137c..504c8fd 100644 --- a/client/src/lib/response.js +++ b/client/src/lib/response.js @@ -1,5 +1,9 @@ -/* -Helper functions to validate API responses +/** + * Helper function to validate API responses + * @param {res} res + * @param {json} json + * @param {function} setAuth + * @returns {void} */ export function validateAdminResponse(res, json, setAuth) { diff --git a/client/src/pages-admin/Dashboard/Dashboard.jsx b/client/src/pages-admin/Dashboard/Dashboard.jsx index 09ba95d..b7bbd67 100644 --- a/client/src/pages-admin/Dashboard/Dashboard.jsx +++ b/client/src/pages-admin/Dashboard/Dashboard.jsx @@ -2,6 +2,11 @@ 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(); diff --git a/client/src/pages-admin/Dashboard/DashboardStats.jsx b/client/src/pages-admin/Dashboard/DashboardStats.jsx index d769411..49eef8d 100644 --- a/client/src/pages-admin/Dashboard/DashboardStats.jsx +++ b/client/src/pages-admin/Dashboard/DashboardStats.jsx @@ -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(); diff --git a/client/src/pages-admin/Dashboard/DashboardTable.jsx b/client/src/pages-admin/Dashboard/DashboardTable.jsx index c73639c..5a3f32d 100644 --- a/client/src/pages-admin/Dashboard/DashboardTable.jsx +++ b/client/src/pages-admin/Dashboard/DashboardTable.jsx @@ -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([]); diff --git a/client/src/pages-admin/Database/Database.jsx b/client/src/pages-admin/Database/Database.jsx index c97f03a..2da3abb 100644 --- a/client/src/pages-admin/Database/Database.jsx +++ b/client/src/pages-admin/Database/Database.jsx @@ -1,7 +1,3 @@ -/* -Database page in app admin area. Contains DatabaseTable and DatabaseModal components to help organize code. - */ - import { Navigate, useOutletContext } from "react-router-dom"; import { useState } from "react"; import TabSelector from "../../components/TabSelector.jsx"; @@ -9,6 +5,11 @@ import DatabaseTable from "./DatabaseTable.jsx"; import DatabaseModal from "./DatabaseModal.jsx"; import styles from "./Database.module.css"; +/** + * Database page in admin app + * @returns {JSX.Element} + */ + function Database() { const { auth } = useOutletContext(); const [activeTab, setActiveTab] = useState(""); diff --git a/client/src/pages-admin/Database/DatabaseModal.jsx b/client/src/pages-admin/Database/DatabaseModal.jsx index eb10450..fd89bea 100644 --- a/client/src/pages-admin/Database/DatabaseModal.jsx +++ b/client/src/pages-admin/Database/DatabaseModal.jsx @@ -13,6 +13,16 @@ import Form from "../../components/Form.jsx"; import Button from "../../components/Button.jsx"; import { validateAdminResponse } from "../../lib/response.js"; +/** + * Modal component used to add and edit rows in database tables + * @param {string} table + * @param {string} rowId + * @param {boolean} modalOpen + * @param {() => void} setModalOpen + * @param {() => void} setRefresh + * @returns {JSX.Element} + */ + function DatabaseModal({ table, rowId, modalOpen, setModalOpen, setRefresh }) { /* Component State */ const { apihost, auth, setAuth } = useOutletContext(); diff --git a/client/src/pages-admin/Database/DatabaseTable.jsx b/client/src/pages-admin/Database/DatabaseTable.jsx index 1ce2dff..f4a7a45 100644 --- a/client/src/pages-admin/Database/DatabaseTable.jsx +++ b/client/src/pages-admin/Database/DatabaseTable.jsx @@ -1,8 +1,3 @@ -/* -Table wrapper component for Database.jsx. -Responsible for fetching and rendering table rows using the app's table component. - */ - import { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { useOutletContext } from "react-router-dom"; @@ -12,6 +7,15 @@ import Table from "../../components/Table.jsx"; import Button from "../../components/Button.jsx"; import { validateAdminResponse } from "../../lib/response.js"; +/** + * Component used to fetch and render database table rows + * @param {string} table + * @param {(string) => void} setRowId + * @param {(boolean) => void} setModalOpen + * @param {number} refresh + * @returns {JSX.Element} + */ + function DatabaseTable({ table, setRowId, setModalOpen, refresh }) { const { apihost, auth, setAuth } = useOutletContext(); const [rows, setRows] = useState([]); diff --git a/client/src/pages-admin/Reporting/Reporting.jsx b/client/src/pages-admin/Reporting/Reporting.jsx index b098f9a..235e06d 100644 --- a/client/src/pages-admin/Reporting/Reporting.jsx +++ b/client/src/pages-admin/Reporting/Reporting.jsx @@ -3,13 +3,17 @@ import { Navigate, useOutletContext } from "react-router-dom"; import { useState } from "react"; import ReportingDisplay from "./ReportingDisplay.jsx"; -const defaultParams = { - startMonth: "", - endMonth: "", - category: "", -}; +/** + * Admin reporting page in admin app + * @returns {JSX.Element} + */ function Reporting() { + const defaultParams = { + startMonth: "", + endMonth: "", + category: "", + }; const { auth } = useOutletContext(); const [params, setParams] = useState(defaultParams); diff --git a/client/src/pages-admin/Reporting/ReportingDisplay.jsx b/client/src/pages-admin/Reporting/ReportingDisplay.jsx index 2faf3b1..4a12ce0 100644 --- a/client/src/pages-admin/Reporting/ReportingDisplay.jsx +++ b/client/src/pages-admin/Reporting/ReportingDisplay.jsx @@ -1,8 +1,3 @@ -/** - * - * - */ - import PropTypes from "prop-types"; import { useState, useEffect } from "react"; import { useOutletContext } from "react-router-dom"; @@ -10,6 +5,12 @@ import { validateAdminResponse } from "../../lib/response.js"; import { toast } from "react-hot-toast"; import parseMonthInput from "../../lib/parseMonthInput.js"; +/** + * A React component to fetch and render admin report data + * @param {{startMonth: string, endMonth: string, categoy: string}} params Parameters for admin report + * @returns {JSX.Element} + */ + function ReportingDisplay({ params }) { const { auth, apihost, setAuth } = useOutletContext(); const [report, setReport] = useState(null); diff --git a/client/src/pages-admin/Reporting/ReportingParams.jsx b/client/src/pages-admin/Reporting/ReportingParams.jsx index e13641e..5761841 100644 --- a/client/src/pages-admin/Reporting/ReportingParams.jsx +++ b/client/src/pages-admin/Reporting/ReportingParams.jsx @@ -2,6 +2,13 @@ import PropTypes from "prop-types"; import Form from "../../components/Form.jsx"; import SelectInput from "../../components/SelectInput.jsx"; +/** + * Component to capture admin report params user input + * @param {{startMonth: string, endMonth: string, category: string}} params Admin report params + * @param {function} updateParams Callback function to update report params + * @return {JSX.Element} + */ + const categories = [ { id: "type", value: "Types" }, { id: "format", value: "Formats" }, diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx index 7f38f59..84a4d1f 100644 --- a/client/src/pages/Home.jsx +++ b/client/src/pages/Home.jsx @@ -7,6 +7,11 @@ import ErrorComponent from "../components/ErrorComponent.jsx"; import styles from "./Home.module.css"; +/** + * App homepage component + * @returns {JSX.Element} + */ + function Home() { const { apihost, options } = useOutletContext(); const [effectTrigger, setEffectTrigger] = useState(true); diff --git a/client/src/pages/Record.jsx b/client/src/pages/Record.jsx index cb5d4c2..f7a0c0a 100644 --- a/client/src/pages/Record.jsx +++ b/client/src/pages/Record.jsx @@ -15,6 +15,11 @@ const defaultFormState = { format: "", }; +/** + * App recording page component + * @returns {JSX.Element} + */ + function Record() { // Component state const { apihost, options } = useOutletContext(); diff --git a/client/src/pages/Report.jsx b/client/src/pages/Report.jsx index a54c86f..37e0567 100644 --- a/client/src/pages/Report.jsx +++ b/client/src/pages/Report.jsx @@ -26,6 +26,11 @@ import CountReport from "../components/CountReport"; import CardWrapper from "../components/CardWrapper"; import TabSelector from "../components/TabSelector.jsx"; +/** + * App report page component + * @returns {JSX.Element} + */ + function Report() { const defaultFormState = { start: "", From 8e05170df4c3901bef6d2de25c7ab02848e5e656 Mon Sep 17 00:00:00 2001 From: James Spears Date: Fri, 13 Dec 2024 10:58:23 -0500 Subject: [PATCH 19/28] Improve SelectInput and DateInput prop handling and typing Signed-off-by: James Spears --- client/src/components/Button.jsx | 2 +- client/src/components/DateInput.jsx | 22 ++++++++++---- client/src/components/SelectInput.jsx | 7 ++--- .../Reporting/ReportingDisplay.jsx | 3 +- .../pages-admin/Reporting/ReportingParams.jsx | 29 ++++++++----------- client/src/pages/Record.jsx | 3 ++ client/src/pages/Report.jsx | 9 ++++-- 7 files changed, 44 insertions(+), 31 deletions(-) diff --git a/client/src/components/Button.jsx b/client/src/components/Button.jsx index 66c1b48..e9857ef 100644 --- a/client/src/components/Button.jsx +++ b/client/src/components/Button.jsx @@ -5,7 +5,7 @@ import styles from "./Button.module.css"; * Custom button component * @param {string} id - prop to set `data-id` property of button * @param {string} text - button text property - * @param {() => void} action - button click callback function + * @param {(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 diff --git a/client/src/components/DateInput.jsx b/client/src/components/DateInput.jsx index a54175d..75e64dd 100644 --- a/client/src/components/DateInput.jsx +++ b/client/src/components/DateInput.jsx @@ -3,16 +3,24 @@ import styles from "./DateInput.module.css"; /* Custom date input with label and callback */ -// todo: begin here with jsdoc task - -function DateInput({ label, value, handleChange }) { - const id = label.toLowerCase(); +/** + * Custom date input with label and callback + * @param id {string} + * @param label {string} + * @param type {'date' | 'month'} + * @param value {string} + * @param handleChange {(event) => void} + * @returns {JSX.Element} + */ +function DateInput({ id, label, type, value, handleChange }) { return (
- +