From b0b1bbe4b88b4855083368c40f5d2946c7be0f4e Mon Sep 17 00:00:00 2001 From: Calvin Lu Date: Tue, 14 May 2024 11:31:22 -0700 Subject: [PATCH] Add hours history logging --- src/api/routes/users.ts | 12 ++- src/app.ts | 26 +++++- src/models/User.model.ts | 9 +- src/spreadsheet/insert.ts | 4 + src/types/environment.d.ts | 7 +- .../spreadsheet-hours/add-single-session.ts | 91 ++++++++++++++----- .../helpers/date-formatter.ts | 5 + .../helpers/get-date-column.ts | 20 ++++ .../spreadsheet-hours/helpers/get-user-row.ts | 35 +++++-- template.env | 7 +- 10 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 src/utils/spreadsheet-hours/helpers/date-formatter.ts create mode 100644 src/utils/spreadsheet-hours/helpers/get-date-column.ts diff --git a/src/api/routes/users.ts b/src/api/routes/users.ts index e8443b2..1354b2c 100644 --- a/src/api/routes/users.ts +++ b/src/api/routes/users.ts @@ -1,6 +1,6 @@ import express from "express"; -import User, { getUserByPassword, updateUser } from "../../models/User.model"; +import User, { addToUserTotalTime, getUserByPassword, updateUser } from "../../models/User.model"; import { createSession, deleteSessionBySessionId, @@ -70,7 +70,10 @@ router.post("/sign-out", async (_req, res) => { } await updateUser(user.user_id, { signed_in: false }); - await createSession(user.user_id, user.last_signed_in, Date.now(), false); + const session = await createSession(user.user_id, user.last_signed_in, Date.now(), false); + + await addToUserTotalTime(user.user_id, session.end_time - session.start_time); + await addSingleSessionToSpreadsheet(user, session); return res.status(200).json({ description: `Signed out as ${user.first_name} ${user.last_name}!`, @@ -117,6 +120,7 @@ router.post("/session", async (req, res) => { const session = await createSession(user.user_id, req.body.start_time, req.body.end_time, true); + await addToUserTotalTime(user.user_id, session.end_time - session.start_time); await addSingleSessionToSpreadsheet(user, session); return res.status(200).json({ @@ -169,6 +173,8 @@ router.patch("/session", async (req, res) => { end_time: editedSessionTime.endTime, }); + // TODO: Add update to spreadsheet and total time + return res.status(200).json({ description: "Edited session!", }); @@ -202,6 +208,8 @@ router.delete("/session", async (req, res) => { }); } + // TODO: Update spreadsheet and total time + return res.status(200).json({ description: "Deleted session!", }); diff --git a/src/app.ts b/src/app.ts index 622d9ba..a72ead7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,7 +7,11 @@ require("express-async-errors"); // Adds error handling for async functions, unn import logger from "./utils/logger"; import api from "./api"; import middleware from "./utils/middleware"; -import { insertColumn, insertRow } from "./spreadsheet"; +import { addSingleSessionToSpreadsheet } from "./utils/spreadsheet-hours"; +import User from "./models/User.model"; +import Session from "./models/Session.model"; +import fakeSessions from "./integration-tests/data/fakeSessions"; +import fakeUsers from "./integration-tests/data/fakeUsers"; const app = express(); @@ -20,4 +24,22 @@ app.listen(process.env.PORT, () => { logger.info(`Listening on port ${process.env.PORT}!`); }); -export default app; // For integration testing \ No newline at end of file +const sessions = fakeSessions; +const users = fakeUsers; +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + +new Promise(async () => { + let s_i = 0; + let u_i = 0; + + while (s_i < sessions.length && u_i < users.length) { + addSingleSessionToSpreadsheet(users[u_i], sessions[s_i]); + s_i++; + u_i++; + await delay(300); + console.log(s_i); + } +}); + +export default app; // For integration testing diff --git a/src/models/User.model.ts b/src/models/User.model.ts index 55a491a..173ffb4 100644 --- a/src/models/User.model.ts +++ b/src/models/User.model.ts @@ -61,6 +61,13 @@ async function updateUser(user_id: number, values: Partial): Promise(sql, [time_in_ms, user_id]); + + return resHeader.affectedRows === 1; +} + async function deleteUserByPassword(password: number): Promise { const sql = `DELETE FROM ${usersTableName} WHERE password = ? RETURNING user_id, first_name, last_name, password, signed_in, last_signed_in, total_time`; const [users] = await db.query(sql, [password]); @@ -69,4 +76,4 @@ async function deleteUserByPassword(password: number): Promise { } export default User; -export { getAllUsers, getUserById, getUserByPassword, createUser, updateUser, deleteUserByPassword }; +export { getAllUsers, getUserById, getUserByPassword, createUser, updateUser, addToUserTotalTime, deleteUserByPassword }; diff --git a/src/spreadsheet/insert.ts b/src/spreadsheet/insert.ts index 8aa3619..64200f0 100644 --- a/src/spreadsheet/insert.ts +++ b/src/spreadsheet/insert.ts @@ -33,6 +33,8 @@ async function insertColumn(spreadsheetId: string, sheetId: number, newColumnLet }, }) ); + + return columnLetter; } // newRowNumber: What letter the new column will have @@ -66,6 +68,8 @@ async function insertRow(spreadsheetId: string, sheetId: number, newRowNumber?: }, }) ); + + return rowNumber + 1; } export { insertColumn, insertRow }; diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index 92a68af..bf793da 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -9,11 +9,14 @@ declare namespace NodeJS { HOURS_SPREADSHEET_ID: string; TOTAL_HOURS_SHEET_ID: number; - TOTAL_HOURS_SHEET_NAMES_COLUMNS_RANGE: string; + TOTAL_HOURS_SHEET_FIRST_NAME_COLUMN: string; + TOTAL_HOURS_SHEET_LAST_NAME_COLUMN: string; TOTAL_HOURS_SHEET_HOURS_COLUMN: string; HOURS_HISTORY_SHEET_ID: number; - HOURS_HISTORY_SHEET_NAMES_RANGE: string; + HOURS_HISTORY_SHEET_FIRST_NAME_COLUMN: string; + HOURS_HISTORY_SHEET_LAST_NAME_COLUMN: string; + HOURS_HISTORY_SHEET_DATE_ROW: number; DB_HOST: string; DB_USER: string; diff --git a/src/utils/spreadsheet-hours/add-single-session.ts b/src/utils/spreadsheet-hours/add-single-session.ts index 3f91de1..648bdf3 100644 --- a/src/utils/spreadsheet-hours/add-single-session.ts +++ b/src/utils/spreadsheet-hours/add-single-session.ts @@ -1,8 +1,10 @@ import Session from "../../models/Session.model"; import User from "../../models/User.model"; -import { insertRow, readCell, writeCell } from "../../spreadsheet"; +import { insertColumn, insertRow, readCell, writeCell } from "../../spreadsheet"; import { Cell } from "../../spreadsheet/cell"; import { RangeNotFound } from "../errors"; +import { dateToMMDD } from "./helpers/date-formatter"; +import { getDateColumnFromHoursHistorySheet } from "./helpers/get-date-column"; import { getUserRowFromTotalHoursSheet, getUserRowFromHoursHistorySheet } from "./helpers/get-user-row"; import { msTimesToHourDuration } from "./helpers/times-to-duration"; @@ -13,39 +15,84 @@ async function addSingleSessionToSpreadsheet(user: User, session: Session): Prom return; } + const sessionTime = msTimesToHourDuration(session.start_time, session.end_time) const totalHoursSheetRow = await getUserRowFromTotalHoursSheet(user); // TODO: If row does not exists throw error const totalHoursCell = new Cell(process.env.TOTAL_HOURS_SHEET_HOURS_COLUMN, totalHoursSheetRow); - let hoursCellData; - try { - hoursCellData = await readCell(process.env.HOURS_SPREADSHEET_ID, process.env.TOTAL_HOURS_SHEET_ID, totalHoursCell); - } catch (e) { - if (e instanceof RangeNotFound) { - hoursCellData = ""; - } else { - throw e; - } - } + // let totalHoursCellData; + // try { + // totalHoursCellData = await readCell(process.env.HOURS_SPREADSHEET_ID, process.env.TOTAL_HOURS_SHEET_ID, totalHoursCell); + // } catch (e) { // Can throw error if row and column exist but there is nothing in the cell + // if (e instanceof RangeNotFound) { + // totalHoursCellData = ""; + // } else { + // throw e; + // } + // } + + // let totalHours = isFinite(parseFloat(totalHoursCellData)) ? parseFloat(totalHoursCellData) : 0; // If is not finite, then set to 0 TODO: have it check the hours logged in database instead of using zero + // totalHours += sessionTime; + // await writeCell(process.env.HOURS_SPREADSHEET_ID, process.env.TOTAL_HOURS_SHEET_ID, totalHoursCell, totalHours); - let totalHours = isFinite(parseFloat(hoursCellData)) ? parseFloat(hoursCellData) : 0; // If is not finite, then set to 0 TODO: have it check the hours logged in database - totalHours += msTimesToHourDuration(session.start_time, session.end_time); - await writeCell(process.env.HOURS_SPREADSHEET_ID, process.env.TOTAL_HOURS_SHEET_ID, totalHoursCell, totalHours); - - // TODO: hours history calculation - const hoursHistorySheetRow = await getUserRowFromHoursHistorySheet(user); + // // TODO: hours history calculation + let hoursHistorySheetUserRow = await getUserRowFromHoursHistorySheet(user); // IF user's row does not exist, append at bottom - if (hoursHistorySheetRow === -1) { - await insertRow(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID); + if (hoursHistorySheetUserRow === -1) { + hoursHistorySheetUserRow = await insertRow(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID); + // Add name to header row + await writeCell( + process.env.HOURS_SPREADSHEET_ID, + process.env.HOURS_HISTORY_SHEET_ID, + new Cell(process.env.HOURS_HISTORY_SHEET_FIRST_NAME_COLUMN, hoursHistorySheetUserRow), + user.first_name + ); + await writeCell( + process.env.HOURS_SPREADSHEET_ID, + process.env.HOURS_HISTORY_SHEET_ID, + new Cell(process.env.HOURS_HISTORY_SHEET_LAST_NAME_COLUMN, hoursHistorySheetUserRow), + user.last_name + ); } - + const date = new Date(session.start_time); + let hoursHistorySheetDateColumn = await getDateColumnFromHoursHistorySheet(date); + let dateHours = 0; // IF current date column does not exist, append at the right - // Add date to header row - // ELSE get existing cell data from user's row and current date column intersection + if (!hoursHistorySheetDateColumn) { + hoursHistorySheetDateColumn = await insertColumn(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID); + + // Add date to header row + await writeCell( + process.env.HOURS_SPREADSHEET_ID, + process.env.HOURS_HISTORY_SHEET_ID, + new Cell(hoursHistorySheetDateColumn, process.env.HOURS_HISTORY_SHEET_DATE_ROW), + dateToMMDD(date) + ); + } else { + // ELSE get existing cell data from user's row and current date column intersection + let dateHoursData; + try { + dateHoursData = await readCell(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID, new Cell(hoursHistorySheetDateColumn, hoursHistorySheetUserRow)) + } catch (e) { + if (e instanceof RangeNotFound) { + dateHoursData = ""; + } else { + throw e; + } + } + + // TODO: If is not finite, have it check the hours logged in database instead of using zero + if (isFinite(parseFloat(dateHoursData))) { + dateHours = parseFloat(dateHoursData); + } + } + // Add new session time to existing cell data time + dateHours += sessionTime; // Overwrite existing cell data + await writeCell(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID, new Cell(hoursHistorySheetDateColumn, hoursHistorySheetUserRow), dateHours); } export { addSingleSessionToSpreadsheet }; diff --git a/src/utils/spreadsheet-hours/helpers/date-formatter.ts b/src/utils/spreadsheet-hours/helpers/date-formatter.ts new file mode 100644 index 0000000..10160d5 --- /dev/null +++ b/src/utils/spreadsheet-hours/helpers/date-formatter.ts @@ -0,0 +1,5 @@ +function dateToMMDD(date: Date): string { + return `${date.getMonth() + 1}/${date.getDate()}`; +} + +export { dateToMMDD }; diff --git a/src/utils/spreadsheet-hours/helpers/get-date-column.ts b/src/utils/spreadsheet-hours/helpers/get-date-column.ts new file mode 100644 index 0000000..01df92d --- /dev/null +++ b/src/utils/spreadsheet-hours/helpers/get-date-column.ts @@ -0,0 +1,20 @@ +import { readCellRange } from "../../../spreadsheet" +import { columnToLetter } from "../../../spreadsheet/cell"; +import { dateToMMDD } from "./date-formatter"; + +// Returns the column letter that contains the date data in total hours sheet +// Returns empty string if column cannot be found +async function getDateColumnFromHoursHistorySheet(date: Date): Promise { + const rowRange = `${process.env.HOURS_HISTORY_SHEET_DATE_ROW}:${process.env.HOURS_HISTORY_SHEET_DATE_ROW}`; + const dates = (await readCellRange(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID, rowRange)).flat(); + + for (let i = 0; i < dates.length; i++) { + if (dates[i] === dateToMMDD(date)) { + return columnToLetter(i); + } + } + + return ""; +} + +export { getDateColumnFromHoursHistorySheet } \ No newline at end of file diff --git a/src/utils/spreadsheet-hours/helpers/get-user-row.ts b/src/utils/spreadsheet-hours/helpers/get-user-row.ts index 2d67b47..5b646ab 100644 --- a/src/utils/spreadsheet-hours/helpers/get-user-row.ts +++ b/src/utils/spreadsheet-hours/helpers/get-user-row.ts @@ -4,11 +4,21 @@ import { readCellRange } from "../../../spreadsheet"; // Returns the row number (as seen in Google Sheets) that contains user data in main sheet // Returns -1 if row cannot be found async function getUserRowFromTotalHoursSheet(user: User): Promise { - const names = await readCellRange(process.env.HOURS_SPREADSHEET_ID, process.env.TOTAL_HOURS_SHEET_ID, process.env.TOTAL_HOURS_SHEET_NAMES_COLUMNS_RANGE); + const firstNameRange = `${process.env.TOTAL_HOURS_SHEET_FIRST_NAME_COLUMN}:${process.env.TOTAL_HOURS_SHEET_FIRST_NAME_COLUMN}`; + const lastNameRange = `${process.env.TOTAL_HOURS_SHEET_LAST_NAME_COLUMN}:${process.env.TOTAL_HOURS_SHEET_LAST_NAME_COLUMN}`; - let i; - for (i = 0; i < names.length; i++) { - if (user.first_name.toLowerCase() === names[i][0].trim().toLowerCase() && user.last_name.toLowerCase() === names[i][1].trim().toLowerCase()) { + const firstNames = ( + await readCellRange(process.env.HOURS_SPREADSHEET_ID, process.env.TOTAL_HOURS_SHEET_ID, firstNameRange) + ).flat(); + const lastNames = ( + await readCellRange(process.env.HOURS_SPREADSHEET_ID, process.env.TOTAL_HOURS_SHEET_ID, lastNameRange) + ).flat(); + + for (let i = 0; i < firstNames.length; i++) { + if ( + user.first_name.toLowerCase() === firstNames[i].trim().toLowerCase() && + user.last_name.toLowerCase() === lastNames[i].trim().toLowerCase() + ) { return i + 1; } } @@ -19,11 +29,22 @@ async function getUserRowFromTotalHoursSheet(user: User): Promise { // Returns the row number (as seen in Google Sheets) that contains user data in total hours sheet // Returns -1 if row cannot be found async function getUserRowFromHoursHistorySheet(user: User): Promise { - const names = await readCellRange(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID, process.env.HOURS_HISTORY_SHEET_NAMES_RANGE); + const firstNameRange = `${process.env.HOURS_HISTORY_SHEET_FIRST_NAME_COLUMN}:${process.env.HOURS_HISTORY_SHEET_FIRST_NAME_COLUMN}`; + const lastNameRange = `${process.env.HOURS_HISTORY_SHEET_LAST_NAME_COLUMN}:${process.env.HOURS_HISTORY_SHEET_LAST_NAME_COLUMN}`; + + const firstNames = ( + await readCellRange(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID, firstNameRange) + ).flat(); + const lastNames = ( + await readCellRange(process.env.HOURS_SPREADSHEET_ID, process.env.HOURS_HISTORY_SHEET_ID, lastNameRange) + ).flat(); let i; - for (i = 0; i < names.length; i++) { - if (user.first_name.toLowerCase() === names[i][0].trim().toLowerCase() && user.last_name.toLowerCase() === names[i][1].trim().toLowerCase()) { + for (i = 0; i < firstNames.length; i++) { + if ( + user.first_name.toLowerCase() === firstNames[i].trim().toLowerCase() && + user.last_name.toLowerCase() === lastNames[i].trim().toLowerCase() + ) { return i + 1; } } diff --git a/template.env b/template.env index 5e49848..ee6f2b0 100644 --- a/template.env +++ b/template.env @@ -3,11 +3,14 @@ PORT= HOURS_SPREADSHEET_ID= TOTAL_HOURS_SHEET_ID= -TOTAL_HOURS_SHEET_NAMES_COLUMNS_RANGE= +TOTAL_HOURS_SHEET_FIRST_NAME_COLUMN= +TOTAL_HOURS_SHEET_LAST_NAME_COLUMN= TOTAL_HOURS_SHEET_HOURS_COLUMN= HOURS_HISTORY_SHEET_ID= -HOURS_HISTORY_SHEET_NAMES_RANGE= +HOURS_HISTORY_SHEET_FIRST_NAME_COLUMN= +HOURS_HISTORY_SHEET_LAST_NAME_COLUMN= +HOURS_HISTORY_SHEET_DATE_ROW= DEFAULT_EXP_BACKOFF_MAX_ATTEMPTS=