From 8f5690fd980ff6d9d7ec3d0be7e3f49b3261de60 Mon Sep 17 00:00:00 2001 From: Murphy Date: Thu, 25 Jul 2024 20:40:53 +0800 Subject: [PATCH 01/11] update package.json adding react-datepicker library for calendar functionality --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index 96367f2f..81a2d6ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.0", "react-bootstrap": "^1.6.1", + "react-datepicker": "^7.3.0", "react-dom": "^18.2.0", "react-redux": "^7.2.1", "react-router-dom": "^6.3.0", From 8c9a5d94d25eb7951b58d5db726aedc85c492282 Mon Sep 17 00:00:00 2001 From: realtmxi Date: Thu, 1 Aug 2024 20:11:39 +0800 Subject: [PATCH 02/11] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index feb5266a..2d79e064 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ backend/yarn.lock node_modules/.yarn-integrity .vscode/ docs/ +backend/ATT75011.env \ No newline at end of file From 5c963da122555b8f571d9066f61e687fc67eda00 Mon Sep 17 00:00:00 2001 From: realtmxi Date: Tue, 6 Aug 2024 20:56:26 +0800 Subject: [PATCH 03/11] Update package.json --- frontend/package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 81a2d6ac..63167093 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "@mui/material": "^5.8.0", "@mui/styles": "^5.8.0", "@mui/x-date-pickers": "^5.0.0-alpha.3", + "bootstrap": "^5.3.3", "cross-env": "^7.0.3", "dagre": "^0.8.5", "date-fns": "^2.23.0", @@ -18,13 +19,16 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.0", "react-bootstrap": "^1.6.1", + "react-date-picker": "^11.0.0", "react-datepicker": "^7.3.0", "react-dom": "^18.2.0", "react-redux": "^7.2.1", "react-router-dom": "^6.3.0", "react-star-rating-component": "^1.4.1", "reactflow": "^11.8.3", - "redux": "^4.0.5" + "reactstrap": "^9.2.2", + "redux": "^4.0.5", + "styled-components": "^6.1.12" }, "devDependencies": { "react-scripts": "5.0.1", From 6ffd44d78817f0670155ff4a87f45c0e012f0af9 Mon Sep 17 00:00:00 2001 From: realtmxi Date: Tue, 6 Aug 2024 20:57:03 +0800 Subject: [PATCH 04/11] create calendar and related components --- frontend/src/components/calendar/calendar.js | 199 +++++++++++++++ frontend/src/helpers/calendarHelper.js | 163 +++++++++++++ frontend/src/routes.js | 3 + frontend/src/styles/calendarStyles.js | 241 +++++++++++++++++++ 4 files changed, 606 insertions(+) create mode 100644 frontend/src/components/calendar/calendar.js create mode 100644 frontend/src/helpers/calendarHelper.js create mode 100644 frontend/src/styles/calendarStyles.js diff --git a/frontend/src/components/calendar/calendar.js b/frontend/src/components/calendar/calendar.js new file mode 100644 index 00000000..06a483e3 --- /dev/null +++ b/frontend/src/components/calendar/calendar.js @@ -0,0 +1,199 @@ +import React, { useState, useEffect, Fragment } from "react"; +import PropTypes from "prop-types"; +import * as Styled from "../../styles/calendarStyles"; +import calendar, { + isDate, + isSameDay, + isSameMonth, + getDateISO, + getNextMonth, + getPreviousMonth, + WEEK_DAYS, + CALENDAR_MONTHS, +} from "../../helpers/calendarHelper"; +import "bootstrap/dist/css/bootstrap.min.css"; + +export default function Calendar({ date, onDateChanged }) { + const [dateState, setDateState] = useState({ + current: new Date(), + month: new Date().getMonth() + 1, + year: new Date().getFullYear(), + }); + + const [today, setToday] = useState(new Date()); + + useEffect(() => { + addDateToState(date); + }, []); + + const addDateToState = (date) => { + const isDateObject = isDate(date); + const _date = isDateObject ? date : new Date(); + setDateState({ + current: isDateObject ? date : null, + month: _date.getMonth() + 1, + year: _date.getFullYear(), + }); + }; + + const getCalendarDates = () => { + const { current, month, year } = dateState; + const calendarMonth = month || (current ? current.getMonth() + 1 : THIS_MONTH); + const calendarYear = year || (current ? current.getFullYear() : THIS_YEAR); + return calendar(calendarMonth, calendarYear); + }; + + const renderMonthAndYear = () => { + const { month, year } = dateState; + const formatter = new Intl.DateTimeFormat("zh-CN", { + day: "numeric", + month: "short", + year: "numeric", + }); + const formattedDate = formatter.format(dateState.current); + + // Resolve the month name from the CALENDAR_MONTHS object map + const monthname = Object.keys(CALENDAR_MONTHS)[Math.max(0, Math.min(month - 1, 11))]; + return ( + + + + {monthname} {year} + + + + ); + }; + + const renderDayLabel = (day, index) => { + // Resolve the day of the week label + const daylabel = WEEK_DAYS[day].toUpperCase(); + return ( + + {daylabel} + + ); + }; + + const renderCalendarDate = (date, index) => { + const { current, month, year } = dateState; + const _date = new Date(date.join("-")); + + // Check if calendar date is same day as today + const isToday = isSameDay(_date, today); + // Check if calendar date is same day as currently selected date + const isCurrent = current && isSameDay(_date, current); + // Check if calendar date is in the same month as the state month and year + const inMonth = + month && year && isSameMonth(_date, new Date([year, month, 1].join("-"))); + // The click handler + const onClick = gotoDate(_date); + const props = { index, inMonth, onClick, title: _date.toDateString() }; + + const DateComponent = isCurrent + ? Styled.HighlightedCalendarDate + : isToday + ? Styled.TodayCalendarDate + : Styled.CalendarDate; + + return ( + + {_date.getDate()} + + ); + }; + + const gotoDate = (date) => (evt) => { + evt && evt.preventDefault(); + const { current } = dateState; + if (!(current && isSameDay(date, current))) { + addDateToState(date); + onDateChanged(date); + } + }; + + const gotoPreviousMonth = () => { + const { month, year } = dateState; + const previousMonth = getPreviousMonth(month, year); + setDateState({ + month: previousMonth.month, + year: previousMonth.year, + current: dateState.current, + }); + }; + + const gotoNextMonth = () => { + const { month, year } = dateState; + const nextMonth = getNextMonth(month, year); + setDateState({ + month: nextMonth.month, + year: nextMonth.year, + current: dateState.current, + }); + }; + + const handlePrevious = (evt) => { + gotoPreviousMonth(); + }; + + const handleNext = (evt) => { + gotoNextMonth(); + }; + + return ( + + {renderMonthAndYear()} + + + + {Object.keys(WEEK_DAYS).map(renderDayLabel)} + + + {getCalendarDates().map(renderCalendarDate)} + + + + ); +} + +Calendar.propTypes = { + date: PropTypes.instanceOf(Date), + onDateChanged: PropTypes.func, +}; + + + + +const gotoDate = (date) => (evt) => { + evt && evt.preventDefault(); + const { current } = dateState; + if (!(current && isSameDay(date, current))) { + addDateToState(date); + onDateChanged(date); + } + }; + const gotoPreviousMonth = () => { + const { month, year } = dateState; + const previousMonth = getPreviousMonth(month, year); + setDateState({ + month: previousMonth.month, + year: previousMonth.year, + current: dateState.current, + }); + }; + const gotoNextMonth = () => { + const { month, year } = dateState; + const nextMonth = getNextMonth(month, year); + setDateState({ + month: nextMonth.month, + year: nextMonth.year, + current: dateState.current, + }); + }; + const handlePrevious = (evt) => { + gotoPreviousMonth(); + }; + const handleNext = (evt) => { + gotoNextMonth(); + }; + diff --git a/frontend/src/helpers/calendarHelper.js b/frontend/src/helpers/calendarHelper.js new file mode 100644 index 00000000..991401b6 --- /dev/null +++ b/frontend/src/helpers/calendarHelper.js @@ -0,0 +1,163 @@ +// Render the custom calendar, prviding functionality for date selection +import "bootstrap/dist/css/bootstrap.min.css"; + +// (int) The current year +export const THIS_YEAR = +(new Date().getFullYear()); + +// (int) The current month starting from 1 - 12 +// 1 => January, 12 => December +export const THIS_MONTH = +(new Date().getMonth()) + 1; + +// Week days names and shortnames +export const WEEK_DAYS = { + Sunday: "Sun", + Monday: "Mon", + Tuesday: "Tue", + Wednesday: "Wed", + Thursday: "Thu", + Friday: "Fri", + Saturday: "Sat" +} + +// Calendar months names and short names +export const CALENDAR_MONTHS = { + January: "Jan", + February: "Feb", + March: "Mar", + April: "Apr", + May: "May", + June: "Jun", + July: "Jul", + August: "Aug", + September: "Sep", + October: "Oct", + November: "Nov", + December: "Dec" +} + +// Weeks displayed on calendar +export const CALENDAR_WEEKS = 6; + +// Pads a string value with leading zeroes(0) until length is reached +// For example: zeroPad(5, 2) => "05" +export const zeroPad = (value, length) => { + return `${value}`.padStart(length, '0'); +} + +// (int) Number days in a month for a given year from 28 - 31 +export const getMonthDays = (month = THIS_MONTH, year = THIS_YEAR) => { + const months30 = [4, 6, 9, 11]; + const leapYear = year % 4 === 0; + return month === 2 + ? leapYear + ? 29 + : 28 + : months30.includes(month) + ? 30 + : 31; +} + +// (int) First day of the month for a given year from 1 - 7 +// 1 => Sunday, 7 => Saturday +export const getMonthFirstDay = (month = THIS_MONTH, year = THIS_YEAR) => { + return +(new Date(`${year}-${zeroPad(month, 2)}-01`).getDay()) + 1; +} + +// (bool) Checks if a value is a date - this is just a simple check +export const isDate = date => { + const isDate = Object.prototype.toString.call(date) === '[object Date]'; + const isValidDate = date && !Number.isNaN(date.valueOf()); + + return isDate && isValidDate; + } + + // (bool) Checks if two date values are of the same month and year + export const isSameMonth = (date, basedate = new Date()) => { + if (!(isDate(date) && isDate(basedate))) return false; + const basedateMonth = +(basedate.getMonth()) + 1; + const basedateYear = basedate.getFullYear(); + const dateMonth = +(date.getMonth()) + 1; + const dateYear = date.getFullYear(); + return (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear); + } + + // (bool) Checks if two date values are the same day + export const isSameDay = (date, basedate = new Date()) => { + if (!(isDate(date) && isDate(basedate))) return false; + const basedateDate = basedate.getDate(); + const basedateMonth = +(basedate.getMonth()) + 1; + const basedateYear = basedate.getFullYear(); + const dateDate = date.getDate(); + const dateMonth = +(date.getMonth()) + 1; + const dateYear = date.getFullYear(); + return (+basedateDate === +dateDate) && (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear); + } + + // (string) Formats the given date as YYYY-MM-DD + // Months and Days are zero padded + export const getDateISO = (date = new Date) => { + if (!isDate(date)) return null; + return [ + date.getFullYear(), + zeroPad(+date.getMonth() + 1, 2), + zeroPad(+date.getDate(), 2) + ].join('-'); + } + + // ({month, year}) Gets the month and year before the given month and year + // For example: getPreviousMonth(1, 2000) => {month: 12, year: 1999} + // while: getPreviousMonth(12, 2000) => {month: 11, year: 2000} + export const getPreviousMonth = (month, year) => { + const prevMonth = (month > 1) ? month - 1 : 12; + const prevMonthYear = (month > 1) ? year : year - 1; + return { month: prevMonth, year: prevMonthYear }; + } + + // ({month, year}) Gets the month and year after the given month and year + // For example: getNextMonth(1, 2000) => {month: 2, year: 2000} + // while: getNextMonth(12, 2000) => {month: 1, year: 2001} + export const getNextMonth = (month, year) => { + const nextMonth = (month < 12) ? month + 1 : 1; + const nextMonthYear = (month < 12) ? year : year + 1; + return { month: nextMonth, year: nextMonthYear }; + } + + // Calendar builder for a month in the specified year +// Returns an array of the calendar dates. +// Each calendar date is represented as an array => [YYYY, MM, DD] +export default (month = THIS_MONTH, year = THIS_YEAR) => { + // Get number of days in the month and the month's first day + const monthDays = getMonthDays(month, year); + const monthFirstDay = getMonthFirstDay(month, year); + // Get number of days to be displayed from previous and next months + // These ensure a total of 42 days (6 weeks) displayed on the calendar + + const daysFromPrevMonth = monthFirstDay - 1; + const daysFromNextMonth = (CALENDAR_WEEKS * 7) - (daysFromPrevMonth + monthDays); + // Get the previous and next months and years + + const { month: prevMonth, year: prevMonthYear } = getPreviousMonth(month, year); + const { month: nextMonth, year: nextMonthYear } = getNextMonth(month, year); + // Get number of days in previous month + const prevMonthDays = getMonthDays(prevMonth, prevMonthYear); + // Builds dates to be displayed from previous month + + const prevMonthDates = [...new Array(daysFromPrevMonth)].map((n, index) => { + const day = index + 1 + (prevMonthDays - daysFromPrevMonth); + return [ prevMonthYear, zeroPad(prevMonth, 2), zeroPad(day, 2) ]; + }); + // Builds dates to be displayed from current month + + const thisMonthDates = [...new Array(monthDays)].map((n, index) => { + const day = index + 1; + return [year, zeroPad(month, 2), zeroPad(day, 2)]; + }); + // Builds dates to be displayed from next month + + const nextMonthDates = [...new Array(daysFromNextMonth)].map((n, index) => { + const day = index + 1; + return [nextMonthYear, zeroPad(nextMonth, 2), zeroPad(day, 2)]; + }); + // Combines all dates from previous, current and next months + return [ ...prevMonthDates, ...thisMonthDates, ...nextMonthDates ]; + } \ No newline at end of file diff --git a/frontend/src/routes.js b/frontend/src/routes.js index f2035445..66882ed3 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -91,6 +91,7 @@ import VisualizeClientAssessment from "./components/clientAssessment/VisualizeCl import VisualizeReferral from "./components/referrals/VisualizeReferral"; import Matching from "./components/clients/Matching"; import VisualizeNotification from './components/notifications/visualizeNotification'; +import Calendar from "./components/calendar/calendar"; const routes = ( @@ -239,6 +240,8 @@ const routes = ( }/> }/> + }/> + }/> }/> diff --git a/frontend/src/styles/calendarStyles.js b/frontend/src/styles/calendarStyles.js new file mode 100644 index 00000000..7bd51118 --- /dev/null +++ b/frontend/src/styles/calendarStyles.js @@ -0,0 +1,241 @@ +import styled from "styled-components"; +import "bootstrap/dist/css/bootstrap.min.css"; + +export const Arrow = styled.button` + appearance: none; + user-select: none; + outline: none !important; + display: inline-block; + position: relative; + cursor: pointer; + padding: 0; + border: none; + border-top: 1.6em solid transparent; + border-bottom: 1.6em solid transparent; + transition: all 0.25s ease-out; +`; + +export const ArrowLeft = styled(Arrow)` + border-right: 2.4em solid #ccc; + left: 1.5rem; + :hover { + border-right-color: #06c; + } +`; + +export const ArrowRight = styled(Arrow)` + border-left: 2.4em solid #ccc; + right: 1.5rem; + :hover { + border-left-color: #06c; + } +`; + +export const CalendarContainer = styled.div` + font-size: 5px; + border: 2px solid #06c; + border-radius: 5px; + overflow: hidden; +`; + +export const CalendarHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +export const CalendarGrid = styled.div` + display: grid; + grid-template: repeat(7, auto) / repeat(7, auto); +`; + +export const CalendarMonth = styled.div` + font-weight: 500; + font-size: 5em; + color: #06c; + text-align: center; + padding: 0.5em 0.25em; + word-spacing: 5px; + user-select: none; +`; + +export const CalendarCell = styled.div` + text-align: center; + align-self: center; + letter-spacing: 0.1rem; + padding: 0.6em 0.25em; + user-select: none; + grid-column: ${(props) => (props.index % 7) + 1} / span 1; +`; + +export const CalendarDay = styled(CalendarCell)` + font-weight: 600; + font-size: 2.25em; + color: #06c; + border-top: 2px solid #06c; + border-bottom: 2px solid #06c; + border-right: ${(props) => + (props.index % 7) + 1 === 7 ? `none` : `2px solid #06c`}; +`; + +export const CalendarDate = styled(CalendarCell)` + font-weight: ${(props) => (props.inMonth ? 500 : 300)}; + font-size: 4em; + cursor: pointer; + border-bottom: ${(props) => + (props.index + 1) / 7 <= 5 ? `1px solid #ddd` : `none`}; + border-right: ${(props) => + (props.index % 7) + 1 === 7 ? `none` : `1px solid #ddd`}; + color: ${(props) => (props.inMonth ? `#333` : `#ddd`)}; + grid-row: ${(props) => Math.floor(props.index / 7) + 2} / span 1; + transition: all 0.4s ease-out; + :hover { + color: #06c; + background: rgba(0, 102, 204, 0.075); + } +`; + +export const HighlightedCalendarDate = styled(CalendarDate)` + color: #fff !important; + background: #06c !important; + position: relative; + ::before { + content: ""; + position: absolute; + top: -1px; + left: -1px; + width: calc(100% + 2px); + height: calc(100% + 2px); + border: 2px solid #06c; + } +`; + +export const TodayCalendarDate = styled(HighlightedCalendarDate)` + color: #06c !important; + background: transparent !important; + ::after { + content: ""; + position: absolute; + right: 0; + bottom: 0; + border-bottom: 0.75em solid #06c; + border-left: 0.75em solid transparent; + border-top: 0.75em solid transparent; + } + :hover { + color: #06c !important; + background: rgba(0, 102, 204, 0.075) !important; + } +`; + +export const BlockedCalendarDate = styled(CalendarDate)` + color: black !important; + background: gray !important; + position: relative; + :hover { + color: black !important; + background: gray !important; + border-color:gray; + cursor:default; + } +`; + +import { + FormGroup, + Label, + Input, + Button, + Dropdown, + DropdownToggle, + DropdownMenu, + } from "reactstrap"; + + export const DatePickerContainer = styled.div` + position: relative; + `; + + export const DatePickerFormGroup = styled(FormGroup)` + display: flex; + justify-content: space-between; + position: relative; + width: 100%; + border: 2px solid #06c; + border-radius: 5px; + overflow: hidden; + `; + + export const DatePickerLabel = styled(Label)` + margin: 0; + padding: 0 2rem; + font-weight: 600; + font-size: 0.7rem; + letter-spacing: 2px; + text-transform: uppercase; + color: #06c; + border-right: 2px solid #06c; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 102, 204, 0.05); + `; + + export const DatePickerInput = styled(Input)` + font-weight: 500; + font-size: 1rem; + color: #333; + box-shadow: none; + border: none; + text-align: center; + letter-spacing: 1px; + background: transparent !important; + display: flex; + align-items: center; + ::placeholder { + color: #999; + font-size: 0.9rem; + } + width:100%; + height:100%; + `; + + export const DatePickerDropdown = styled(Dropdown)` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + `; + + export const DatePickerDropdownToggle = styled(DropdownToggle)` + position: relative; + width: 100%; + height: 100%; + background: transparent; + opacity: 0; + filter: alpha(opacity=0); + `; + + export const DatePickerDropdownMenu = styled(DropdownMenu)` + margin-top: 3rem; + left: 0; + width: 100%; + height: 75vh !important; + border: none; + padding: 0; + transform: none !important; + `; + + export const DatePickerButton = styled(Button)` + position: absolute; + border: 2px solid #06c; + margin-top:2%; + right:50% !important; + background: transparent; + font-size: 1.2rem; + color: #06c; + :hover { + border: white solid #06c; + color: white !important; + background: #06c; + } + `; \ No newline at end of file From 27d11d53ca86181d8cdf9070a7ca135e76cfdc1c Mon Sep 17 00:00:00 2001 From: realtmxi Date: Sat, 10 Aug 2024 10:06:19 +0800 Subject: [PATCH 05/11] Display Appointment in Calendar Page --- frontend/src/components/calendar/calendar.js | 274 ++++++++++++------- frontend/src/components/layouts/TopNavbar.js | 7 + 2 files changed, 187 insertions(+), 94 deletions(-) diff --git a/frontend/src/components/calendar/calendar.js b/frontend/src/components/calendar/calendar.js index 06a483e3..6d514b7f 100644 --- a/frontend/src/components/calendar/calendar.js +++ b/frontend/src/components/calendar/calendar.js @@ -12,6 +12,8 @@ import calendar, { CALENDAR_MONTHS, } from "../../helpers/calendarHelper"; import "bootstrap/dist/css/bootstrap.min.css"; +import { fetchMultipleGeneric, deleteSingleGeneric, updateSingleGeneric } from "../../api/genericDataApi"; +import { Link } from "../shared" export default function Calendar({ date, onDateChanged }) { const [dateState, setDateState] = useState({ @@ -20,11 +22,15 @@ export default function Calendar({ date, onDateChanged }) { year: new Date().getFullYear(), }); - const [today, setToday] = useState(new Date()); + const [today] = useState(new Date()); + const [appointments, setAppointments] = useState([]); + const [selectedDateAppointments, setSelectedDateAppointments] = useState([]); + const [editingAppointment, setEditingAppointment] = useState(null); useEffect(() => { addDateToState(date); - }, []); + fetchAppointments(); + }, [date]); const addDateToState = (date) => { const isDateObject = isDate(date); @@ -36,37 +42,145 @@ export default function Calendar({ date, onDateChanged }) { }); }; + const fetchAppointments = async () => { + try { + const response = await fetchMultipleGeneric('appointment'); + console.log("Raw appointment data:", response.data); + + const appointmentData = response.data.map(appointment => { + let datetime, dateType; + const processedAppointment = { ...appointment, characteristicOccurrences: {} }; + + if (appointment.characteristicOccurrences) { + for (const occ of appointment.characteristicOccurrences) { + processedAppointment.characteristicOccurrences[occ.occurrenceOf?.name] = occ.dataStringValue || occ.dataDateValue || occ.objectValue; + + if (occ.occurrenceOf?.name === 'Date and Time') { + datetime = occ.dataDateValue; + dateType = 'DateTime'; + } else if (occ.occurrenceOf?.name === 'Date') { + datetime = occ.dataDateValue; + dateType = 'Date'; + } + } + } + + const parsedDate = parseDate(datetime); + console.log(`Parsed date for appointment ${appointment._id}:`, parsedDate); + + return { + ...processedAppointment, + date: parsedDate, + dateType + }; + }); + + console.log("Processed appointment data:", appointmentData); + + setAppointments(appointmentData.filter(app => app.date !== null)); + } catch (error) { + console.error("Error fetching appointments:", error); + } + }; + + const handleEdit = (appointment) => { + setEditingAppointment(appointment); + }; + + const handleSave = async (updatedAppointment) => { + try { + await updateSingleGeneric('appointment', updatedAppointment._id, updatedAppointment); + setEditingAppointment(null); + fetchAppointments(); + } catch (error) { + console.error("Error updating appointment:", error); + } + }; + + const handleDelete = async (appointmentId) => { + if (window.confirm("Are you sure you want to delete this appointment?")) { + try { + await deleteSingleGeneric('appointment', appointmentId); + fetchAppointments(); + } catch (error) { + console.error("Error deleting appointment:", error); + } + } + }; + + const parseDate = (dateString) => { + if (!dateString) return null; + + const parsedDate = new Date(dateString); + + if (isNaN(parsedDate.getTime())) { + console.error(`Failed to parse date: ${dateString}`); + return null; + } + + return parsedDate; + }; + const getCalendarDates = () => { const { current, month, year } = dateState; - const calendarMonth = month || (current ? current.getMonth() + 1 : THIS_MONTH); - const calendarYear = year || (current ? current.getFullYear() : THIS_YEAR); + const calendarMonth = month || (current ? current.getMonth() + 1 : today.getMonth() + 1); + const calendarYear = year || (current ? current.getFullYear() : today.getFullYear()); return calendar(calendarMonth, calendarYear); }; - const renderMonthAndYear = () => { + const gotoDate = (date) => { + const { current } = dateState; + if (!(current && isSameDay(date, current))) { + setDateState(prevState => ({ + ...prevState, + current: date, + month: date.getMonth() + 1, + year: date.getFullYear() + })); + if (typeof onDateChanged === 'function') { + onDateChanged(date); + } + + const selectedAppointments = appointments.filter(app => isSameDay(app.date, date)); + setSelectedDateAppointments(selectedAppointments); + } + }; + + const gotoPreviousMonth = () => { const { month, year } = dateState; - const formatter = new Intl.DateTimeFormat("zh-CN", { - day: "numeric", - month: "short", - year: "numeric", - }); - const formattedDate = formatter.format(dateState.current); + const previousMonth = getPreviousMonth(month, year); + setDateState(prevState => ({ + ...prevState, + month: previousMonth.month, + year: previousMonth.year + })); + }; - // Resolve the month name from the CALENDAR_MONTHS object map - const monthname = Object.keys(CALENDAR_MONTHS)[Math.max(0, Math.min(month - 1, 11))]; + const gotoNextMonth = () => { + const { month, year } = dateState; + const nextMonth = getNextMonth(month, year); + setDateState(prevState => ({ + ...prevState, + month: nextMonth.month, + year: nextMonth.year + })); + }; + + const renderMonthAndYear = () => { + const { month, year } = dateState; + const monthname = CALENDAR_MONTHS[Math.max(0, Math.min(month - 1, 11))]; return ( - + {monthname} {year} - + ); }; const renderDayLabel = (day, index) => { - // Resolve the day of the week label const daylabel = WEEK_DAYS[day].toUpperCase(); return ( @@ -76,18 +190,14 @@ export default function Calendar({ date, onDateChanged }) { }; const renderCalendarDate = (date, index) => { - const { current, month, year } = dateState; const _date = new Date(date.join("-")); + const { current, month, year } = dateState; - // Check if calendar date is same day as today const isToday = isSameDay(_date, today); - // Check if calendar date is same day as currently selected date const isCurrent = current && isSameDay(_date, current); - // Check if calendar date is in the same month as the state month and year - const inMonth = - month && year && isSameMonth(_date, new Date([year, month, 1].join("-"))); - // The click handler - const onClick = gotoDate(_date); + const inMonth = month && year && isSameMonth(_date, new Date([year, month, 1].join("-"))); + const onClick = () => gotoDate(_date); + const props = { index, inMonth, onClick, title: _date.toDateString() }; const DateComponent = isCurrent @@ -96,48 +206,49 @@ export default function Calendar({ date, onDateChanged }) { ? Styled.TodayCalendarDate : Styled.CalendarDate; + const dateAppointments = appointments.filter(app => isSameDay(app.date, _date)); + return ( {_date.getDate()} + {dateAppointments.length > 0 && ( + + {dateAppointments.length} + + )} ); }; - const gotoDate = (date) => (evt) => { - evt && evt.preventDefault(); - const { current } = dateState; - if (!(current && isSameDay(date, current))) { - addDateToState(date); - onDateChanged(date); + const renderAppointmentDetails = (appointment) => { + if (editingAppointment && editingAppointment._id === appointment._id) { + return ( +
+

Editing Appointment: {appointment._id}

+ {/* Add form fields for editing appointment details */} + + +
+ ); } - }; - - const gotoPreviousMonth = () => { - const { month, year } = dateState; - const previousMonth = getPreviousMonth(month, year); - setDateState({ - month: previousMonth.month, - year: previousMonth.year, - current: dateState.current, - }); - }; - - const gotoNextMonth = () => { - const { month, year } = dateState; - const nextMonth = getNextMonth(month, year); - setDateState({ - month: nextMonth.month, - year: nextMonth.year, - current: dateState.current, - }); - }; - - const handlePrevious = (evt) => { - gotoPreviousMonth(); - }; - - const handleNext = (evt) => { - gotoNextMonth(); + return ( +
+

Appointment ID: {appointment._id}

+

Date: {appointment.date.toLocaleDateString()}

+

Time: {appointment.dateType === 'DateTime' ? appointment.date.toLocaleTimeString() : 'N/A'}

+
Characteristics:
+
    + {Object.entries(appointment.characteristicOccurrences).map(([key, value]) => ( +
  • {key}: {value.toString()}
  • + ))} +
+
+ Edit + +
+
+
+ ); }; return ( @@ -152,6 +263,13 @@ export default function Calendar({ date, onDateChanged }) { {getCalendarDates().map(renderCalendarDate)} + + {selectedDateAppointments.length > 0 && ( + +

Appointments for {dateState.current.toDateString()}

+ {selectedDateAppointments.map(renderAppointmentDetails)} +
+ )} ); } @@ -161,39 +279,7 @@ Calendar.propTypes = { onDateChanged: PropTypes.func, }; - - - -const gotoDate = (date) => (evt) => { - evt && evt.preventDefault(); - const { current } = dateState; - if (!(current && isSameDay(date, current))) { - addDateToState(date); - onDateChanged(date); - } - }; - const gotoPreviousMonth = () => { - const { month, year } = dateState; - const previousMonth = getPreviousMonth(month, year); - setDateState({ - month: previousMonth.month, - year: previousMonth.year, - current: dateState.current, - }); - }; - const gotoNextMonth = () => { - const { month, year } = dateState; - const nextMonth = getNextMonth(month, year); - setDateState({ - month: nextMonth.month, - year: nextMonth.year, - current: dateState.current, - }); - }; - const handlePrevious = (evt) => { - gotoPreviousMonth(); - }; - const handleNext = (evt) => { - gotoNextMonth(); - }; - +Calendar.defaultProps = { + date: new Date(), + onDateChanged: () => {}, +}; \ No newline at end of file diff --git a/frontend/src/components/layouts/TopNavbar.js b/frontend/src/components/layouts/TopNavbar.js index 2f2c826b..6fea73f5 100644 --- a/frontend/src/components/layouts/TopNavbar.js +++ b/frontend/src/components/layouts/TopNavbar.js @@ -237,6 +237,13 @@ function TopNavBar() { Persons + + + + + Calendar + + ) : null} From cbd99d2ed9ad5e06de3930dc0086d832c49deb22 Mon Sep 17 00:00:00 2001 From: realtmxi Date: Mon, 12 Aug 2024 19:36:13 +0800 Subject: [PATCH 06/11] Delete, Edit appointment in Calendar Page --- frontend/src/components/calendar/calendar.js | 6 +- frontend/src/components/calendar/event.js | 0 frontend/src/styles/calendarStyles.js | 131 +++++-------------- 3 files changed, 37 insertions(+), 100 deletions(-) create mode 100644 frontend/src/components/calendar/event.js diff --git a/frontend/src/components/calendar/calendar.js b/frontend/src/components/calendar/calendar.js index 6d514b7f..60b2c45c 100644 --- a/frontend/src/components/calendar/calendar.js +++ b/frontend/src/components/calendar/calendar.js @@ -236,9 +236,11 @@ export default function Calendar({ date, onDateChanged }) {

Appointment ID: {appointment._id}

Date: {appointment.date.toLocaleDateString()}

Time: {appointment.dateType === 'DateTime' ? appointment.date.toLocaleTimeString() : 'N/A'}

-
Characteristics:
+ {/*
Characteristics:
*/}
    - {Object.entries(appointment.characteristicOccurrences).map(([key, value]) => ( + {Object.entries(appointment.characteristicOccurrences) + .filter(([key]) => key !== 'Date') // Filter out the 'Date' characteristic + .map(([key, value]) => (
  • {key}: {value.toString()}
  • ))}
diff --git a/frontend/src/components/calendar/event.js b/frontend/src/components/calendar/event.js new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/styles/calendarStyles.js b/frontend/src/styles/calendarStyles.js index 7bd51118..58a849cb 100644 --- a/frontend/src/styles/calendarStyles.js +++ b/frontend/src/styles/calendarStyles.js @@ -140,102 +140,37 @@ export const BlockedCalendarDate = styled(CalendarDate)` } `; -import { - FormGroup, - Label, - Input, - Button, - Dropdown, - DropdownToggle, - DropdownMenu, - } from "reactstrap"; - - export const DatePickerContainer = styled.div` - position: relative; - `; - - export const DatePickerFormGroup = styled(FormGroup)` - display: flex; - justify-content: space-between; - position: relative; - width: 100%; - border: 2px solid #06c; - border-radius: 5px; - overflow: hidden; - `; - - export const DatePickerLabel = styled(Label)` - margin: 0; - padding: 0 2rem; - font-weight: 600; - font-size: 0.7rem; - letter-spacing: 2px; - text-transform: uppercase; - color: #06c; - border-right: 2px solid #06c; - display: flex; - align-items: center; - justify-content: center; - background: rgba(0, 102, 204, 0.05); - `; - - export const DatePickerInput = styled(Input)` - font-weight: 500; - font-size: 1rem; - color: #333; - box-shadow: none; - border: none; - text-align: center; - letter-spacing: 1px; - background: transparent !important; - display: flex; - align-items: center; - ::placeholder { - color: #999; - font-size: 0.9rem; - } - width:100%; - height:100%; - `; - - export const DatePickerDropdown = styled(Dropdown)` - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - `; - - export const DatePickerDropdownToggle = styled(DropdownToggle)` - position: relative; - width: 100%; - height: 100%; - background: transparent; - opacity: 0; - filter: alpha(opacity=0); - `; - - export const DatePickerDropdownMenu = styled(DropdownMenu)` - margin-top: 3rem; - left: 0; - width: 100%; - height: 75vh !important; - border: none; +export const AppointmentIndicator = styled.div` + background-color: #007bff; + color: white; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + position: absolute; + bottom: 2px; + right: 2px; +`; + +export const AppointmentList = styled.div` + margin-top: 20px; + padding: 10px; + background-color: #f8f9fa; + border-radius: 5px; + + h3 { + margin-bottom: 10px; + } + + ul { + list-style-type: none; padding: 0; - transform: none !important; - `; - - export const DatePickerButton = styled(Button)` - position: absolute; - border: 2px solid #06c; - margin-top:2%; - right:50% !important; - background: transparent; - font-size: 1.2rem; - color: #06c; - :hover { - border: white solid #06c; - color: white !important; - background: #06c; - } - `; \ No newline at end of file + } + + li { + margin-bottom: 5px; + } +`; \ No newline at end of file From 7e6806b8e14a8a96d43cab2e01a146f4229575fa Mon Sep 17 00:00:00 2001 From: Murphy Tian Date: Tue, 14 Jan 2025 16:28:38 +0800 Subject: [PATCH 07/11] [Fix] calendar Month display --- frontend/src/helpers/calendarHelper.js | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/helpers/calendarHelper.js b/frontend/src/helpers/calendarHelper.js index 991401b6..c71c20ef 100644 --- a/frontend/src/helpers/calendarHelper.js +++ b/frontend/src/helpers/calendarHelper.js @@ -20,20 +20,20 @@ export const WEEK_DAYS = { } // Calendar months names and short names -export const CALENDAR_MONTHS = { - January: "Jan", - February: "Feb", - March: "Mar", - April: "Apr", - May: "May", - June: "Jun", - July: "Jul", - August: "Aug", - September: "Sep", - October: "Oct", - November: "Nov", - December: "Dec" -} +export const CALENDAR_MONTHS = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +]; // Weeks displayed on calendar export const CALENDAR_WEEKS = 6; From a7082b9f01ecc92aa3e6ca9515cc973600a64e8c Mon Sep 17 00:00:00 2001 From: Murphy Tian Date: Tue, 14 Jan 2025 20:55:15 +0800 Subject: [PATCH 08/11] [Feat] delete appointment details, adding a floating appointment modal window --- frontend/package.json | 1 + frontend/src/components/calendar/calendar.js | 89 +++++-- frontend/src/styles/calendarStyles.js | 233 ++++++++----------- 3 files changed, 169 insertions(+), 154 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 63167093..72bab5db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "dagre": "^0.8.5", "date-fns": "^2.23.0", "lodash": "^4.17.20", + "lucide-react": "^0.471.1", "material-ui-phone-number": "^3.0.0", "notistack": "^2.0.5", "react": "^18.2.0", diff --git a/frontend/src/components/calendar/calendar.js b/frontend/src/components/calendar/calendar.js index 60b2c45c..b69de8e0 100644 --- a/frontend/src/components/calendar/calendar.js +++ b/frontend/src/components/calendar/calendar.js @@ -13,7 +13,8 @@ import calendar, { } from "../../helpers/calendarHelper"; import "bootstrap/dist/css/bootstrap.min.css"; import { fetchMultipleGeneric, deleteSingleGeneric, updateSingleGeneric } from "../../api/genericDataApi"; -import { Link } from "../shared" +import { Link } from "../shared"; +import AppointmentModal from "./calendarModal"; export default function Calendar({ date, onDateChanged }) { const [dateState, setDateState] = useState({ @@ -26,6 +27,8 @@ export default function Calendar({ date, onDateChanged }) { const [appointments, setAppointments] = useState([]); const [selectedDateAppointments, setSelectedDateAppointments] = useState([]); const [editingAppointment, setEditingAppointment] = useState(null); + const [selectedAppointment, setSelectedAppointment] = useState(null); + const [modalPosition, setModalPosition] = useState({ x: 0, y: 0 }); useEffect(() => { addDateToState(date); @@ -108,6 +111,13 @@ export default function Calendar({ date, onDateChanged }) { } }; + const handleAppointmentClick = (e, appointment) => { + e.stopPropagation(); // Prevent cell click handler from firing + const rect = e.currentTarget.getBoundingClientRect(); + setModalPosition({ x: rect.right, y: rect.top }); + setSelectedAppointment(appointment); + }; + const parseDate = (dateString) => { if (!dateString) return null; @@ -141,8 +151,8 @@ export default function Calendar({ date, onDateChanged }) { onDateChanged(date); } - const selectedAppointments = appointments.filter(app => isSameDay(app.date, date)); - setSelectedDateAppointments(selectedAppointments); + // const selectedAppointments = appointments.filter(app => isSameDay(app.date, date)); + // setSelectedDateAppointments(selectedAppointments); } }; @@ -192,31 +202,51 @@ export default function Calendar({ date, onDateChanged }) { const renderCalendarDate = (date, index) => { const _date = new Date(date.join("-")); const { current, month, year } = dateState; - + const isToday = isSameDay(_date, today); const isCurrent = current && isSameDay(_date, current); const inMonth = month && year && isSameMonth(_date, new Date([year, month, 1].join("-"))); const onClick = () => gotoDate(_date); - - const props = { index, inMonth, onClick, title: _date.toDateString() }; - - const DateComponent = isCurrent - ? Styled.HighlightedCalendarDate - : isToday - ? Styled.TodayCalendarDate - : Styled.CalendarDate; - + const dateAppointments = appointments.filter(app => isSameDay(app.date, _date)); - + const maxDisplayAppointments = 3; + + // Helper function to get the appointment display name + const getAppointmentName = (appointment) => { + const characteristics = appointment.characteristicOccurrences; + return characteristics['Appointment Name'] || 'Untitled Appointment'; + }; + return ( - - {_date.getDate()} - {dateAppointments.length > 0 && ( - - {dateAppointments.length} - - )} - + + + {_date.getDate()} + + + + {dateAppointments.slice(0, maxDisplayAppointments).map((app) => ( + handleAppointmentClick(e, app)} + > + {getAppointmentName(app)} + + ))} + + {dateAppointments.length > maxDisplayAppointments && ( + + {dateAppointments.length - maxDisplayAppointments} more + + )} + + ); }; @@ -266,12 +296,23 @@ export default function Calendar({ date, onDateChanged }) { - {selectedDateAppointments.length > 0 && ( + // Appointment details + {/* {selectedDateAppointments.length > 0 && (

Appointments for {dateState.current.toDateString()}

{selectedDateAppointments.map(renderAppointmentDetails)}
- )} + )} */} + + {selectedAppointment && ( + setSelectedAppointment(null)} + onEdit={() => handleEdit(selectedAppointment)} + onDelete={() => handleDelete(selectedAppointment._id)} + />)} + ); } diff --git a/frontend/src/styles/calendarStyles.js b/frontend/src/styles/calendarStyles.js index 58a849cb..d9823ca7 100644 --- a/frontend/src/styles/calendarStyles.js +++ b/frontend/src/styles/calendarStyles.js @@ -1,176 +1,149 @@ import styled from "styled-components"; import "bootstrap/dist/css/bootstrap.min.css"; -export const Arrow = styled.button` - appearance: none; - user-select: none; - outline: none !important; - display: inline-block; - position: relative; - cursor: pointer; - padding: 0; - border: none; - border-top: 1.6em solid transparent; - border-bottom: 1.6em solid transparent; - transition: all 0.25s ease-out; -`; - -export const ArrowLeft = styled(Arrow)` - border-right: 2.4em solid #ccc; - left: 1.5rem; - :hover { - border-right-color: #06c; - } -`; - -export const ArrowRight = styled(Arrow)` - border-left: 2.4em solid #ccc; - right: 1.5rem; - :hover { - border-left-color: #06c; - } -`; - export const CalendarContainer = styled.div` - font-size: 5px; - border: 2px solid #06c; - border-radius: 5px; + font-family: 'Google Sans', Roboto, Arial, sans-serif; + border: 1px solid #dadce0; + border-radius: 8px; overflow: hidden; + background: white; + width: 100%; `; export const CalendarHeader = styled.div` display: flex; align-items: center; justify-content: space-between; + padding: 10px 20px; + border-bottom: 1px solid #dadce0; +`; + +export const CalendarMonth = styled.div` + font-size: 22px; + font-weight: 400; + color: #3c4043; + letter-spacing: 0; `; export const CalendarGrid = styled.div` display: grid; - grid-template: repeat(7, auto) / repeat(7, auto); + grid-template-columns: repeat(7, 1fr); + background: white; `; -export const CalendarMonth = styled.div` +export const CalendarDay = styled.div` + color: #70757a; + font-size: 11px; font-weight: 500; - font-size: 5em; - color: #06c; + letter-spacing: 0; text-align: center; - padding: 0.5em 0.25em; - word-spacing: 5px; - user-select: none; + padding: 15px 0; + border-bottom: 1px solid #dadce0; `; export const CalendarCell = styled.div` - text-align: center; - align-self: center; - letter-spacing: 0.1rem; - padding: 0.6em 0.25em; - user-select: none; - grid-column: ${(props) => (props.index % 7) + 1} / span 1; -`; + aspect-ratio: 1/1; + border-right: 1px solid #dadce0; + border-bottom: 1px solid #dadce0; + padding: 8px; + position: relative; + background: white; // Remove isToday condition + color: ${props => props.inMonth ? '#3c4043' : '#70757a'}; // Remove isToday condition + + &:last-child { + border-right: none; + } + + &:hover { + background-color: #f8f9fa; // Remove isToday condition + } -export const CalendarDay = styled(CalendarCell)` - font-weight: 600; - font-size: 2.25em; - color: #06c; - border-top: 2px solid #06c; - border-bottom: 2px solid #06c; - border-right: ${(props) => - (props.index % 7) + 1 === 7 ? `none` : `2px solid #06c`}; + ${props => props.isSelected && ` + background-color: #e8f0fe; + `} `; -export const CalendarDate = styled(CalendarCell)` - font-weight: ${(props) => (props.inMonth ? 500 : 300)}; - font-size: 4em; - cursor: pointer; - border-bottom: ${(props) => - (props.index + 1) / 7 <= 5 ? `1px solid #ddd` : `none`}; - border-right: ${(props) => - (props.index % 7) + 1 === 7 ? `none` : `1px solid #ddd`}; - color: ${(props) => (props.inMonth ? `#333` : `#ddd`)}; - grid-row: ${(props) => Math.floor(props.index / 7) + 2} / span 1; - transition: all 0.4s ease-out; - :hover { - color: #06c; - background: rgba(0, 102, 204, 0.075); - } +export const DateNumber = styled.div` +font-size: 12px; +margin-bottom: 4px; +text-align: right; +color: ${props => props.isToday ? '#1a73e8' : 'inherit'}; +font-weight: ${props => props.isToday ? '500' : '400'}; + +${props => props.isToday && ` + background-color: #1a73e8; + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; +`} `; -export const HighlightedCalendarDate = styled(CalendarDate)` - color: #fff !important; - background: #06c !important; - position: relative; - ::before { - content: ""; - position: absolute; - top: -1px; - left: -1px; - width: calc(100% + 2px); - height: calc(100% + 2px); - border: 2px solid #06c; - } +export const AppointmentList = styled.div` + display: flex; + flex-direction: column; + gap: 2px; `; -export const TodayCalendarDate = styled(HighlightedCalendarDate)` - color: #06c !important; - background: transparent !important; - ::after { - content: ""; - position: absolute; - right: 0; - bottom: 0; - border-bottom: 0.75em solid #06c; - border-left: 0.75em solid transparent; - border-top: 0.75em solid transparent; - } - :hover { - color: #06c !important; - background: rgba(0, 102, 204, 0.075) !important; +export const AppointmentPreview = styled.div` + font-size: 11px; + padding: 0 4px; + height: 18px; + line-height: 18px; + border-radius: 3px; + background: #1a73e8; + color: white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + + &:hover { + background: #1557b0; } `; -export const BlockedCalendarDate = styled(CalendarDate)` - color: black !important; - background: gray !important; - position: relative; - :hover { - color: black !important; - background: gray !important; - border-color:gray; - cursor:default; +export const MoreAppointments = styled.div` + font-size: 11px; + color: #70757a; + padding: 0 4px; + cursor: pointer; + + &:hover { + color: #1a73e8; } `; -export const AppointmentIndicator = styled.div` - background-color: #007bff; - color: white; +export const Arrow = styled.button` + border: none; + background: transparent; + padding: 8px; + cursor: pointer; border-radius: 50%; - width: 20px; - height: 20px; display: flex; align-items: center; justify-content: center; - font-size: 12px; - position: absolute; - bottom: 2px; - right: 2px; -`; + color: #70757a; -export const AppointmentList = styled.div` - margin-top: 20px; - padding: 10px; - background-color: #f8f9fa; - border-radius: 5px; - - h3 { - margin-bottom: 10px; + &:hover { + background: #f8f9fa; } +`; - ul { - list-style-type: none; - padding: 0; +export const ArrowLeft = styled(Arrow)` + &::before { + content: '‹'; + font-size: 24px; } +`; - li { - margin-bottom: 5px; +export const ArrowRight = styled(Arrow)` + &::before { + content: '›'; + font-size: 24px; } `; \ No newline at end of file From 853f5e1457ac2b69f9e6017adb88c8e93d13c4be Mon Sep 17 00:00:00 2001 From: Murphy Tian Date: Tue, 14 Jan 2025 20:55:49 +0800 Subject: [PATCH 09/11] [Feat] delete appointment details, adding a floating appointment modal window --- .../src/components/calendar/calendarModal.js | 78 +++++++++++++++++++ frontend/src/styles/calendarModalStyles.js | 50 ++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 frontend/src/components/calendar/calendarModal.js create mode 100644 frontend/src/styles/calendarModalStyles.js diff --git a/frontend/src/components/calendar/calendarModal.js b/frontend/src/components/calendar/calendarModal.js new file mode 100644 index 00000000..cf8aa145 --- /dev/null +++ b/frontend/src/components/calendar/calendarModal.js @@ -0,0 +1,78 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Edit2, Trash2, X } from 'lucide-react'; +import * as Styled from '../../styles/calendarModalStyles'; +import { Link } from "../shared"; + +const AppointmentDetails = ({ appointment, position, onClose, onEdit, onDelete }) => { + const popupRef = useRef(null); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + + useEffect(() => { + if (popupRef.current) { + const popupRect = popupRef.current.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let top = position.y; + let left = position.x; + + // Adjust if popup would go off screen + if (left + popupRect.width > viewportWidth) { + left = position.x - popupRect.width; + } + if (top + popupRect.height > viewportHeight) { + top = position.y - popupRect.height; + } + + setPopupPosition({ top, left }); + } + }, [position]); + + const formatDateTime = (dateTime) => { + const date = new Date(dateTime); + return { + date: date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }), + time: date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) + }; + }; + + const { date, time } = formatDateTime(appointment.date); + + const handleClickOutside = (e) => { + if (popupRef.current && !popupRef.current.contains(e.target)) { + onClose(); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( + + + + + + + + + + + + + {appointment.characteristicOccurrences['Appointment Name']} + + + + {date} • {time} + + + ); +}; + +export default AppointmentDetails; \ No newline at end of file diff --git a/frontend/src/styles/calendarModalStyles.js b/frontend/src/styles/calendarModalStyles.js new file mode 100644 index 00000000..069846bb --- /dev/null +++ b/frontend/src/styles/calendarModalStyles.js @@ -0,0 +1,50 @@ +import styled from "styled-components"; + +export const Popup = styled.div` + position: fixed; + background: white; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + padding: 16px; + min-width: 280px; + max-width: 400px; + z-index: 1000; + font-family: 'Google Sans', Roboto, Arial, sans-serif; +`; + +export const PopupHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; +`; + +export const HeaderActions = styled.div` + display: flex; + gap: 8px; +`; + +export const ActionIcon = styled.button` + border: none; + background: none; + padding: 8px; + cursor: pointer; + border-radius: 50%; + color: #5f6368; + + &:hover { + background: #f1f3f4; + } +`; + +export const Title = styled.div` + font-size: 22px; + font-weight: 400; + color: #3c4043; + margin-bottom: 8px; +`; + +export const DateTime = styled.div` + font-size: 14px; + color: #3c4043; +`; \ No newline at end of file From a45199735f69b16b546dfe794578107a73ed13f1 Mon Sep 17 00:00:00 2001 From: Murphy Tian Date: Tue, 14 Jan 2025 21:18:34 +0800 Subject: [PATCH 10/11] [Fix] calendar click more does not work bug --- frontend/src/components/calendar/calendar.js | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/calendar/calendar.js b/frontend/src/components/calendar/calendar.js index b69de8e0..4eb53576 100644 --- a/frontend/src/components/calendar/calendar.js +++ b/frontend/src/components/calendar/calendar.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, Fragment } from "react"; +import React, { useState, useEffect, Fragment, useRef } from "react"; import PropTypes from "prop-types"; import * as Styled from "../../styles/calendarStyles"; import calendar, { @@ -29,6 +29,7 @@ export default function Calendar({ date, onDateChanged }) { const [editingAppointment, setEditingAppointment] = useState(null); const [selectedAppointment, setSelectedAppointment] = useState(null); const [modalPosition, setModalPosition] = useState({ x: 0, y: 0 }); + const [expandedDate, setExpandedDate] = useState(null); useEffect(() => { addDateToState(date); @@ -118,6 +119,11 @@ export default function Calendar({ date, onDateChanged }) { setSelectedAppointment(appointment); }; + const handleMoreClick = (e, date) => { + e.stopPropagation(); // Prevent cell click handler + setExpandedDate(expandedDate === getDateISO(date) ? null : getDateISO(date)); + }; + const parseDate = (dateString) => { if (!dateString) return null; @@ -210,8 +216,8 @@ export default function Calendar({ date, onDateChanged }) { const dateAppointments = appointments.filter(app => isSameDay(app.date, _date)); const maxDisplayAppointments = 3; + const isExpanded = expandedDate === getDateISO(_date); - // Helper function to get the appointment display name const getAppointmentName = (appointment) => { const characteristics = appointment.characteristicOccurrences; return characteristics['Appointment Name'] || 'Untitled Appointment'; @@ -223,14 +229,15 @@ export default function Calendar({ date, onDateChanged }) { isToday={isToday} isCurrent={isCurrent} inMonth={inMonth} + expanded={isExpanded} onClick={onClick} > {_date.getDate()} - - {dateAppointments.slice(0, maxDisplayAppointments).map((app) => ( + + {(isExpanded ? dateAppointments : dateAppointments.slice(0, maxDisplayAppointments)).map((app) => ( ))} - {dateAppointments.length > maxDisplayAppointments && ( - + {!isExpanded && dateAppointments.length > maxDisplayAppointments && ( + handleMoreClick(e, _date)} + > {dateAppointments.length - maxDisplayAppointments} more )} @@ -312,7 +321,7 @@ export default function Calendar({ date, onDateChanged }) { onEdit={() => handleEdit(selectedAppointment)} onDelete={() => handleDelete(selectedAppointment._id)} />)} - + ); } From 0c609a3d645f863abc371969ee23e60eb4e2a75c Mon Sep 17 00:00:00 2001 From: Murphy Tian Date: Tue, 14 Jan 2025 22:23:15 +0800 Subject: [PATCH 11/11] [Feat] google calendar sync --- .../src/components/calendar/calendarModal.js | 5 + frontend/src/components/calendar/event.js | 0 .../components/calendar/googleCalendarSync.js | 49 ++++++++ frontend/src/helpers/googleCalendarHelper.js | 106 ++++++++++++++++++ frontend/src/styles/calendarModalStyles.js | 6 + frontend/src/styles/calendarStyles.js | 64 +++++++---- 6 files changed, 211 insertions(+), 19 deletions(-) delete mode 100644 frontend/src/components/calendar/event.js create mode 100644 frontend/src/components/calendar/googleCalendarSync.js create mode 100644 frontend/src/helpers/googleCalendarHelper.js diff --git a/frontend/src/components/calendar/calendarModal.js b/frontend/src/components/calendar/calendarModal.js index cf8aa145..434388e0 100644 --- a/frontend/src/components/calendar/calendarModal.js +++ b/frontend/src/components/calendar/calendarModal.js @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Edit2, Trash2, X } from 'lucide-react'; import * as Styled from '../../styles/calendarModalStyles'; import { Link } from "../shared"; +import GoogleCalendarSync from './googleCalendarSync'; const AppointmentDetails = ({ appointment, position, onClose, onEdit, onDelete }) => { const popupRef = useRef(null); @@ -71,6 +72,10 @@ const AppointmentDetails = ({ appointment, position, onClose, onEdit, onDelete } {date} • {time} + + + + ); }; diff --git a/frontend/src/components/calendar/event.js b/frontend/src/components/calendar/event.js deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/components/calendar/googleCalendarSync.js b/frontend/src/components/calendar/googleCalendarSync.js new file mode 100644 index 00000000..c2f88f6e --- /dev/null +++ b/frontend/src/components/calendar/googleCalendarSync.js @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { googleCalendarService } from '../../helpers/googleCalendarHelper'; + +const GoogleCalendarSync = ({ appointment }) => { + const [syncing, setSyncing] = useState(false); + const [error, setError] = useState(null); + + const handleSync = async () => { + try { + setSyncing(true); + setError(null); + + if (!googleCalendarService.isAuthenticated) { + await googleCalendarService.authenticate(); + } + + await googleCalendarService.syncAppointment(appointment); + alert('Successfully synced with Google Calendar!'); + } catch (err) { + setError(err.message); + } finally { + setSyncing(false); + } + }; + + return ( + <> + + {error &&
{error}
} + + ); +}; + +export default GoogleCalendarSync; \ No newline at end of file diff --git a/frontend/src/helpers/googleCalendarHelper.js b/frontend/src/helpers/googleCalendarHelper.js new file mode 100644 index 00000000..21daed1c --- /dev/null +++ b/frontend/src/helpers/googleCalendarHelper.js @@ -0,0 +1,106 @@ +const GOOGLE_API_KEY = 'YOUR_GOOGLE_API_KEY'; +const GOOGLE_CLIENT_ID = 'YOUR_CLIENT_ID'; +const SCOPES = 'https://www.googleapis.com/auth/calendar'; + +export class GoogleCalendarService { + constructor() { + this.isAuthenticated = false; + this.tokenClient = null; + } + + async initialize() { + // Load the Google API client library + await this.loadGoogleApi(); + + this.tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: GOOGLE_CLIENT_ID, + scope: SCOPES, + callback: this.handleAuthResponse, + }); + } + + async loadGoogleApi() { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://apis.google.com/js/api.js'; + script.onload = () => { + gapi.load('client', async () => { + try { + await gapi.client.init({ + apiKey: GOOGLE_API_KEY, + discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest'], + }); + resolve(); + } catch (error) { + reject(error); + } + }); + }; + script.onerror = reject; + document.body.appendChild(script); + }); + } + + handleAuthResponse = (response) => { + if (response.error) { + throw new Error('Authentication failed'); + } + this.isAuthenticated = true; + }; + + async authenticate() { + if (!this.tokenClient) { + await this.initialize(); + } + this.tokenClient.requestAccessToken(); + } + + async syncAppointment(appointment) { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const event = { + summary: appointment.characteristicOccurrences['Appointment Name'], + start: { + dateTime: appointment.date.toISOString(), + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + end: { + dateTime: new Date(appointment.date.getTime() + 60 * 60 * 1000).toISOString(), // Default 1 hour + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + description: `First Name: ${appointment.characteristicOccurrences['First Name']}\nLast Name: ${appointment.characteristicOccurrences['Last Name']}`, + }; + + try { + const response = await gapi.client.calendar.events.insert({ + calendarId: 'primary', + resource: event, + }); + return response.result; + } catch (error) { + console.error('Error syncing with Google Calendar:', error); + throw error; + } + } + + async syncMultipleAppointments(appointments) { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const results = []; + for (const appointment of appointments) { + try { + const result = await this.syncAppointment(appointment); + results.push({ success: true, appointment, result }); + } catch (error) { + results.push({ success: false, appointment, error }); + } + } + return results; + } +} + +export const googleCalendarService = new GoogleCalendarService(); \ No newline at end of file diff --git a/frontend/src/styles/calendarModalStyles.js b/frontend/src/styles/calendarModalStyles.js index 069846bb..aa8288d4 100644 --- a/frontend/src/styles/calendarModalStyles.js +++ b/frontend/src/styles/calendarModalStyles.js @@ -47,4 +47,10 @@ export const Title = styled.div` export const DateTime = styled.div` font-size: 14px; color: #3c4043; +`; + +export const SyncContainer = styled.div` + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #dadce0; `; \ No newline at end of file diff --git a/frontend/src/styles/calendarStyles.js b/frontend/src/styles/calendarStyles.js index d9823ca7..76bd6281 100644 --- a/frontend/src/styles/calendarStyles.js +++ b/frontend/src/styles/calendarStyles.js @@ -42,25 +42,21 @@ export const CalendarDay = styled.div` `; export const CalendarCell = styled.div` - aspect-ratio: 1/1; + aspect-ratio: ${props => props.expanded ? 'auto' : '1/1'}; border-right: 1px solid #dadce0; border-bottom: 1px solid #dadce0; padding: 8px; position: relative; - background: white; // Remove isToday condition - color: ${props => props.inMonth ? '#3c4043' : '#70757a'}; // Remove isToday condition + background: white; + color: ${props => props.inMonth ? '#3c4043' : '#70757a'}; &:last-child { border-right: none; } &:hover { - background-color: #f8f9fa; // Remove isToday condition + background-color: #f8f9fa; } - - ${props => props.isSelected && ` - background-color: #e8f0fe; - `} `; export const DateNumber = styled.div` @@ -83,38 +79,46 @@ ${props => props.isToday && ` `} `; -export const AppointmentList = styled.div` - display: flex; - flex-direction: column; - gap: 2px; -`; - export const AppointmentPreview = styled.div` + display: flex; + align-items: center; font-size: 11px; padding: 0 4px; height: 18px; line-height: 18px; - border-radius: 3px; - background: #1a73e8; - color: white; + color: #3c4043; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; + &::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: #1a73e8; + margin-right: 4px; + flex-shrink: 0; + } + &:hover { - background: #1557b0; + background: #f1f3f4; + border-radius: 3px; } `; export const MoreAppointments = styled.div` font-size: 11px; color: #70757a; - padding: 0 4px; + padding: 4px 4px 4px 12px; // Extra left padding to align with appointments cursor: pointer; + border-radius: 3px; &:hover { color: #1a73e8; + background: #f1f3f4; } `; @@ -146,4 +150,26 @@ export const ArrowRight = styled(Arrow)` content: '›'; font-size: 24px; } +`; + +export const AppointmentList = styled.div` + display: flex; + flex-direction: column; + gap: 2px; + max-height: ${props => props.expanded ? '200px' : '100%'}; + overflow-y: auto; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; + } `; \ No newline at end of file