diff --git a/src/pages/testing/index.tsx b/src/pages/testing/index.tsx index 1dc3c1e..d2952fd 100644 --- a/src/pages/testing/index.tsx +++ b/src/pages/testing/index.tsx @@ -1,6 +1,11 @@ // use this to test your stuff +import QuickShiftBtn from '@/sprintFiles/QuickShift/QuickShiftBtn' const index = () => { - return
inex
+ return ( +
+ +
+ ) } export default index diff --git a/src/sprintFiles/QuickShift/NewQuickShiftCard.tsx b/src/sprintFiles/QuickShift/NewQuickShiftCard.tsx new file mode 100644 index 0000000..fbaa4f9 --- /dev/null +++ b/src/sprintFiles/QuickShift/NewQuickShiftCard.tsx @@ -0,0 +1,57 @@ +import { + Button, + Dialog, + Typography, + DialogContent, + DialogTitle, +} from '@mui/material' +import { useState } from 'react' +import ScheduledShiftForm from './QuickShiftForm' + +//Quick Shift == New Shift card that deals wi +//TODOS: Look below +// Split quickshift card into shift card and quickshiftbtn. +// Have an assigned user. Well-defined date. +// no categories +// select a single day +// skips the shift step of creating schedule. +// will only create a scheduled shift, NOT a SHIFT object. +function NewQuickShiftCard({ + setOpen, + open, +}: { + setOpen: (value: React.SetStateAction) => void + open: boolean +}) { + // const [shiftValues, setShiftValues] = useState(null) + const handleClose = () => { + setOpen(false) + } + + const handleOpen = () => { + setOpen(true) + } + return ( + <> + + + Create Quick Shift + + + + + + + ) +} +export default NewQuickShiftCard diff --git a/src/sprintFiles/QuickShift/QuickShiftBtn.tsx b/src/sprintFiles/QuickShift/QuickShiftBtn.tsx new file mode 100644 index 0000000..bc51f5f --- /dev/null +++ b/src/sprintFiles/QuickShift/QuickShiftBtn.tsx @@ -0,0 +1,39 @@ +import { + Button, + Dialog, + Typography, + DialogContent, + DialogTitle, +} from '@mui/material' +import React from 'react' +import { useState } from 'react' +import NewQuickShiftCard from './NewQuickShiftCard' + +//Quick Shift == New Shift card that deals wi +//TODOS: Look below +// Split quickshift card into shift card and quickshiftbtn. +// Have an assigned user. Well-defined date. +// no categories +// select a single day +// skips the shift step of creating schedule. +// will only create a scheduled shift, NOT a SHIFT object. +function QuickShiftBtn() { + const [open, setOpen] = useState(false) + // const [shiftValues, setShiftValues] = useState(null) + const handleClose = () => { + setOpen(false) + } + + const handleOpen = () => { + setOpen(true) + } + return ( + + + + + ) +} +export default QuickShiftBtn diff --git a/src/sprintFiles/QuickShift/QuickShiftForm.tsx b/src/sprintFiles/QuickShift/QuickShiftForm.tsx new file mode 100644 index 0000000..2aa1dfa --- /dev/null +++ b/src/sprintFiles/QuickShift/QuickShiftForm.tsx @@ -0,0 +1,406 @@ +import { Formik, Form, FormikHelpers, Field } from 'formik' +import { + Stack, + Button, + Typography, + TextField, + Autocomplete, +} from '@mui/material' +import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers' +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { MobileTimePicker } from '@mui/x-date-pickers/MobileTimePicker' +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker' + +import dayjs, { Dayjs } from 'dayjs' +import * as Yup from 'yup' +import { + TextInput, + SelectInput, +} from '../../components/shared/forms/CustomFormikFields' +import { useSelector } from 'react-redux' +import React, { useEffect, useState } from 'react' +import { RootState } from '../../store/store' +import { EntityId } from '@reduxjs/toolkit' +import { House, ScheduledShift, Shift, User } from '../../types/schema' +import styles from './ShiftForm.module.css' +import { + selectScheduledShiftById, + useAddNewScheduledShiftMutation, + useUpdateScheduledShiftMutation, +} from './scheduledShiftApiSlice' +import { + selectShiftById, + useAddNewShiftMutation, + useUpdateShiftMutation, +} from '@/features/shift/shiftApiSlice' +import { useGetShiftsQuery } from '@/features/shift/shiftApiSlice' +import { selectCurrentHouse } from '@/features/auth/authSlice' +import { useGetUsersQuery } from '@/features/user/userApiSlice' +import uuid from 'react-uuid' +import { formatMilitaryTime } from '@/utils/utils' + +const ShiftSchema = Yup.object({ + name: Yup.string() + .required('Name is required') + .min(1, 'Name must have at least 1 characters'), + description: Yup.string(), + possibleDays: Yup.array().of(Yup.string()), + startTime: Yup.date().required('Start time is required'), + endTime: Yup.date().required('End time is required'), + // category: Yup.string().required('Cagegory is required'), + hours: Yup.number().required('Hours credit is required'), + verificationBuffer: Yup.number(), + assignedDay: Yup.string(), + + assignedUser: Yup.object(), //reassign the assignedUser + date: Yup.date(), +}) + +const daysList = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', +] + +// const shiftCategories = ['cook dinner', 'clean bathroom', 'wash dishes', 'clean basement'] + +const emptyShift = { + name: '', + category: '', + possibleDays: [], + startTime: dayjs('2023-04-17T12:00'), + endTime: dayjs('2023-04-17T18:30'), + hours: 0, + despription: '', + verificationBuffer: 0, + assignedUser: { label: '', id: '' }, + assignedDay: dayjs(), +} + +/** + * 1. Fill out the quick shift form like a shift form + * 2. Formik uses validation schema + setField value to track form changes + * 3. For any option, setFieldValue, TextField, and other formik components/funcs will update initialValues + * 4. When submit is pressed, initialValues is sent to onSubmit function. + * 5. onSubmit function accesses the field values from formik. Can manipulate as wanted + * 6. OnSubmit formats the field info to match a scheduled shift. Sends it to firebase + */ + +const QuickShiftForm = ({ + setOpen, + shiftId, + isNewShift, +}: { + setOpen: (value: React.SetStateAction) => void + shiftId?: string + isNewShift: boolean +}) => { + // const authUser = useSelector(selectCurrentUser) as User + const currentHouse = useSelector(selectCurrentHouse) as House + + //** House shifts */ + const { data: shiftsData, isSuccess: isShiftsSuccess } = useGetShiftsQuery( + currentHouse.id + ) + + //** for editing shifts */ + const shift: Shift = useSelector( + (state: RootState) => + selectShiftById(currentHouse.id)(state, shiftId as EntityId) as Shift + ) + + //** Holds the house shifts categories */ + const [houseCategories, setHouseCategories] = useState([ + 'Uncategorized', + ]) + + //* Get API helpers to create or update a shift + const [ + addNewScheduledShift, + { + // isLoading: isLoadingNewShift, + // isSuccess: isSuccessNewShift, + // isError: isErrorNewShift, + // error: errorNewShift, + }, + ] = useAddNewScheduledShiftMutation() + const [ + updateScheduledShift, + { + // isLoading: isLoadingUpdateShift, + // isSuccess: isSuccessUpdateShift, + // isError: isErrorUpdateShift, + // error: errorUpdateShift, + }, + ] = useUpdateScheduledShiftMutation() + + const [chosenDate, setDate] = useState(null) + + /** + * + * Has part of the data formatted into JSONcopy, which is what the original shift should've looked like + * Note that JSONCopy won't match a Shift object in our firebase like a regular scheduled shift would + * Rest of info is stored in a defaulted scheduledShift. Bare min. info to fit a quickshift. + * + */ + const onSubmit = async ( + values: { + name: string + category: string + hours: number + startTime: Dayjs + endTime: Dayjs + possibleDays: string[] + description: string + verificationBuffer: number + + assignedDay: Dayjs + assignedUser: labeledUser | undefined + }, + formikBag: FormikHelpers + ) => { + const { + name, + category: categoryString, + hours, + description, + possibleDays, + startTime: startTimeObject, + endTime: endTimeObject, + verificationBuffer, + assignedUser, + assignedDay, + } = values + + const startTime = Number(startTimeObject.format('HHmm')) + const endTime = Number(endTimeObject.format('HHmm')) + let category + if (categoryString === undefined || categoryString === 'Uncategorized') { + category = '' + } else { + category = categoryString + } + + let result + const timeWindow = { startTime, endTime } + const timeWindowDisplay = + formatMilitaryTime(startTime) + ' - ' + formatMilitaryTime(endTime) + const data = { data: {}, houseId: '', shiftId: '' } + const shiftObject = { + name, + category, + hours, + possibleDays, + description, + timeWindow, + verificationBuffer, + timeWindowDisplay, + assignedUser, + assignedDay, + } + + data.data = { + id: '', + shiftID: '', + date: assignedDay.toString(), + assignedUser: assignedUser, + status: 'live', + verifiedBy: '', + verifiedAt: '', + unverifiedAt: '', + penaltyHours: 0, + jsonCopy: JSON.stringify(shiftObject), //TODO : check if this fails + } + + data.houseId = currentHouse.id + data.shiftId = shiftId ? shiftId : '' + // console.log('data: ', data) + console.log({ + pushedData: data, + datadata: data.data, + isNewShift: isNewShift, + shiftId: shiftId, + }) + + if (isNewShift || !shiftId) { + result = await addNewScheduledShift(data) + } else { + result = await updateScheduledShift(data) + } + if (result) { + console.log('success with shift: ', result) + } + + formikBag.resetForm() + setOpen(false) + } + + // React.useEffect(() => { + // console.log('This is the selected shift', shift) + // }, [shift]) + + const { + data: users, + // isLoading: isUsersLoading, + // isSuccess: isUsersSuccess, + // isError: isUsersError, + } = useGetUsersQuery({}) + + //Using this instead of setFieldValue in formik because of issues with the fields + //Will use the specific date that a quick shift must be in. + const [userOptions, setUserOptions] = useState([{ label: '', id: '' }]) + type labeledUser = { + label: string + id: string + } + const [targetUser, setTargetUser] = useState(userOptions[0]) + + useEffect(() => { + // console.log({ ents: users?.entities, ids: users?.ids, targuser: targetUser, userOpt: Val: inputValue}) + { + const tempOptions: labeledUser[] = [] + if (users == undefined) return + users.ids?.map((id: EntityId) => { + let user = users?.entities[id] + if (user != undefined) { + let userWithLabel: labeledUser = { + label: user.displayName, + id: user.id, + } //TODO: add an ID here as well. + // Object.assign(userWithLabel, user) + tempOptions.push(userWithLabel) + } + }) + emptyShift.assignedUser = tempOptions[0] + setUserOptions(tempOptions) + setTargetUser(tempOptions[0]) + } + console.log('OPTIONS: ', { options: userOptions }) + }, [users]) + const [inputValue, setInputValue] = React.useState('') + /** + * 1. load in the options, push the labeled options to a user options list + * 2. use the userOptions to create the members list + * 3. treat the form like a normal shift form. Except: Categories, assignedUser, possible days will be set to defaults. + * 4. when picking an option from the dropdown, change the member OBJECT to match + * 5. + */ + + return ( + <> + + {({ isSubmitting, values, setFieldValue, errors }) => { + return ( +
+ + + + + setFieldValue('assignedDay', newValue) + } + /> + + + setFieldValue('startTime', newValue)} + /> + { + setFieldValue('endTime', newValue) + }} + /> + + {targetUser != undefined ? ( + ( + + )} + value={undefined} + // onChange={(e) => { + // setTargetUser(e) + // }} + inputValue={inputValue} + onInputChange={(event, newInputValue) => { + setInputValue(newInputValue) + }} + onChange={(event: any, newValue: labeledUser) => { + console.log({ newValue: newValue }) + setFieldValue('assignedUser', newValue) + }} + /> + ) : null} + + + + + + + + + + + + ) + }} +
+ + ) +} + +export default QuickShiftForm diff --git a/src/sprintFiles/QuickShift/ShiftForm.module.css b/src/sprintFiles/QuickShift/ShiftForm.module.css new file mode 100644 index 0000000..b5924ea --- /dev/null +++ b/src/sprintFiles/QuickShift/ShiftForm.module.css @@ -0,0 +1,46 @@ +.shiftBox { + padding: 5%; +} + +.title { + padding-bottom: 2%; + float: left; + display: block; +} + +.close { + float: right; + padding-bottom: 5%; + display: block; +} + +.line { + width: 95%; +} + +.content { +} + +.formField { + width: 100%; + padding-bottom: 1%; + padding-right: 3%; + float: left; +} + +.flex { + width: 100%; + display: flex; + justify-content: space-between; +} + +.submit { + float: right; + margin-bottom: 10px; + margin-right: 5%; +} + +.clear { + float: left; + margin-bottom: 10px; +} diff --git a/src/sprintFiles/QuickShift/scheduledShiftApiSlice.ts b/src/sprintFiles/QuickShift/scheduledShiftApiSlice.ts new file mode 100644 index 0000000..7095a9e --- /dev/null +++ b/src/sprintFiles/QuickShift/scheduledShiftApiSlice.ts @@ -0,0 +1,132 @@ +import { createSelector, createEntityAdapter, EntityId } from '@reduxjs/toolkit' +import { fbScheduledShift, ScheduledShift } from '../../types/schema' +import { apiSlice } from '../../store/api/apiSlice' +import { RootState } from '../../store/store' +import dayjs from 'dayjs' + +const scheduledShiftsAdapter = createEntityAdapter({}) + +const initialState = scheduledShiftsAdapter.getInitialState() + +export const scheduledShiftsApiSlice = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + getScheduledShifts: builder.query({ + query: (houseId) => ({ + url: `houses/${houseId}/scheduledShifts`, + method: 'GET', + data: { body: 'hello world' }, + params: { queryType: 'scheduledshifts' }, + // validateStatus: (response, result) => { + // console.log('response: ', response, ' -- result: ', result) + // return response.status === 200 && !result.isError + // }, + }), + // keepUnusedDataFor: 60, + //Andrei Note: deal with the conversion of fbSched shift to a JS scheduled shift. + transformResponse: (responseData: fbScheduledShift[]) => { + const loadedShifts = responseData.map((entity) => { + let jsSchedShift: ScheduledShift = { + id: entity.id, + shiftID: entity.shiftID, + date: dayjs(entity.date), + assignedUser: entity.assignedUser, + status: entity.status, + verifiedBy: dayjs(entity.verifiedBy), + verifiedAt: dayjs(entity.verifiedAt), + unverifiedAt: dayjs(entity.unverifiedAt), + penaltyHours: entity.penaltyHours, + jsonCopy: entity.jsonCopy, + } + return jsSchedShift + }) + console.debug(loadedShifts) + return scheduledShiftsAdapter.setAll(initialState, loadedShifts) + }, + providesTags: (result) => { + if (result?.ids) { + return [ + { type: 'ScheduledShift', id: 'LIST' }, + ...result.ids.map((id) => ({ + type: 'ScheduledShift' as const, + id, + })), + ] + } else return [{ type: 'ScheduledShift', id: 'LIST' }] + }, + }), + addNewScheduledShift: builder.mutation({ + query: (data) => { + console.log({ apiData: data }) + console.log({ data: data, datadata: data.data }) + + return { + url: `houses/${data.houseId}/scheduledShifts`, + method: 'POST', + body: { + ...data.data, + }, + } + }, + invalidatesTags: [{ type: 'ScheduledShift', id: 'LIST' }], + }), + updateScheduledShift: builder.mutation({ + // query: (data) => ({ + // url: `houses/${data.houseId}/scheduledShifts/${data.shiftId}`, + // method: 'PATCH', + // body: { + // ...data.data, + // }, + // }), + query: (data) => { + console.log({ apiDataUpdate: data }) + console.log({ data: data, datadata: data.data }) + + return { + url: `houses/${data.houseId}/scheduledShifts/${data.shiftId}`, + method: 'PATCH', + body: { + ...data.data, + }, + } + }, + + invalidatesTags: (result, error, arg) => [ + { type: 'ScheduledShift', id: arg.id }, + ], + }), + // deleteScheduledShifts: builder.mutation({ + // query: (data) => ({ + // url: `houses/${data.houseId}/scheduledShifts/${data.shiftId}`, + // method: 'DELETE', + // }), + // invalidatesTags: (result, error, arg) => [{ type: 'Shift', id: arg.id }], + // }), + }), +}) + +export const { + useGetScheduledShiftsQuery, + useAddNewScheduledShiftMutation, + useUpdateScheduledShiftMutation, + // useDeleteScheduledShiftsMutation, +} = scheduledShiftsApiSlice + +// Creates memoized selector to get normalized state based on the query parameter +const selectScheduledShiftsData = createSelector( + (state: RootState, queryParameter: string) => + scheduledShiftsApiSlice.endpoints.getScheduledShifts.select(queryParameter)( + state + ), + (scheduledShiftsResult) => scheduledShiftsResult.data ?? initialState +) + +// Creates memoized selector to get a shift by its ID based on the query parameter +export const selectScheduledShiftById = (queryParameter: string) => + createSelector( + (state: RootState) => selectScheduledShiftsData(state, queryParameter), + (_: unknown, scheduledShiftId: EntityId) => scheduledShiftId, + ( + data: { entities: { [x: string]: unknown } }, + scheduledShiftId: string | number + ) => data.entities[scheduledShiftId] + ) diff --git a/src/types/schema.ts b/src/types/schema.ts index 7fc1681..a25ab9a 100644 --- a/src/types/schema.ts +++ b/src/types/schema.ts @@ -1,3 +1,5 @@ +import dayjs from 'dayjs' + export type User = { // this id is to help the standard table generalize the id attribute id?: string @@ -80,7 +82,23 @@ export type Shift = { } // TODO: add date, verifiedAt, and unverifiedBy attributes +//TODO : When getting date, verifiedBy, and verifiedAt, unverifiedAt : turn from string->date obj, when setting : turn from date obj -> string. +// Todo: Get: pull the string, then call JSON.parse on it (cast). Set: turn it into a string. export type ScheduledShift = { + id: string + shiftID: string + date: dayjs.Dayjs + assignedUser: string + status: string + verifiedBy: dayjs.Dayjs + verifiedAt: dayjs.Dayjs + unverifiedAt: dayjs.Dayjs + penaltyHours: number + jsonCopy: Shift //TODO : check if this fails +} + +//There's a difference between how objects are stored in Firebase vs in-browser. Look at note on ScheduledShift type for more info. +export type fbScheduledShift = { id: string shiftID: string date: string @@ -91,6 +109,7 @@ export type ScheduledShift = { verifiedAt: string unverifiedAt: string penaltyHours: number + jsonCopy: Shift //TODO : check if this fails } export type House = {