diff --git a/src/components/TimeCardPage/CellTypes/ConflictableTimeEntry.tsx b/src/components/TimeCardPage/CellTypes/ConflictableTimeEntry.tsx new file mode 100644 index 0000000..daf5a55 --- /dev/null +++ b/src/components/TimeCardPage/CellTypes/ConflictableTimeEntry.tsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState } from "react"; +import { RowSchema, TimeRowEntry } from "../../../schemas/RowSchema"; +import { TimeEntry } from "./TimeEntry"; +import { + Button, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Editable, + EditablePreview, + EditableInput, + Input, + Box, + ButtonGroup, + Flex, + useDisclosure, + useEditableControls, + StackDivider, + HStack, + VStack, + Text, + IconButton +} from "@chakra-ui/react"; + +import { + ChatIcon, + CheckIcon, + CloseIcon, + EditIcon, + AddIcon, + DeleteIcon, + WarningIcon +} from "@chakra-ui/icons"; + + + +interface ConflicatableTimeEntryProps { + field: string; + row: RowSchema; + updateFields: Function; +} + +/** + * This represents a time entry cell that accounts for multiple time entries (i.e. an associate's and a supervisor's entries) + * and allows for conflicts to be shown. + */ +export function ConflicatableTimeEntry(props: ConflicatableTimeEntryProps) { + const [selectedTime, setSelectedTime] = useState(null); + + const handleTimeSelection = (time: number) => { + setSelectedTime(time); + }; + + // determine if the associate and supervisor times are conflicting for this cell + const hasAssociateTime: boolean = props.row.Associate !== undefined; + const hasSupervisorTime: boolean = props.row.Supervisor !== undefined; + const hasAdminTime: boolean = props.row.Admin !== undefined; + + // TODO : + // -- only show conflict button on conflict + // -- auto-populate with the following rules: + // ---- if associate and supervisor times are the same, auto-populate with that time + // ---- if associate and supervisor times are different, auto-populate with null + // -- make sure that time conflict entry cells only show up on admins + + // Conflicts are defined as: + // 1. associate submitted a time, and supervisor did not + // 2. Supervisor submitted a time for this entry, and associate did not + // 3. Associate time and supervisor reported time differ + var hasConflict = hasAssociateTime !== hasSupervisorTime || + (hasAssociateTime && hasSupervisorTime && props.row.Associate[props.field] !== props.row.Supervisor[props.field]); + + useEffect(() => { + if (!hasConflict && hasAssociateTime && !hasAdminTime) { + setSelectedTime(props.row.Associate[props.field]) + } else if (hasAdminTime) { + setSelectedTime(props.row.Admin[props.field]) + } + }, []); + + // TODO: Toggle clicker to display popup with conflict only if hasConflict is true + return ( + <> + { hasConflict && ( ) } + + + ); + + // TODO: make sure to update props field appropriately to trigger the correct autosaving +} + +interface TimeConflictPopupProps { + field: string; + associateEntry: TimeRowEntry; + supervisorEntry: TimeRowEntry; + onSelectTime: Function; +} + +export function TimeConflictPopup(props: TimeConflictPopupProps) { + const { isOpen, onOpen, onClose } = useDisclosure(); + + const hasAssociateTime: boolean = props.associateEntry !== undefined && props.associateEntry !== null; + const hasSupervisorTime: boolean = props.supervisorEntry !== undefined && props.supervisorEntry !== null; + + const associateTime: number = hasAssociateTime ? props.associateEntry[props.field] : null; + const supervisorTime: number = hasSupervisorTime ? props.supervisorEntry[props.field] : null; + + const convertMinutesToTime = (minutes) => { + const hours = Math.round(minutes / 60) + const mins = minutes % 60; + return hours + ":" + mins; + } + + return ( + <> + } onClick={onOpen}> + + + + + + Time Conflict + + + + + These are the entries submitted: + + + + + Associate Time: {hasAssociateTime ? convertMinutesToTime(props.associateEntry[props.field]) : "none"} + Supervisor Time: {hasSupervisorTime ? convertMinutesToTime(props.supervisorEntry[props.field]) : "none"} + + + + + + ) +} \ No newline at end of file diff --git a/src/components/TimeCardPage/CellTypes/HoursCell.tsx b/src/components/TimeCardPage/CellTypes/HoursCell.tsx index 9de3e90..510d7da 100644 --- a/src/components/TimeCardPage/CellTypes/HoursCell.tsx +++ b/src/components/TimeCardPage/CellTypes/HoursCell.tsx @@ -5,16 +5,20 @@ import { Box } from '@chakra-ui/react'; interface DurationProps { row: RowSchema; + userType: string; // Associate, Supervisor, or Admin } +/** + * Calculation component that shows the length of time for the given start and end times of the shift + */ export function Duration(props: DurationProps) { const row = props.row; const [duration, setDuration] = useState(""); useEffect(() => { - if (row.Associate !== undefined && row.Associate.Start !== undefined && row.Associate.End !== undefined) { - setDuration(String(((row.Associate.End - row.Associate.Start) / 60).toFixed(2))); + if (row[props.userType] !== undefined && row[props.userType].Start !== undefined && row[props.userType].End !== undefined) { + setDuration(String(((row[props.userType].End - row[props.userType].Start) / 60).toFixed(2))); } - }, [row.Associate?.Start, row.Associate?.End]) + }, [row[props.userType]?.Start, row[props.userType]?.End]) return {duration} } diff --git a/src/components/TimeCardPage/CellTypes/HoursPickerPopup.tsx b/src/components/TimeCardPage/CellTypes/HoursPickerPopup.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TimeCardPage/CellTypes/TimeEntry.tsx b/src/components/TimeCardPage/CellTypes/TimeEntry.tsx index 90ff346..5dd836b 100644 --- a/src/components/TimeCardPage/CellTypes/TimeEntry.tsx +++ b/src/components/TimeCardPage/CellTypes/TimeEntry.tsx @@ -6,65 +6,81 @@ interface TimeEntryProps { field: string; row: RowSchema; updateFields: Function; + userType: string; // Associate, Supervisor, or Admin + time: number; } export function TimeEntry(props: TimeEntryProps) { - const [minutes, setMinutes] = useState(undefined); + // Date object for the time picker to take in + const [convertedTime, setConvertedTime] = useState(null); - const onChange = (time) => { - var rowToMutate = props.row.Associate; - if (rowToMutate === undefined) { - rowToMutate = { - Start:undefined, End:undefined, AuthorID:"" - } - } + // converted given minutes (number of minutes from 0:00) to Date() object, and update the converted time + const convertMinutesToTime = (minutes) => { + if (minutes !== undefined && minutes !== null) { + const newTime = new Date(); + newTime.setHours(Math.round(minutes / 60)); + newTime.setMinutes(minutes % 60); + setConvertedTime(newTime); + } else { + setConvertedTime(null); + } + }; + + const handleChange = (time) => { + // TODO : This seems to be only able to hook up to the associates. + // We probably want this to easily switch between supervisor vs. associated, + // maybe an enum passed in via the props, similar to the field? + var rowToMutate = props.row[props.userType]; + if (rowToMutate === undefined) { + rowToMutate = { + Start: undefined, + End: undefined, + AuthorID: "", + }; + } + if (time !== null) { + const [hours, parsedMinutes] = time.split(":"); + const calculatedTime = Number(hours) * 60 + Number(parsedMinutes); + rowToMutate[props.field] = calculatedTime; - if (time !== null) { - const [hours, parsedMinutes] = time.split(":"); - const calculatedTime = Number(hours) * 60 + Number(parsedMinutes) - setMinutes(calculatedTime); - rowToMutate[props.field] = calculatedTime; - } else { - // Value is null, so mark it as undefined in our processing - rowToMutate[props.field] = undefined; - setMinutes(undefined); - } - //Triggering parent class to update its references here as well - props.updateFields("Associate", rowToMutate); + // Reset the Date object to the new time - this handles what is actually + // displayed in the time picker visually before page refresh + const newTime = new Date(); + newTime.setHours(hours); + newTime.setMinutes(parsedMinutes); + setConvertedTime(newTime); + } else { + // Value is null, so mark it as undefined in our processing + rowToMutate[props.field] = undefined; + setConvertedTime(null); } - + //Triggering parent class to update its references here as well + props.updateFields(props.userType, rowToMutate); + }; + useEffect(() => { - if (props.row.Associate !== undefined) { - setMinutes(props.row.Associate[props.field]); + let minutes; + if (props.row[props.userType] !== undefined && props.row[props.userType] !== null) { + minutes = (props.row[props.userType][props.field]); + } else if (props.userType === "Admin") { + minutes = props.time; } + convertMinutesToTime(minutes); + console.log(convertedTime); }, []); - return renderClockTime(minutes, onChange); -} + useEffect(() => { + convertMinutesToTime(props.time); + //handleChange(props.time); + console.log("Input time has been changed, " + props.time); + }, [props.time]); -const renderClockTime = (minutes: number, updateClockTime) => { - if (minutes !== undefined) { - const convertedTime = new Date(); - convertedTime.setHours(Math.round(minutes / 60)); - convertedTime.setMinutes(minutes % 60); - return ( - { - updateClockTime(value); - }} - value={convertedTime} - disableClock={true} - /> - ); - } return ( { - updateClockTime(value); - }} - value={null} + onChange={handleChange} + value={convertedTime} disableClock={true} /> ); -}; +} diff --git a/src/components/TimeCardPage/Modals/TimeConflictModal.tsx b/src/components/TimeCardPage/Modals/TimeConflictModal.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TimeCardPage/TimeSheet.tsx b/src/components/TimeCardPage/TimeSheet.tsx index 42ae2b2..a49d5f9 100644 --- a/src/components/TimeCardPage/TimeSheet.tsx +++ b/src/components/TimeCardPage/TimeSheet.tsx @@ -189,6 +189,7 @@ export default function Page() { if (newCurrentTimesheets.length > 0) { setTab(newCurrentTimesheets[0].CompanyID) } + console.log(newCurrentTimesheets[0]); } const renderWarning = () => { @@ -241,7 +242,12 @@ export default function Page() { {selectedTab === "Total" ? () - : (currentTimesheets.length > 0 && )} + : (currentTimesheets.length > 0 && )} ) diff --git a/src/components/TimeCardPage/TimeTable.tsx b/src/components/TimeCardPage/TimeTable.tsx index 2902bc0..561be13 100644 --- a/src/components/TimeCardPage/TimeTable.tsx +++ b/src/components/TimeCardPage/TimeTable.tsx @@ -54,6 +54,7 @@ interface TableProps { timesheet: TimeSheetSchema; columns: String[]; onTimesheetChange: Function; + userType: string; // Associate, Supervisor, or Admin } function TimeTable(props: TableProps) { @@ -152,7 +153,7 @@ function TimeTable(props: TableProps) { { - onRowChange(row, index)} prevDate={dateToSend} TimesheetID={props.timesheet.TimesheetID} /> + onRowChange(row, index)} prevDate={dateToSend} TimesheetID={props.timesheet.TimesheetID} userType={props.userType} /> } ); } diff --git a/src/components/TimeCardPage/TimeTableRow.tsx b/src/components/TimeCardPage/TimeTableRow.tsx index d28cec5..4f3378a 100644 --- a/src/components/TimeCardPage/TimeTableRow.tsx +++ b/src/components/TimeCardPage/TimeTableRow.tsx @@ -1,84 +1,105 @@ -import React, { useEffect, useState } from 'react'; -import 'react-time-picker/dist/TimePicker.css'; -import 'react-clock/dist/Clock.css'; -import { Fragment } from 'react'; +import React, { useEffect, useState } from "react"; +import "react-time-picker/dist/TimePicker.css"; +import "react-clock/dist/Clock.css"; +import { Fragment } from "react"; -import { Td } from '@chakra-ui/react'; +import { Td } from "@chakra-ui/react"; -import { TimeEntry } from './CellTypes/TimeEntry'; -import { Duration } from './CellTypes/HoursCell' -import { DateCell } from './CellTypes/DateCell'; -import { TypeCell } from './CellTypes/CellType'; -import { CommentCell } from './CellTypes/CommentCell'; -import { RowSchema } from '../../schemas/RowSchema'; -import ApiClient from 'src/components/Auth/apiClient' +import { TimeEntry } from "./CellTypes/TimeEntry"; +import { Duration } from "./CellTypes/HoursCell"; +import { DateCell } from "./CellTypes/DateCell"; +import { TypeCell } from "./CellTypes/CellType"; +import { CommentCell } from "./CellTypes/CommentCell"; +import { RowSchema } from "../../schemas/RowSchema"; +import ApiClient from "src/components/Auth/apiClient"; -import * as updateSchemas from 'src/schemas/backend/UpdateTimesheet' -import apiClient from 'src/components/Auth/apiClient'; +import * as updateSchemas from "src/schemas/backend/UpdateTimesheet"; +import apiClient from "src/components/Auth/apiClient"; +import { ConflicatableTimeEntry } from "./CellTypes/ConflictableTimeEntry"; interface RowProps { - row: RowSchema; - prevDate: number; - onRowChange: Function; - TimesheetID: number; + row: RowSchema; + prevDate: number; + onRowChange: Function; + TimesheetID: number; + userType: string; // Associate, Supervisor, or Admin } - - - function Row(props: RowProps) { - - const [fields, setFields] = useState(undefined); - - const updateField = (key, value) => { - - const newFields = { - ...fields, - [key]: value - } - - setFields(newFields); - props.onRowChange(newFields); - //Send a request to update the db on this item being changed - ApiClient.updateTimesheet(updateSchemas.TimesheetUpdateRequest.parse({ - TimesheetID: props.TimesheetID, - Operation: updateSchemas.TimesheetOperations.UPDATE, - Payload: updateSchemas.UpdateRequest.parse({ - Type: updateSchemas.TimesheetListItems.TABLEDATA, - Id: props.row.UUID, - Attribute: key, - Data: value - }) - })); - - } - - useEffect(() => { - if (props.row !== undefined) { - setFields(RowSchema.parse(props.row)); - } - }, []) - - if (fields !== undefined) { - const items = { - "Type": , - "Date": , - "Clock-in": , - "Clock-out": , - "Hours": , - "Comment": , - } - const itemOrdering = ["Type", "Date", "Clock-in", "Clock-out", "Hours", "Comment"]; - - return - {itemOrdering.map((entry) => {items[entry]})} - - - } else { - return - - + const [fields, setFields] = useState(undefined); + + const updateField = (key, value) => { + const newFields = { + ...fields, + [key]: value, + }; + + setFields(newFields); + props.onRowChange(newFields); + //Send a request to update the db on this item being changed + ApiClient.updateTimesheet( + updateSchemas.TimesheetUpdateRequest.parse({ + TimesheetID: props.TimesheetID, + Operation: updateSchemas.TimesheetOperations.UPDATE, + Payload: updateSchemas.UpdateRequest.parse({ + Type: updateSchemas.TimesheetListItems.TABLEDATA, + Id: props.row.UUID, + Attribute: key, + Data: value, + }), + }) + ); + }; + + useEffect(() => { + if (props.row !== undefined) { + setFields(RowSchema.parse(props.row)); } + }, []); + + if (fields !== undefined) { + const items = { + Type: , + Date: , + // TODO : The userType is likely able to be adjusted, only show conflictable time entry for admins. + "Clock-in": ( + + ), + "Clock-out": ( + + ), + Hours: , + Comment: ( + + ), + }; + const itemOrdering = [ + "Type", + "Date", + "Clock-in", + "Clock-out", + "Hours", + "Comment", + ]; + + return ( + + {itemOrdering.map((entry) => ( + {items[entry]} + ))} + + ); + } else { + return ; + } } export default Row;