diff --git a/package-lock.json b/package-lock.json index 60bdf62..94b3a5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "react-scripts": "^5.0.1", "react-tabs": "^6.0.0", "react-time-picker": "^6.0.4", + "react-toastify": "^9.1.3", "reactjs-popup": "^2.0.5", "typescript": "^5.0.4", "uuid": "^9.0.0", @@ -27786,6 +27787,18 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "license": "BSD-3-Clause", diff --git a/package.json b/package.json index 9fb1438..562a8c5 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-scripts": "^5.0.1", "react-tabs": "^6.0.0", "react-time-picker": "^6.0.4", + "react-toastify": "^9.1.3", "reactjs-popup": "^2.0.5", "typescript": "^5.0.4", "uuid": "^9.0.0", diff --git a/src/components/Auth/apiClient.tsx b/src/components/Auth/apiClient.tsx index 568c090..07ec819 100644 --- a/src/components/Auth/apiClient.tsx +++ b/src/components/Auth/apiClient.tsx @@ -2,6 +2,7 @@ import { Auth } from "aws-amplify"; import axios, { AxiosInstance } from "axios"; import { TimeSheetSchema } from "../../schemas/TimesheetSchema"; import { UserSchema } from "../../schemas/UserSchema"; +import { UserTypes } from "../TimeCardPage/types"; const defaultBaseUrl = process.env.REACT_APP_API_BASE_URL ?? "http://localhost:3000"; @@ -66,7 +67,7 @@ export class ApiClient { } public async updateTimesheet(req): Promise { - return this.axiosInstance.post('/auth/timesheet', req) + return this.axiosInstance.post("/auth/timesheet", req); } // TODO: setup endpoint for associate/supervisor/admin so it returns a list of timesheets for given uuid @@ -74,12 +75,12 @@ export class ApiClient { return this.get("auth/timesheet") as Promise; } - public async updateUserTimesheet(updatedEntry): Promise { - //TODO - Format json? - return this.post("/auth/timesheet", { - timesheet: updatedEntry, - }) as Promise; - } + // public async updateUserTimesheet(updatedEntry): Promise { + // //TODO - Format json? + // return this.post("/auth/timesheet", { + // timesheet: updatedEntry, + // }) as Promise; + // } public async getPasswordTest(): Promise { return this.get("/auth/timesheet") as Promise; @@ -92,7 +93,7 @@ export class ApiClient { UserID: "abc", FirstName: "john", LastName: "doe", - Type: "Supervisor", + Type: UserTypes.Associate, Picture: "https://www.google.com/koala.png", }; } @@ -104,7 +105,7 @@ export class ApiClient { UserID: "bcd", FirstName: "joe", LastName: "jane", - Type: "Associate", + Type: UserTypes.Associate, Picture: "https://www.google.com/panda.png", }, ]; diff --git a/src/components/TimeCardPage/SubmitCard.tsx b/src/components/TimeCardPage/SubmitCard.tsx index 39f70e5..4df6e61 100644 --- a/src/components/TimeCardPage/SubmitCard.tsx +++ b/src/components/TimeCardPage/SubmitCard.tsx @@ -1,51 +1,213 @@ -import CommentModal from './CommentModal'; -import { CardState } from './types' +import CommentModal from "./CommentModal"; +import { CardState } from "./types"; -import React, { useState, useEffect } from 'react' -import { Box, Card, CardHeader, CardBody, CardFooter, Button } from '@chakra-ui/react'; -import { DEFAULT_COLORS } from 'src/constants'; +import React, { useState, useEffect } from "react"; +import { + Box, + Card, + CardHeader, + CardBody, + CardFooter, + Button, +} from "@chakra-ui/react"; +import { DEFAULT_COLORS } from "src/constants"; +import ApiClient from "src/components/Auth/apiClient"; +import * as updateSchemas from "src/schemas/backend/UpdateTimesheet"; +import { StatusType, StatusEntryType } from "src/schemas/StatusSchema"; +import { UserTypes } from "./types"; +import { TimesheetStatusSchema } from "src/schemas/backend/Timesheet"; +import moment from "moment"; +import { useToast } from "@chakra-ui/react"; +interface submitCardProps { + timesheetId: number; + associateId: string; + userType: UserTypes; // TODO : This should really be in global context for react + timesheetStatus: StatusType; + refreshTimesheetCallback: Function; +} -export default function SubmitCard() { + +export default function SubmitCard(props: submitCardProps) { + + const toast = useToast(); + + /** Whether or not the logged-in user has submitted this timesheet yet.*/ const [submitted, setSubmitted] = useState(false); + + /** The date and time (as a moment) that the logged-in user submitted/reviewed/finalized this timesheet.*/ const [submitDate, setSubmitDate] = useState(null); + + /** + * The card state which corresponds to the latest status update from the timesheet. Corresponds to card color. + * Note that this is *not* dependent on the logged in user. I.e. if the latest status update was that + * the supervisor had submitted their timesheet review, the card state would be CardState.InReviewAdmin for + * any associate, supervisor, or admin that was viewing the timesheet. + */ const [state, setState] = useState(CardState.Unsubmitted); + // TODO: Add information about who submitted when as state variables? i.e. included the authorIds somewhere useEffect(() => { - //TODO - API Call to determine if the table has been submitted or not. - //Will set submitted? here and also submitDate if it was submitted to grab the date - }, []) + let statusEntry: StatusEntryType = undefined; - const submitAction = () => { - setSubmitted(!submitted); - const currentTime = new Date(); - setSubmitDate(currentTime.toString()); - if (state === CardState.Unsubmitted) { - setState(CardState.InReviewEmployer); + // Determine the appropriate status entry to match up with the logged in user's role + switch (props.userType) { + case UserTypes.Associate: + statusEntry = props.timesheetStatus.HoursSubmitted; + break; + + case UserTypes.Supervisor: + statusEntry = props.timesheetStatus.HoursReviewed; + break; + + case UserTypes.Admin: + statusEntry = props.timesheetStatus.Finalized; + break; } - else { + + const isSubmitted = statusEntry === undefined; + setSubmitted(isSubmitted); + + // Set the submitted date to when this + if (submitted) { + setSubmitDate(moment.unix(statusEntry.Date)); + } + + // Determine the latest status update to set the card state + if (props.timesheetStatus.Finalized !== undefined) { + setState(CardState.AdminFinalized); + } else if (props.timesheetStatus.HoursReviewed !== undefined) { + setState(CardState.InReviewAdmin); + } else if (props.timesheetStatus.HoursSubmitted !== undefined) { + setState(CardState.InReviewSupervisor); + } else { setState(CardState.Unsubmitted); } - } + }, []); + + const submitAction = () => { + // Update the current timesheet to be submitted by the logged-in user. + // The type of status can be determined on the backend by the user type + try { + // TODO comment this out if not testing end-to-end functionality + ApiClient.updateTimesheet( + // TODO: This needs to get updated; match up status change request schema with backend) + updateSchemas.StatusChangeRequest.parse({ + TimesheetId: props.timesheetId, + AssociateId: props.associateId, + }) + ); + + // TODO: Confirm successful 2xx code response from API + props.refreshTimesheetCallback(); + + toast({ + title: "Successful submission!", + status: "success", + duration: 3000, + isClosable: true, + }); + } catch (err) { + // TODO: Send toast error message + // toast.error('Uh oh - something went wrong with submitting...') + toast({ + title: "Uh oh, something went wrong...", + status: "error", + duration: 3000, + isClosable: true, + }); + return; + } + + // setSubmitted(submitted); + // const currentTime = new Date(); + // setSubmitDate(currentTime.toString()); + // if (state === CardState.Unsubmitted) { + // setState(CardState.InReviewSupervisor); + // } else { + // setState(CardState.Unsubmitted); + // } + }; return ( - + + className="mb-2 text-center" + > - + - {submitted && - {submitDate} - {state} - + {submitted && ( + + {submitDate} + {/*{state}*/} + {/* TODO: this should come from each StatusEntry in the props.timesheetStatus object */} + {/*Associate: abcb34993-1289378457-abdbd, 10-12-2023*/} + {/*Supervisor: some-toher-id, 10-14-2023*/} + {/*Admin: not submitted*/} + +
+ {props.timesheetStatus.HoursSubmitted && props.timesheetStatus.HoursSubmitted.Date ? ( +

+ Associate: {props.timesheetStatus.HoursSubmitted.AuthorID}, {customDateFormat(props.timesheetStatus.HoursSubmitted.Date)} +

+ ) : (

Associate: Unsubmitted

)} + + {props.timesheetStatus.HoursReviewed && props.timesheetStatus.HoursReviewed.Date ? ( +

+ Supervsior: {props.timesheetStatus.HoursReviewed.AuthorID}, {customDateFormat(props.timesheetStatus.HoursReviewed.Date)} +

+ ) : (

Supervsior:Unsubmitted

)} + + {props.timesheetStatus.Finalized && props.timesheetStatus.Finalized.Date ? ( +

+ Admin: {props.timesheetStatus.Finalized.AuthorID}, {customDateFormat(props.timesheetStatus.Finalized.Date)} +

+ ) :(

Admin: Unsubmitted

)} + + + +
+ -
} + + + + + + +
+ )}
-
+
); +} + +// function customDateFormat(date) { +// const dateX = new Date(date * 1000); +// const year = dateX.getFullYear(); // Get the full year (e.g., 2023) +// const month = (dateX.getMonth() + 1).toString().padStart(2, '0'); // Get the month (1-12) and pad with leading zero if needed +// const day = dateX.getDate().toString().padStart(2, '0'); // Get the day of the month (1-31) and pad with leading zero if needed +// +// return `${month}/${day}/${year.toString().slice(-2)}`; +// } +function customDateFormat(date) { + const dateX = new Date(date * 1000); + return dateX.toLocaleDateString('en-US', { year: '2-digit', month: '2-digit', day: '2-digit' }); } \ No newline at end of file diff --git a/src/components/TimeCardPage/TimeSheet.tsx b/src/components/TimeCardPage/TimeSheet.tsx index 42ae2b2..ec7101c 100644 --- a/src/components/TimeCardPage/TimeSheet.tsx +++ b/src/components/TimeCardPage/TimeSheet.tsx @@ -1,8 +1,8 @@ -import React, { useState, useMemo } from 'react'; -import TimeTable from './TimeTable' -import { useEffect } from 'react'; -import SubmitCard from './SubmitCard'; -import DateSelectorCard from './SelectWeekCard'; +import React, { useState, useMemo } from "react"; +import TimeTable from "./TimeTable"; +import { useEffect } from "react"; +import SubmitCard from "./SubmitCard"; +import DateSelectorCard from "./SelectWeekCard"; import { Alert, @@ -22,57 +22,111 @@ import { Spacer, HStack, VStack, - ButtonGroup -} from '@chakra-ui/react' + ButtonGroup, +} from "@chakra-ui/react"; +import { + TIMESHEET_DURATION, + TIMEZONE, + EXAMPLE_TIMESHEET, + EXAMPLE_TIMESHEET_2, +} from "src/constants"; -import { TIMESHEET_DURATION, TIMEZONE, EXAMPLE_TIMESHEET, EXAMPLE_TIMESHEET_2 } from 'src/constants'; - -import { Review_Stages, TABLE_COLUMNS } from './types'; -import moment, { Moment } from 'moment-timezone'; +import { Review_Stages, TABLE_COLUMNS } from "./types"; +import moment, { Moment } from "moment-timezone"; -import apiClient from '../Auth/apiClient'; -import AggregationTable from './AggregationTable'; -import { v4 as uuidv4 } from 'uuid'; -import { UserSchema } from '../../schemas/UserSchema' +import apiClient from "../Auth/apiClient"; +import AggregationTable from "./AggregationTable"; +import { v4 as uuidv4 } from "uuid"; +import { UserSchema } from "../../schemas/UserSchema"; +import { TimeSheetSchema } from "src/schemas/TimesheetSchema"; -import { SearchIcon, WarningIcon, DownloadIcon } from '@chakra-ui/icons'; -import { Select, components } from 'chakra-react-select' +import { SearchIcon, WarningIcon, DownloadIcon } from "@chakra-ui/icons"; +import { Select, components } from "chakra-react-select"; +import { useToast } from '@chakra-ui/react' -//TODO - Eventually automate this -const user = 'Example User' +//TODO - Eventually automate this +const user = "Example User"; const testingEmployees = [ - { UserID: "abc", FirstName: "joe", LastName: "jane", Type: "Employee", Picture: "https://www.google.com/koala.png" }, - { UserID: "bcd", FirstName: "david", LastName: "lev", Type: "Employee", Picture: "https://www.google.com/panda.png" }, - { UserID: "cde", FirstName: "crys", LastName: "tal", Type: "Employee", Picture: "https://www.google.com/capybara.png" }, - { UserID: "def", FirstName: "ken", LastName: "ney", Type: "Employee", Picture: "https://www.google.com/koala.png" }, -] + { + UserID: "abc", + FirstName: "joe", + LastName: "jane", + Type: "Employee", + Picture: "https://www.google.com/koala.png", + }, + { + UserID: "bcd", + FirstName: "david", + LastName: "lev", + Type: "Employee", + Picture: "https://www.google.com/panda.png", + }, + { + UserID: "cde", + FirstName: "crys", + LastName: "tal", + Type: "Employee", + Picture: "https://www.google.com/capybara.png", + }, + { + UserID: "def", + FirstName: "ken", + LastName: "ney", + Type: "Employee", + Picture: "https://www.google.com/koala.png", + }, +]; + + +const mockStatusObject ={ + HoursSubmitted:{ + Date:1698590844, + AuthorID: "John", + }, + HoursReviewed: { + Date: 1698590844, + AuthorID: "Jane", + }, + Finalized: { + Date: 1698590844, + AuthorID: "Jackie", + }, + +}; -function ProfileCard({ employee }) { + +function ProfileCard({ employee }) { return ( - + {employee?.FirstName + " " + employee?.LastName} - ) + ); } function SearchEmployeeTimesheet({ employees, setSelected }) { - const handleChange = (selectedOption) => { setSelected(selectedOption); - } + }; const customStyles = { control: (base) => ({ ...base, - flexDirection: 'row-reverse', + flexDirection: "row-reverse", }), - } + }; const DropdownIndicator = (props) => { return ( @@ -83,29 +137,38 @@ function SearchEmployeeTimesheet({ employees, setSelected }) { }; return ( - - `${option.FirstName + " " + option.LastName}`} - getOptionValue={option => `${option.FirstName + " " + option.LastName}`} /> + getOptionLabel={(option) => + `${option.FirstName + " " + option.LastName}` + } + getOptionValue={(option) => + `${option.FirstName + " " + option.LastName}` + } + /> - ) + ); } export default function Page() { - //const today = moment(); - const [selectedDate, setSelectedDate] = useState(moment().startOf('week').day(0)); + const toast = useToast() + //const today = moment(); + const [selectedDate, setSelectedDate] = useState( + moment().startOf("week").day(0) + ); const updateDateRange = (date: Moment) => { setSelectedDate(date); - //TODO - Refactor this to use the constant in merge with contants branch + //TODO - Refactor this to use the constant in merge with contants branch setCurrentTimesheetsToDisplay(userTimesheets, date); - } + }; // fetch the information of the user whos timesheet is being displayed // if user is an employee selected and user would be the same @@ -122,35 +185,49 @@ export default function Page() { // TODO: add types const [userTimesheets, setUserTimesheets] = useState([]); const [currentTimesheets, setCurrentTimesheets] = useState([]); - const [selectedTimesheet, setTimesheet] = useState(undefined); + const [selectedTimesheet, setTimesheet] = + useState(undefined); const [selectedTab, setTab] = useState(undefined); - // this hook should always run first useEffect(() => { - apiClient.getUser().then(userInfo => { + apiClient.getUser().then((userInfo) => { setUser(userInfo); if (userInfo.Type === "Supervisor" || userInfo.Type === "Admin") { - apiClient.getAllUsers().then(users => { + apiClient.getAllUsers().then((users) => { setAssociates(users); setSelectedUser(users[0]); - }) + }); } - setSelectedUser(userInfo) - }) + setSelectedUser(userInfo); + }); // if employee setSelectedUSer to be userinfo // if supervisor/admin get all users // set selected user - }, []) + }, []); - // Pulls user timesheets, marking first returned as the active one - useEffect(() => { - apiClient.getUserTimesheets(selectedUser?.UserID).then(timesheets => { + const getUpdatedTimesheet = (userId) => { + apiClient.getUserTimesheets(userId).then((timesheets) => { setUserTimesheets(timesheets); //By Default just render / select the first timesheet for now setCurrentTimesheetsToDisplay(timesheets, selectedDate); }); - }, [selectedUser]) + } + + // Pulls user timesheets, marking first returned as the active one + useEffect(() => { + getUpdatedTimesheet(selectedUser?.UserID) + }, [selectedUser]); + + // Callback function that triggers GET request to the API to grab the most recent version of timesheets for the current selected user, + // and re-sets the current timesheet state variable + const forceRefreshTimesheet = () => { + if (selectedUser !== undefined) { + getUpdatedTimesheet(selectedUser.UserID); + } else { + toast({title: 'error', status: 'error', duration: 3000, isClosable: true}) + } + } const processTimesheetChange = (updated_sheet) => { // Updating the rows of the selected timesheets from our list of timesheets @@ -158,91 +235,142 @@ export default function Page() { if (entry.TimesheetID === selectedTimesheet.TimesheetID) { return { ...entry, - TableData: updated_sheet.TableData - } + TableData: updated_sheet.TableData, + }; } - return entry + return entry; }); setUserTimesheets(modifiedTimesheets); //Also need to update our list of currently selected - TODO come up with a way to not need these duplicated lists - setCurrentTimesheets(currentTimesheets.map( - (entry) => { + setCurrentTimesheets( + currentTimesheets.map((entry) => { if (entry.TimesheetID === selectedTimesheet.TimesheetID) { return { ...entry, - TableData: updated_sheet.TableData - } + TableData: updated_sheet.TableData, + }; } - return entry - } - )); + return entry; + }) + ); // selectedTimesheet.TableData = rows; - } + }; - const setCurrentTimesheetsToDisplay = (timesheets, currentStartDate: Moment) => { - const newCurrentTimesheets = timesheets.filter(sheet => moment.unix(sheet.StartDate).isSame(currentStartDate, 'day')); + const setCurrentTimesheetsToDisplay = ( + timesheets, + currentStartDate: Moment + ) => { + const newCurrentTimesheets = timesheets.filter((sheet) => + moment.unix(sheet.StartDate).isSame(currentStartDate, "day") + ); setCurrentTimesheets(newCurrentTimesheets); setTimesheet(newCurrentTimesheets[0]); if (newCurrentTimesheets.length > 0) { - setTab(newCurrentTimesheets[0].CompanyID) + setTab(newCurrentTimesheets[0].CompanyID); } - } + }; const renderWarning = () => { const currentDate = moment().tz(TIMEZONE); const dateToCheck = moment(selectedDate); - dateToCheck.add(TIMESHEET_DURATION, 'days'); - if (currentDate.isAfter(dateToCheck, 'days')) { - return - - Your timesheet is late! - Please submit this as soon as possible - + dateToCheck.add(TIMESHEET_DURATION, "days"); + if (currentDate.isAfter(dateToCheck, "days")) { + return ( + + + Your timesheet is late! + + Please submit this as soon as possible + + + ); } else { - const dueDuration = dateToCheck.diff(currentDate, 'days'); - return - - Your timesheet is due in {dueDuration} days! - Remember to press the submit button! - + const dueDuration = dateToCheck.diff(currentDate, "days"); + return ( + + + Your timesheet is due in {dueDuration} days! + + Remember to press the submit button! + + + ); } - } - - + }; return ( <> - {(user?.Type === "Supervisor" || user?.Type === "Admin") ? + {user?.Type === "Supervisor" || user?.Type === "Admin" ? ( <> - - } /> - } /> - : <>} + + } /> + } /> + + ) : ( + <> + )} - {selectedTimesheet && } - + {selectedTimesheet && ( + + )} {useMemo(() => renderWarning(), [selectedDate])} - {currentTimesheets.map( - (sheet) => ( - { setTimesheet(sheet); setTab(sheet.CompanyID) }}>{sheet.CompanyID} - ) + {currentTimesheets.map((sheet) => ( + { + setTimesheet(sheet); + setTab(sheet.CompanyID); + }} + > + {sheet.CompanyID} + + ))} + {currentTimesheets.length > 1 && ( + setTab("Total")}>Total )} - {currentTimesheets.length > 1 && setTab("Total")}>Total} - {selectedTab === "Total" ? - () - : (currentTimesheets.length > 0 && )} - + {selectedTab === "Total" ? ( + + ) : ( + currentTimesheets.length > 0 && ( + + ) + )} + {/* */} - ) -} \ No newline at end of file + ); +} diff --git a/src/components/TimeCardPage/types.tsx b/src/components/TimeCardPage/types.tsx index 4c3d219..c4d5169 100644 --- a/src/components/TimeCardPage/types.tsx +++ b/src/components/TimeCardPage/types.tsx @@ -1,32 +1,44 @@ export enum CellType { - Regular = "Time Worked", - PTO = "PTO" -}; + Regular = "Time Worked", + PTO = "PTO", +} export enum CellStatus { - Active = "Active", - Deleted = "Deleted" + Active = "Active", + Deleted = "Deleted", } export enum CommentType { - Comment = "Comment", - Report = "Report", -}; + Comment = "Comment", + Report = "Report", +} export const enum Review_Stages { - UNSUBMITTED = "Not-Submitted", - EMPLOYEE_SUBMITTED = "Employee Submitted", - ADMIN_REVIEW = "Review (Breaktime)", - APPROVED = "Approved" -}; - -export const TABLE_COLUMNS = ['Type', 'Date', 'Clock-in', 'Clock-Out', 'Hours', 'Comment']; + UNSUBMITTED = "Not-Submitted", + EMPLOYEE_SUBMITTED = "Employee Submitted", + ADMIN_REVIEW = "Review (Breaktime)", + APPROVED = "Approved", +} +export const TABLE_COLUMNS = [ + "Type", + "Date", + "Clock-in", + "Clock-Out", + "Hours", + "Comment", +]; export enum CardState { - Rejected = "Rejected", - InReviewEmployer = "In Review - Employer", - InReviewBreaktime = "In Review - Breaktime", - Completed = "Completed", - Unsubmitted = "Unsubmitted" -} \ No newline at end of file + Rejected = "Rejected", + InReviewSupervisor = "In Review - Supervisor", + InReviewAdmin = "In Review - Admin", + AdminFinalized = "Finalized by Admin", + Unsubmitted = "Unsubmitted", +} + +export enum UserTypes { + Associate = "Associate", + Supervisor = "Supervisor", + Admin = "Admin", +} diff --git a/src/schemas/StatusSchema.tsx b/src/schemas/StatusSchema.tsx new file mode 100644 index 0000000..72ea12a --- /dev/null +++ b/src/schemas/StatusSchema.tsx @@ -0,0 +1,20 @@ +import { z } from "zod"; + +// The status is either undefined, for not being at that stage yet, or +// contains the date and author of approving this submission +export const StatusEntryType = z.union( + [z.object({ + Date: z.number(), + AuthorID: z.string() + }), + z.undefined()]); + +// Status type contains the four stages of the pipeline we have defined +export const StatusType = z.object({ + HoursSubmitted: StatusEntryType, + HoursReviewed: StatusEntryType, + Finalized: StatusEntryType +}); + +export type StatusEntryType = z.infer +export type StatusType = z.infer \ No newline at end of file diff --git a/src/schemas/TimesheetSchema.tsx b/src/schemas/TimesheetSchema.tsx index bbd835e..3ce5e00 100644 --- a/src/schemas/TimesheetSchema.tsx +++ b/src/schemas/TimesheetSchema.tsx @@ -1,32 +1,22 @@ import { z } from "zod"; -import { RowSchema, ScheduledRowSchema, CommentSchema } from './RowSchema'; - -// The status is either undefined, for not being at that stage yet, or -// contains the date and author of approving this submission -export const StatusEntryType = z.union( - [z.object({ - Date: z.number(), - AuthorID: z.string() - }), - z.undefined()]); - -// Status type contains the four stages of the pipeline we have defined -export const StatusType = z.object({ - HoursSubmitted: StatusEntryType, - HoursReviewed: StatusEntryType, - ScheduleSubmitted: StatusEntryType, - Finalized: StatusEntryType -}); +import { RowSchema, ScheduledRowSchema, CommentSchema } from "./RowSchema"; +import { StatusType } from "./StatusSchema"; export const TimeSheetSchema = z.object({ - TimesheetID: z.number(), - UserID: z.string(), + TimesheetID: z.number(), + UserID: z.string(), StartDate: z.number(), - Status: StatusType, - CompanyID: z.string(), - TableData: z.array(RowSchema), + Status: StatusType, + CompanyID: z.string(), + TableData: z.array(RowSchema), ScheduleTableData: z.union([z.undefined(), z.array(ScheduledRowSchema)]), - WeekNotes: z.union([z.undefined(), z.array(CommentSchema)]), -}); + WeekNotes: z.union([z.undefined(), z.array(CommentSchema)]), +}); export type TimeSheetSchema = z.infer; + +export enum TimesheetStatus { + HOURS_SUBMITTED = "HoursSubmitted", + HOURS_REVIEWED = "HoursReviewed", + FINALIZED = "Finalized", +} diff --git a/src/schemas/UserSchema.tsx b/src/schemas/UserSchema.tsx index a51af17..e73e4c0 100644 --- a/src/schemas/UserSchema.tsx +++ b/src/schemas/UserSchema.tsx @@ -1,10 +1,11 @@ import { z } from "zod"; +import { UserTypes } from "src/components/TimeCardPage/types"; export const UserSchema = z.object({ UserID: z.string(), FirstName: z.string(), LastName: z.string(), - Type: z.enum(["Associate", "Supervisor", "Admin"]), + Type: z.enum([UserTypes.Associate, UserTypes.Supervisor, UserTypes.Admin]), Picture: z.string().optional(), }); diff --git a/src/schemas/backend/Timesheet.ts b/src/schemas/backend/Timesheet.ts index 949513e..541cfcc 100644 --- a/src/schemas/backend/Timesheet.ts +++ b/src/schemas/backend/Timesheet.ts @@ -73,15 +73,13 @@ export const StatusEntryType = z.union( z.undefined()]); // Status type contains the four stages of the pipeline we have defined -export const TimesheetStatus = z.object({ +export const TimesheetStatusSchema = z.object({ HoursSubmitted: StatusEntryType, HoursReviewed: StatusEntryType, ScheduleSubmitted: StatusEntryType, Finalized: StatusEntryType }); - - /** * Represents the database schema for a weekly timesheet */ @@ -89,16 +87,16 @@ export const TimeSheetSchema = z.object({ TimesheetID: z.number(), UserID: z.string(), StartDate: z.number(), - Status: TimesheetStatus, + Status: TimesheetStatusSchema, CompanyID: z.string(), HoursData: z.array(TimesheetEntrySchema).default([]), ScheduleData: z.array(ScheduleEntrySchema).default([]), WeekNotes: z.array(NoteSchema).default([]), }) -export type TimesheetStatus = z.infer +export type TimesheetStatus = z.infer export type TimeEntrySchema = z.infer export type ScheduleEntrySchema = z.infer export type NoteSchema = z.infer export type TimesheetEntrySchema = z.infer -export type TimeSheetSchema = z.infer +export type TimeSheetSchema = z.infer \ No newline at end of file diff --git a/src/schemas/backend/UpdateTimesheet.ts b/src/schemas/backend/UpdateTimesheet.ts index 851382d..a546ff7 100644 --- a/src/schemas/backend/UpdateTimesheet.ts +++ b/src/schemas/backend/UpdateTimesheet.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { RowSchema, CommentSchema, ScheduledRowSchema } from "../RowSchema" import * as dbtypes from './Timesheet' +import { TimesheetStatus } from "../TimesheetSchema"; /* ------------------------------------------------------------------------------------------------------------------- @@ -11,8 +12,18 @@ import * as dbtypes from './Timesheet' ------------------------------------------------------------------------------------------------------------------- */ +/* + The supported timesheet operations currently supported. + Most operations relate to items that are inside the timesheet, whether it is the rows of the timesheet, the comments someone left + on it for example. + + INSERT - Inserting an item into the timesheet + UPDATE - Updating a specific item in the timesheet + DELETE - Deleting a speciic item in the timesheet -// Currently supported timesheet operations + STATUS_CHANGE - When the timesheet has been submitted / should be advanced to the next stage + CREATE-TIMESHEET - Operation for creating a timesheet, if it would be useful to have in the future. +*/ export const enum TimesheetOperations { INSERT = "INSERT", UPDATE = "UPDATE", @@ -22,21 +33,36 @@ export const enum TimesheetOperations { } - +/* + The available types of items that are currently supported in the timesheet that list operations can be performed on. + TABLEDATA - the rows of the timesheet- basically their worked schedule + SCHEDULEDATA - the expected schedule they should have worked + WEEKNOTES - the comments left by an employer for that week +*/ export const enum TimesheetListItems { TABLEDATA = "TABLEDATA", - SCHEDULEDATA = "SCHEDULEDATA", + SCHEDULEDATA = "SCHEDULEDATA", // TODO : delete this WEEKNOTES = "WEEKNOTES" } const availableListTypes = z.enum([TimesheetListItems.TABLEDATA, TimesheetListItems.SCHEDULEDATA, TimesheetListItems.WEEKNOTES]) +/* + The schema for a delete request + @Type: The type of the item this delete request is processing - see available types in TimesheetListItems + @Id: The id of the item we are deleting - to know what to remove +*/ export const DeleteRequest = z.object({ Type: availableListTypes, Id: z.string() }) export type DeleteRequest = z.infer +/* + The schema for an insert request for an item + @Type: The type of the item that we are inserting, to know what we should be adding this item to + @Item: The item we are actually inserting, should be the actual item itself. +*/ export const InsertRequest = z.object({ Type: availableListTypes, Item: z.union([RowSchema, CommentSchema, ScheduledRowSchema, dbtypes.TimesheetEntrySchema]), @@ -44,10 +70,10 @@ export const InsertRequest = z.object({ export type InsertRequest = z.infer /* Schema for updating an item from the three possible list of items in the timesheet - Type: The field of the timesheet we are updating from the three supported - Id: the id of the entry we are updating - correlates to that row / entry in the list of items - Attribute: The specific attribute of the object we are updating - Data: The payload we are updating this attribute to be - can be a wide range of things currently + @Type: The field of the timesheet we are updating from the three supported + @Id: the id of the entry we are updating - correlates to that row / entry in the list of items + @Attribute: The specific attribute of the object we are updating + @Data: The payload we are updating this attribute to be - can be a wide range of things currently */ export const UpdateRequest = z.object({ Type: availableListTypes, @@ -57,8 +83,25 @@ export const UpdateRequest = z.object({ }) export type UpdateRequest = z.infer +/* + Schema for changing the status of a timesheet + @TimesheetId: The id of the timesheet we are updating + @AssociateId: The id of the associate whose timesheet is being submitted +*/ +export const StatusChangeRequest = z.object({ + TimesheetId: z.number(), + AssociateId: z.string(), + authorId: z.string(), + dateSubmitted: z.number(), + statusType: z.enum([TimesheetStatus.FINALIZED, TimesheetStatus.HOURS_REVIEWED, TimesheetStatus.HOURS_SUBMITTED]) +}) +export type StatusChangeRequest = z.infer -// The main request body that is used to determine what we should be updating in a request +/* The main request body that is used to determine what we should be updating in a request + @TimesheetID: The id of the timesheet we are updating + @Operation: The type of operation we are performing on this timesheet + @Payload: The contents to be used in the operation for updating this. +*/ export const TimesheetUpdateRequest = z.object({ TimesheetID: z.number(), Operation: z.enum([ @@ -67,8 +110,8 @@ export const TimesheetUpdateRequest = z.object({ TimesheetOperations.DELETE, TimesheetOperations.STATUS_CHANGE, TimesheetOperations.CREATE_TIMESHEET - ]), - Payload: z.any(), + ]), + Payload: z.any() }) export type TimesheetUpdateRequest = z.infer diff --git a/src/setupTests.js b/src/setupTests.js index 1dd407a..4c2ef26 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -3,3 +3,4 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom"; +