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
diff --git a/frontend/package.json b/frontend/package.json
index 96367f2f..72bab5db 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,21 +9,27 @@
"@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",
"lodash": "^4.17.20",
+ "lucide-react": "^0.471.1",
"material-ui-phone-number": "^3.0.0",
"notistack": "^2.0.5",
"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",
diff --git a/frontend/src/components/calendar/calendar.js b/frontend/src/components/calendar/calendar.js
new file mode 100644
index 00000000..4eb53576
--- /dev/null
+++ b/frontend/src/components/calendar/calendar.js
@@ -0,0 +1,337 @@
+import React, { useState, useEffect, Fragment, useRef } 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";
+import { fetchMultipleGeneric, deleteSingleGeneric, updateSingleGeneric } from "../../api/genericDataApi";
+import { Link } from "../shared";
+import AppointmentModal from "./calendarModal";
+
+export default function Calendar({ date, onDateChanged }) {
+ const [dateState, setDateState] = useState({
+ current: new Date(),
+ month: new Date().getMonth() + 1,
+ year: new Date().getFullYear(),
+ });
+
+ const [today] = useState(new Date());
+ 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 });
+ const [expandedDate, setExpandedDate] = useState(null);
+
+ useEffect(() => {
+ addDateToState(date);
+ fetchAppointments();
+ }, [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 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 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 handleMoreClick = (e, date) => {
+ e.stopPropagation(); // Prevent cell click handler
+ setExpandedDate(expandedDate === getDateISO(date) ? null : getDateISO(date));
+ };
+
+ 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 : today.getMonth() + 1);
+ const calendarYear = year || (current ? current.getFullYear() : today.getFullYear());
+ return calendar(calendarMonth, calendarYear);
+ };
+
+ 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 previousMonth = getPreviousMonth(month, year);
+ setDateState(prevState => ({
+ ...prevState,
+ month: previousMonth.month,
+ year: previousMonth.year
+ }));
+ };
+
+ 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) => {
+ const daylabel = WEEK_DAYS[day].toUpperCase();
+ return (
+
+ {daylabel}
+
+ );
+ };
+
+ 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 dateAppointments = appointments.filter(app => isSameDay(app.date, _date));
+ const maxDisplayAppointments = 3;
+ const isExpanded = expandedDate === getDateISO(_date);
+
+ const getAppointmentName = (appointment) => {
+ const characteristics = appointment.characteristicOccurrences;
+ return characteristics['Appointment Name'] || 'Untitled Appointment';
+ };
+
+ return (
+
+
+ {_date.getDate()}
+
+
+
+ {(isExpanded ? dateAppointments : dateAppointments.slice(0, maxDisplayAppointments)).map((app) => (
+ handleAppointmentClick(e, app)}
+ >
+ {getAppointmentName(app)}
+
+ ))}
+
+ {!isExpanded && dateAppointments.length > maxDisplayAppointments && (
+ handleMoreClick(e, _date)}
+ >
+ {dateAppointments.length - maxDisplayAppointments} more
+
+ )}
+
+
+ );
+ };
+
+ const renderAppointmentDetails = (appointment) => {
+ if (editingAppointment && editingAppointment._id === appointment._id) {
+ return (
+
+
Editing Appointment: {appointment._id}
+ {/* Add form fields for editing appointment details */}
+
+
+
+ );
+ }
+ return (
+
+
Appointment ID: {appointment._id}
+
Date: {appointment.date.toLocaleDateString()}
+
Time: {appointment.dateType === 'DateTime' ? appointment.date.toLocaleTimeString() : 'N/A'}
+ {/*
Characteristics:
*/}
+
+ {Object.entries(appointment.characteristicOccurrences)
+ .filter(([key]) => key !== 'Date') // Filter out the 'Date' characteristic
+ .map(([key, value]) => (
+ - {key}: {value.toString()}
+ ))}
+
+
+ Edit
+
+
+
+
+ );
+ };
+
+ return (
+
+ {renderMonthAndYear()}
+
+
+
+ {Object.keys(WEEK_DAYS).map(renderDayLabel)}
+
+
+ {getCalendarDates().map(renderCalendarDate)}
+
+
+
+ // Appointment details
+ {/* {selectedDateAppointments.length > 0 && (
+
+ Appointments for {dateState.current.toDateString()}
+ {selectedDateAppointments.map(renderAppointmentDetails)}
+
+ )} */}
+
+ {selectedAppointment && (
+ setSelectedAppointment(null)}
+ onEdit={() => handleEdit(selectedAppointment)}
+ onDelete={() => handleDelete(selectedAppointment._id)}
+ />)}
+
+
+ );
+}
+
+Calendar.propTypes = {
+ date: PropTypes.instanceOf(Date),
+ onDateChanged: PropTypes.func,
+};
+
+Calendar.defaultProps = {
+ date: new Date(),
+ onDateChanged: () => {},
+};
\ No newline at end of file
diff --git a/frontend/src/components/calendar/calendarModal.js b/frontend/src/components/calendar/calendarModal.js
new file mode 100644
index 00000000..434388e0
--- /dev/null
+++ b/frontend/src/components/calendar/calendarModal.js
@@ -0,0 +1,83 @@
+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);
+ 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/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/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
+
+
) : null}
diff --git a/frontend/src/helpers/calendarHelper.js b/frontend/src/helpers/calendarHelper.js
new file mode 100644
index 00000000..c71c20ef
--- /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',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+];
+
+// 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/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/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/calendarModalStyles.js b/frontend/src/styles/calendarModalStyles.js
new file mode 100644
index 00000000..aa8288d4
--- /dev/null
+++ b/frontend/src/styles/calendarModalStyles.js
@@ -0,0 +1,56 @@
+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;
+`;
+
+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
new file mode 100644
index 00000000..76bd6281
--- /dev/null
+++ b/frontend/src/styles/calendarStyles.js
@@ -0,0 +1,175 @@
+import styled from "styled-components";
+import "bootstrap/dist/css/bootstrap.min.css";
+
+export const CalendarContainer = styled.div`
+ 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-columns: repeat(7, 1fr);
+ background: white;
+`;
+
+export const CalendarDay = styled.div`
+ color: #70757a;
+ font-size: 11px;
+ font-weight: 500;
+ letter-spacing: 0;
+ text-align: center;
+ padding: 15px 0;
+ border-bottom: 1px solid #dadce0;
+`;
+
+export const CalendarCell = styled.div`
+ aspect-ratio: ${props => props.expanded ? 'auto' : '1/1'};
+ border-right: 1px solid #dadce0;
+ border-bottom: 1px solid #dadce0;
+ padding: 8px;
+ position: relative;
+ background: white;
+ color: ${props => props.inMonth ? '#3c4043' : '#70757a'};
+
+ &:last-child {
+ border-right: none;
+ }
+
+ &:hover {
+ background-color: #f8f9fa;
+ }
+`;
+
+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 AppointmentPreview = styled.div`
+ display: flex;
+ align-items: center;
+ font-size: 11px;
+ padding: 0 4px;
+ height: 18px;
+ line-height: 18px;
+ 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: #f1f3f4;
+ border-radius: 3px;
+ }
+`;
+
+export const MoreAppointments = styled.div`
+ font-size: 11px;
+ color: #70757a;
+ padding: 4px 4px 4px 12px; // Extra left padding to align with appointments
+ cursor: pointer;
+ border-radius: 3px;
+
+ &:hover {
+ color: #1a73e8;
+ background: #f1f3f4;
+ }
+`;
+
+export const Arrow = styled.button`
+ border: none;
+ background: transparent;
+ padding: 8px;
+ cursor: pointer;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #70757a;
+
+ &:hover {
+ background: #f8f9fa;
+ }
+`;
+
+export const ArrowLeft = styled(Arrow)`
+ &::before {
+ content: '‹';
+ font-size: 24px;
+ }
+`;
+
+export const ArrowRight = styled(Arrow)`
+ &::before {
+ 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