From 17c278bce21bb47cdd7d8a7daa6cbe150fed7bdd Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 19 Mar 2023 11:34:55 +0900 Subject: [PATCH 1/9] update env --- src/env.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/env.json b/src/env.json index 93d1eb8..edd1be7 100644 --- a/src/env.json +++ b/src/env.json @@ -1,3 +1,3 @@ { - "current-env": "production" + "current-env": "development" } From e5c791e797572024799601b5189d7d76dbe159a5 Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 19 Mar 2023 13:52:49 +0900 Subject: [PATCH 2/9] move server interface methods to http folder --- .../request.js => http/serverInterface.ts} | 112 +++++++++++------- 1 file changed, 72 insertions(+), 40 deletions(-) rename src/{hooks/request.js => http/serverInterface.ts} (76%) diff --git a/src/hooks/request.js b/src/http/serverInterface.ts similarity index 76% rename from src/hooks/request.js rename to src/http/serverInterface.ts index 9cc1486..f8e18f3 100644 --- a/src/hooks/request.js +++ b/src/http/serverInterface.ts @@ -1,5 +1,6 @@ -import moment from "moment"; -import { combineDateTimeMoment } from "../util/helpers"; +import moment, { Moment } from "moment"; + +import { combineDateTimeMoment } from "../utils/helpers"; import { db } from "../database/firebase-config"; import { collection, addDoc, Timestamp, getDoc, updateDoc, doc, query, where, getDocs, orderBy, runTransaction, deleteDoc, writeBatch, limit } from "firebase/firestore"; @@ -9,6 +10,19 @@ const settingsCollectionRef = collection(db, "settings"); const API_URL = ""; +interface NewBooking { + email: string; + googleAccountName: string | null; + name: string; + phone: string; + photoURL: string | null; + time: string; + userId: string; + date: Moment; +} + +type BookingStatus = "confirmed" | "blocked" | "cancelled"; + function processBooking(result) { const isDone = moment(result.date.toDate()) < moment(); let bookingData = { @@ -34,15 +48,32 @@ function processSlot(result) { return slotData; } -function makeSlotMoment(momentDate, timeString) { +async function checkUserAlreadyBookedDay(bookingData: NewBooking): Promise { + const startOfDay = new Date(bookingData.date.toDate().setHours(0, 0, 0, 0)); + const endOfDay = new Date(bookingData.date.toDate().setHours(23, 59, 59, 999)); + + const bookingsQuery = query( + bookingsCollectionRef, + where("userId", "==", bookingData.userId), + where("date", ">=", Timestamp.fromMillis(startOfDay.getTime())), + where("date", "<=", Timestamp.fromMillis(endOfDay.getTime())), + where("status", "==", "confirmed") + ); + + const bookingsSnap = await getDocs(bookingsQuery); + + return bookingsSnap.empty; +} + +function makeSlotMoment(momentDate: Moment, timeString) { let timeMoment = moment(timeString, "h:mm a"); let slotMoment = combineDateTimeMoment(momentDate, timeMoment); return slotMoment; } -async function getSlotsForDay(momentDate, status) { - const dateMoment = new moment(momentDate); - let time = new moment().set({ hour: 0, minute: 0, second: 0 }); +async function getSlotsForDay(momentDate: Moment, status = null) { + const dateMoment = moment(momentDate); + let time = moment().set({ hour: 0, minute: 0, second: 0 }); let choosenDate = combineDateTimeMoment(dateMoment, time); @@ -58,7 +89,7 @@ async function getSlotsForDay(momentDate, status) { } //delete bookings -async function deleteRemoteSlot(slotMoment, status) { +export async function deleteRemoteSlot(slotMoment: Moment, status: BookingStatus) { try { const snapQuery = query(slotsCollectionRef, where("date", "==", Timestamp.fromDate(slotMoment.toDate())), where("status", "==", status)); @@ -76,7 +107,7 @@ async function deleteRemoteSlot(slotMoment, status) { } //tells you that the slot you have choosen is not in the db -async function checkSlotNotInDB(slotMoment, status) { +export async function checkSlotNotInDB(slotMoment: Moment, status: BookingStatus) { try { const snapQuery = query( slotsCollectionRef, @@ -87,7 +118,7 @@ async function checkSlotNotInDB(slotMoment, status) { const slotQuerySnap = await getDocs(snapQuery); - return slotQuerySnap.empty; //it empty is true, the slot is not in the db + return slotQuerySnap.empty; //if empty is true, the slot is not in the db } catch (err) { console.log("error in check slot in db", err); throw err; @@ -102,12 +133,12 @@ async function uploadSlot(slotObject) { } //load bookings for given date as json -async function httpGetBooking(id) { +export async function httpGetBooking(id) { try { const bookingRef = doc(db, "bookings", id); - const bookingSnap = await getDoc(bookingRef); + const bookingSnap = await getDoc(bookingRef); if (bookingSnap.exists()) { - let result = { ...bookingSnap.data(), id: bookingSnap.id }; + let result: any = { ...bookingSnap.data(), id: bookingSnap.id }; let bookingData = processBooking(result); return bookingData; @@ -121,13 +152,13 @@ async function httpGetBooking(id) { //get the settings from the db //TODO: get the latest settings from the collection of settings -async function httpGetSettings() { +export async function httpGetSettings() { try { const settingsSnap = await getDocs(settingsCollectionRef); if (settingsSnap) { - let result = settingsSnap.docs[0]; - result = { ...result.data(), id: result.id }; + const currentSettings = settingsSnap.docs[0]; + const result = { ...currentSettings.data(), id: currentSettings.id }; return result; } else { throw new Error(`No default settings found`); @@ -139,7 +170,7 @@ async function httpGetSettings() { //post new settings to the db //TODO: just let him add new setting each time and then we fetch the latest ones , so we can see what changes he made -async function httpSubmitSettings(newSettings) { +export async function httpSubmitSettings(newSettings) { try { // const settingsSnap = await getDocs(settingsCollectionRef) const settingsDoc = doc(db, "settings", newSettings.id); @@ -153,9 +184,9 @@ async function httpSubmitSettings(newSettings) { //returns bookings for an email since yesterday, //TODO: make a new one that returns all bookings for an email every made ahaha -async function httpCheckBooking(phoneNumber) { +export async function httpCheckBooking(phoneNumber) { try { - const yesterdayMoment = new moment().clone().subtract(1, "days"); + const yesterdayMoment = moment().clone().subtract(1, "days"); const q = query(bookingsCollectionRef, where("phone", "==", phoneNumber), where("date", ">", Timestamp.fromMillis(yesterdayMoment.valueOf())), orderBy("date", "desc")); const bookingQuerySnap = await getDocs(q); @@ -176,9 +207,9 @@ async function httpCheckBooking(phoneNumber) { } //load bookings for given date as json -async function httpGetBookings(dateMoment) { +export async function httpGetBookings(dateMoment) { try { - dateMoment = new moment(dateMoment); + dateMoment = moment(dateMoment); let queriedDate = dateMoment.format("YYYY-MM-DD").toString(); let nextDate = dateMoment.add(1, "day").format("YYYY-MM-DD").toString(); let parsedqueriedDate = Date.parse(queriedDate + "T00:00"); @@ -201,7 +232,7 @@ async function httpGetBookings(dateMoment) { } //load already booked time slots for given date as json -async function httpGetSlots(dateMoment) { +export async function httpGetSlots(dateMoment) { try { const slotSnap = await getSlotsForDay(dateMoment); if (slotSnap) { @@ -218,7 +249,7 @@ async function httpGetSlots(dateMoment) { //TODO: //submit an array of slot for a given day to block and if some slots are not in that day then make sure to unblock them!!! -async function httpSubmitBlockedSlots(momentDate, localTimesArray) { +export async function httpSubmitBlockedSlots(momentDate: Moment, localTimesArray) { try { return runTransaction(db, async (transaction) => { const batch = writeBatch(db); //TODO: use this to delete all and write all slots @@ -237,7 +268,8 @@ async function httpSubmitBlockedSlots(momentDate, localTimesArray) { //TODO: upload new blocked slots... for (let localSlotTime of localTimesArray) { const slotMoment = makeSlotMoment(momentDate, localSlotTime); - if (checkSlotNotInDB(slotMoment, "confirmed")) { + const isNotBookedOrBlockedSlot = await checkSlotNotInDB(slotMoment, "confirmed"); + if (isNotBookedOrBlockedSlot) { //make sure the slot is not already taken const slot = { date: Timestamp.fromDate(slotMoment.toDate()), @@ -272,19 +304,26 @@ async function httpSubmitBlockedSlots(momentDate, localTimesArray) { } //submit a new booking to the system -async function httpSubmitBooking(bookingData) { +export async function httpSubmitBooking(bookingData) { try { return await runTransaction(db, async (transaction) => { let bookingTimeMoment = makeSlotMoment(bookingData.date, bookingData.time); + const isNotBookedOrBlockedSlot = await checkSlotNotInDB(bookingTimeMoment, "confirmed"); + + // const isUserBlocked = await checkUserBlocked(bookingData.phone); + + const hasNotAlreadyBookedOnDay = await checkUserAlreadyBookedDay(bookingData); - if (checkSlotNotInDB(bookingTimeMoment, "confirmed")) { + if (!hasNotAlreadyBookedOnDay) { + throw new Error("Looks like you have already booked today :)."); + } + + if (isNotBookedOrBlockedSlot) { const slot = { date: Timestamp.fromDate(bookingTimeMoment.toDate()), status: "confirmed", }; - //alert(JSON.stringify({ ...bookingData, ...slot })); - const response = await addDoc(bookingsCollectionRef, { ...bookingData, ...slot, @@ -304,7 +343,7 @@ async function httpSubmitBooking(bookingData) { } //edit a booking, //NOTE: note being used yet -async function httpEditBooking(booking) { +export async function httpEditBooking(booking) { try { const response = await fetch(`${API_URL}/bookings`, { method: "patch", @@ -319,7 +358,7 @@ async function httpEditBooking(booking) { } } -async function httpCancelBooking(id) { +export async function httpCancelBooking(id) { try { return await runTransaction(db, async (transaction) => { //get reference to booking to be deleted @@ -327,6 +366,10 @@ async function httpCancelBooking(id) { let bookingSnap = await getDoc(bookingDocRef); + if (!bookingSnap || !bookingSnap.exists()) { + throw new Error("The booking you are attempting to cancel was not found!"); + } + let firebaseTimeStamp = bookingSnap.data().date; await deleteRemoteSlot(firebaseTimeStamp, "confirmed"); @@ -338,15 +381,6 @@ async function httpCancelBooking(id) { bookingSnap = await getDoc(bookingDocRef); - if (bookingSnap.exists()) { - //delete slot for this booking from blocked and confirmed slot collection - // let firebaseTimeStamp = bookingSnap.data().date; - // await deleteRemoteSlot(firebaseTimeStamp, "confirmed"); - } else { - //TODO: stop transaction.... - throw new Error("The booking you are attempting to cancel was not found!"); - } - return bookingSnap.data(); }); } catch (err) { @@ -354,5 +388,3 @@ async function httpCancelBooking(id) { throw err; } } - -export { httpGetBooking, httpGetBookings, httpGetSlots, httpSubmitBooking, httpEditBooking, httpCancelBooking, httpCheckBooking, httpGetSettings, httpSubmitSettings, httpSubmitBlockedSlots }; From ad679b44806c3257a08a3d5e27a95f38b814dfd1 Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 19 Mar 2023 13:53:27 +0900 Subject: [PATCH 3/9] prevent making many appointments on same day --- src/components/BookingForm/BookingForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BookingForm/BookingForm.js b/src/components/BookingForm/BookingForm.js index c845318..bdcb229 100644 --- a/src/components/BookingForm/BookingForm.js +++ b/src/components/BookingForm/BookingForm.js @@ -41,7 +41,7 @@ const BookingForm = ({ onCancel, onConfirm, oldData, slots, onGetSlots, slotStat authCtx.handleCustomerLogin(); return; } - onConfirm({ ...values, userId: authCtx.userId, googleAccountName: authCtx.user.displayName, email: authCtx.user.email, photoURL: authCtx.user.photoURL }); + onConfirm({ ...values, userId: authCtx.userId, googleAccountName: authCtx.user.displayName, email: authCtx.user.email, photoURL: authCtx.user.photoURL, createdTime: new Date() }); resetForm(); }} initialValues={{ From c2e1eaad1fa0f672d6aaa7829eb8fa572b57f357 Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 19 Mar 2023 13:54:20 +0900 Subject: [PATCH 4/9] rename util folder to utils --- src/components/Appointment/Appointment.js | 2 +- src/components/Booking/Booking.js | 2 +- src/components/Settings/GeneralSettings.js | 181 ++++++-------------- src/components/SideBar/SideBar.js | 2 +- src/components/TimeSelector/TimeSelector.js | 2 +- src/{util => utils}/data.js | 0 src/{util => utils}/helpers.js | 2 +- 7 files changed, 60 insertions(+), 131 deletions(-) rename src/{util => utils}/data.js (100%) rename src/{util => utils}/helpers.js (96%) diff --git a/src/components/Appointment/Appointment.js b/src/components/Appointment/Appointment.js index 1729a23..7b8e7b0 100644 --- a/src/components/Appointment/Appointment.js +++ b/src/components/Appointment/Appointment.js @@ -2,7 +2,7 @@ import React from "react"; import Button from "react-bootstrap/Button"; import Badge from "react-bootstrap/Badge"; import useModal from "../../hooks/useModal"; -import { setStatus } from "../../util/helpers"; +import { setStatus } from "../../utils/helpers"; import { Card } from "antd"; import "./Appointment.css"; diff --git a/src/components/Booking/Booking.js b/src/components/Booking/Booking.js index 38ed7cb..1fac3c3 100644 --- a/src/components/Booking/Booking.js +++ b/src/components/Booking/Booking.js @@ -2,7 +2,7 @@ import React from "react"; import Button from "react-bootstrap/esm/Button"; import Badge from "react-bootstrap/Badge"; import "./Booking.css"; -import { setStatus } from "../../util/helpers"; +import { setStatus } from "../../utils/helpers"; const Booking = ({ booking, onView }) => { // console.log("booking", booking); diff --git a/src/components/Settings/GeneralSettings.js b/src/components/Settings/GeneralSettings.js index 23c8854..4c3b122 100644 --- a/src/components/Settings/GeneralSettings.js +++ b/src/components/Settings/GeneralSettings.js @@ -1,17 +1,17 @@ -import React, { useState } from "react" -import { Button } from "react-bootstrap" -import ClockPicker from "../ClockPicker/ClockPicker" -import { useFormik } from "formik" +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; +import ClockPicker from "../ClockPicker/ClockPicker"; +import { useFormik } from "formik"; -import * as yup from "yup" -import { compareTimes } from "../../util/helpers" -import "./GeneralSettings.css" +import * as yup from "yup"; +import { compareTimes } from "../../utils/helpers"; +import "./GeneralSettings.css"; const GeneralSettings = ({ onConfirm, onBack, initialValues: defaults }) => { - const [pickerTime, setPickerTime] = useState(null) - const [showPicker, setShowPicker] = useState(false) - const [pickerFunction, setPickerFunction] = useState(() => () => {}) - const [editMode, setEditMode] = useState(false) + const [pickerTime, setPickerTime] = useState(null); + const [showPicker, setShowPicker] = useState(false); + const [pickerFunction, setPickerFunction] = useState(() => () => {}); + const [editMode, setEditMode] = useState(false); const formik = useFormik({ initialValues: { @@ -21,75 +21,62 @@ const GeneralSettings = ({ onConfirm, onBack, initialValues: defaults }) => { validationSchema: yup.object().shape({ startTime: yup.string().required("Starting time is required!"), endTime: yup.string().required("Ending time is required!"), - slotSize: yup - .number() - .min(0, "Minutes must be positive numbers") - .max(120, "Max slot size is 120 minutes") - .required("*Slot size is required!"), - address: yup - .string() - .min(8, "address is Too Short!") - .max(50, "address is Too Long!") - .required("*Address is required!"), + slotSize: yup.number().min(0, "Minutes must be positive numbers").max(120, "Max slot size is 120 minutes").required("*Slot size is required!"), + address: yup.string().min(8, "address is Too Short!").max(50, "address is Too Long!").required("*Address is required!"), }), onSubmit: (values) => { if (validateTime()) { // console.log("time is valid") // alert(JSON.stringify(values, null, 2)) - setEditMode(false) - onConfirm({ ...values }) + setEditMode(false); + onConfirm({ ...values }); } }, - }) + }); const validateTime = () => { - const isValid = compareTimes(formik.values.startTime, formik.values.endTime) + const isValid = compareTimes(formik.values.startTime, formik.values.endTime); if (!isValid) { - formik.setFieldError("startTime", "Start time must be before end time") - formik.setFieldError("endTime", "Start time must be before end time") + formik.setFieldError("startTime", "Start time must be before end time"); + formik.setFieldError("endTime", "Start time must be before end time"); } else { - formik.setFieldError("startTime", "") - formik.setFieldError("endTime", "") + formik.setFieldError("startTime", ""); + formik.setFieldError("endTime", ""); } - return isValid - } + return isValid; + }; const handleChangeStartTime = (value) => { - formik.setFieldValue("startTime", value) - formik.setFieldTouched("startTime") - } + formik.setFieldValue("startTime", value); + formik.setFieldTouched("startTime"); + }; const handleChangeEndTime = (value) => { - formik.setFieldValue("endTime", value) - formik.setFieldTouched("endTime") - } + formik.setFieldValue("endTime", value); + formik.setFieldTouched("endTime"); + }; const handleUsePicker = (setValueFunction, initalValue = null) => { - setPickerTime(initalValue) - setPickerFunction(() => setValueFunction) - setShowPicker(true) - } + setPickerTime(initalValue); + setPickerFunction(() => setValueFunction); + setShowPicker(true); + }; const handleClosePicker = () => { - setPickerFunction(() => () => {}) - setPickerTime(null) - setShowPicker(false) - } + setPickerFunction(() => () => {}); + setPickerTime(null); + setShowPicker(false); + }; const handleGoBack = () => { //TODO: findout why this needs to be called twice // onBack() - onBack() - } + onBack(); + }; return ( <> - +

General Shop Settings

@@ -97,28 +84,16 @@ const GeneralSettings = ({ onConfirm, onBack, initialValues: defaults }) => {
{editMode ? ( <> - - ) : ( <> -
- {formik.touched.startTime && formik.errors.startTime && ( - - {formik.errors.startTime} - - )} + {formik.touched.startTime && formik.errors.startTime && {formik.errors.startTime}}
@@ -165,21 +128,12 @@ const GeneralSettings = ({ onConfirm, onBack, initialValues: defaults }) => {
{formik.values.endTime} {editMode && ( - - handleUsePicker(handleChangeEndTime, formik.values.endTime) - } - > + handleUsePicker(handleChangeEndTime, formik.values.endTime)}> Change )}
- {formik.touched.endTime && formik.errors.endTime && ( - - {formik.errors.endTime} - - )} + {formik.touched.endTime && formik.errors.endTime && {formik.errors.endTime}}
@@ -188,56 +142,31 @@ const GeneralSettings = ({ onConfirm, onBack, initialValues: defaults }) => {
{editMode ? ( - + ) : ( {formik.values.slotSize} )} Minutes
- {formik.touched.slotSize && formik.errors.slotSize && ( - - {formik.errors.slotSize} - - )} + {formik.touched.slotSize && formik.errors.slotSize && {formik.errors.slotSize}}
-