Skip to content

Commit

Permalink
Add hours history logging
Browse files Browse the repository at this point in the history
  • Loading branch information
Cow-Van committed May 14, 2024
1 parent 57cd387 commit b0b1bbe
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 38 deletions.
12 changes: 10 additions & 2 deletions src/api/routes/users.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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}!`,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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!",
});
Expand Down Expand Up @@ -202,6 +208,8 @@ router.delete("/session", async (req, res) => {
});
}

// TODO: Update spreadsheet and total time

return res.status(200).json({
description: "Deleted session!",
});
Expand Down
26 changes: 24 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -20,4 +24,22 @@ app.listen(process.env.PORT, () => {
logger.info(`Listening on port ${process.env.PORT}!`);
});

export default app; // For integration testing
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
9 changes: 8 additions & 1 deletion src/models/User.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ async function updateUser(user_id: number, values: Partial<User>): Promise<boole
return resHeader.affectedRows === 1;
}

async function addToUserTotalTime(user_id: number, time_in_ms: number) {
const sql = `UPDATE ${usersTableName} SET total_time = total_time + ? WHERE user_id = ?`;
const [resHeader] = await db.query<ResultSetHeader>(sql, [time_in_ms, user_id]);

return resHeader.affectedRows === 1;
}

async function deleteUserByPassword(password: number): Promise<User> {
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<UserRowDataPacket[]>(sql, [password]);
Expand All @@ -69,4 +76,4 @@ async function deleteUserByPassword(password: number): Promise<User> {
}

export default User;
export { getAllUsers, getUserById, getUserByPassword, createUser, updateUser, deleteUserByPassword };
export { getAllUsers, getUserById, getUserByPassword, createUser, updateUser, addToUserTotalTime, deleteUserByPassword };
4 changes: 4 additions & 0 deletions src/spreadsheet/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ async function insertColumn(spreadsheetId: string, sheetId: number, newColumnLet
},
})
);

return columnLetter;
}

// newRowNumber: What letter the new column will have
Expand Down Expand Up @@ -66,6 +68,8 @@ async function insertRow(spreadsheetId: string, sheetId: number, newRowNumber?:
},
})
);

return rowNumber + 1;
}

export { insertColumn, insertRow };
7 changes: 5 additions & 2 deletions src/types/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
91 changes: 69 additions & 22 deletions src/utils/spreadsheet-hours/add-single-session.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 };
5 changes: 5 additions & 0 deletions src/utils/spreadsheet-hours/helpers/date-formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function dateToMMDD(date: Date): string {
return `${date.getMonth() + 1}/${date.getDate()}`;
}

export { dateToMMDD };
20 changes: 20 additions & 0 deletions src/utils/spreadsheet-hours/helpers/get-date-column.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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 }
35 changes: 28 additions & 7 deletions src/utils/spreadsheet-hours/helpers/get-user-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> {
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;
}
}
Expand All @@ -19,11 +29,22 @@ async function getUserRowFromTotalHoursSheet(user: User): Promise<number> {
// 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<number> {
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;
}
}
Expand Down
7 changes: 5 additions & 2 deletions template.env
Original file line number Diff line number Diff line change
Expand Up @@ -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=

Expand Down

0 comments on commit b0b1bbe

Please sign in to comment.