From dedcbac1babe3d102d0152a235d47689c204a0e1 Mon Sep 17 00:00:00 2001 From: Tinashe-Austin Date: Thu, 26 Sep 2024 08:54:55 +0200 Subject: [PATCH 1/5] Refactor code to import Employee icon and add Employee route to SideNav component --- frontend/occupi-web/src/App.tsx | 2 +- .../occupi-web/src/assets/icons/Employee.tsx | 22 ++++++++++ .../src/assets/icons/UserProfileGroup.tsx | 23 ++++++---- frontend/occupi-web/src/assets/index.ts | 4 +- .../bookingComponent/BookingComponent.tsx | 14 ++++++- .../components/sideNavComponent/SideNav.tsx | 42 +++++++++++-------- 6 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 frontend/occupi-web/src/assets/icons/Employee.tsx diff --git a/frontend/occupi-web/src/App.tsx b/frontend/occupi-web/src/App.tsx index 5ee42d50..690a8b67 100644 --- a/frontend/occupi-web/src/App.tsx +++ b/frontend/occupi-web/src/App.tsx @@ -36,7 +36,6 @@ function App() { } > } /> - } />*attach appropriate component } />{/**attach appropriate component */} } />{} @@ -48,6 +47,7 @@ function App() { {/* } />*attach appropriate component */} } />{/**attach appropriate component */} } />{/**attach appropriate component */} + } />*attach appropriate component diff --git a/frontend/occupi-web/src/assets/icons/Employee.tsx b/frontend/occupi-web/src/assets/icons/Employee.tsx new file mode 100644 index 00000000..1c42d5aa --- /dev/null +++ b/frontend/occupi-web/src/assets/icons/Employee.tsx @@ -0,0 +1,22 @@ +const Employee = () => { + return ( + + + + + + ); +}; + +export default Employee; diff --git a/frontend/occupi-web/src/assets/icons/UserProfileGroup.tsx b/frontend/occupi-web/src/assets/icons/UserProfileGroup.tsx index e358cdc7..7f1bc833 100644 --- a/frontend/occupi-web/src/assets/icons/UserProfileGroup.tsx +++ b/frontend/occupi-web/src/assets/icons/UserProfileGroup.tsx @@ -1,9 +1,18 @@ const UserProfileGroup = () => { - return ( - - - - ) -} + return ( + + + + ); +}; -export default UserProfileGroup \ No newline at end of file +export default UserProfileGroup; diff --git a/frontend/occupi-web/src/assets/index.ts b/frontend/occupi-web/src/assets/index.ts index 8ddaccd7..fd6b2642 100644 --- a/frontend/occupi-web/src/assets/index.ts +++ b/frontend/occupi-web/src/assets/index.ts @@ -48,12 +48,12 @@ import AI_loader from "./images/AI_loader.gif"; import LCPW from "./images/LCPW.gif"; import ailoader from "./images/ailoader.gif"; import Worker from "./icons/Worker"; - +import Employee from "./icons/Employee"; export { loginpng, OccupiLogo, login_image, CheckSquareContained, CloseDrawer, OpenDrawer, Grid, PieChart, ColorSwatch, Home, UserProfileGroup, Bell, SettingsIcon, Logout, Loading,SettingsImg, Macbook1, Macbook2, Macbook3, Userprofile, Pallete, Privacy, AlertIcon, HelpIcon, GraphCol, Cal, ChevronDown, ChevronLeft, ChevronRight, Bf, Uptrend, DownTrend, SearchIcon,ChevronDownIcon,PlusIcon,VerticalDotsIcon,EditIcon,EyeIcon,DeleteIcon,occupiLogo,Report,Faq - ,UploadButton,Upload,Bar,AI_loader,LCPW,ailoader,Worker,LoadingSM + ,UploadButton,Upload,Bar,AI_loader,LCPW,ailoader,Worker,LoadingSM,Employee } \ No newline at end of file diff --git a/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx b/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx index 90254888..45276bb2 100644 --- a/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx +++ b/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx @@ -30,7 +30,7 @@ import { SearchIcon } from "@assets/index"; import { ChevronDownIcon } from "@assets/index"; import { columns, users, statusOptions } from "../data/Data"; import { capitalize } from "../data/Utils"; -import { OccupancyModal } from "@components/index"; +import { OccupancyModal,TopNav } from "@components/index"; const statusColorMap: Record = { ONSITE: "success", @@ -370,8 +370,20 @@ export default function App() { initial={{ opacity: 0, scale: 0.7 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.2 }} + className="w-full overflow-auto" > + + Employees + + Manage your Employees, and view their occupancy statistics + + } searchQuery={""} onChange={function (e: React.ChangeEvent): void { + throw new Error("Function not implemented."); + } } + + />
{ @@ -73,16 +78,17 @@ const SideNav = () => { function setSelectedPanelF(arg: string) { setSelectedPanel(arg); - if (arg === "Dashboard")navigate("/dashboard"); - else if (arg === "AI Analysis")navigate("/ai-dashboard"); - else if (arg === "Rooms")navigate("/rooms"); - else if (arg === "Notifications")navigate("/notifications"); - else if (arg === "Settings")navigate("/settings"); - else if (arg === "Logout")navigate("/"); - else if (arg === "Reports")navigate("/reports"); - else if (arg === "Help")navigate("/faq"); - else if (arg === "Booking Statistics")navigate("/bookingStats"); - else if (arg === "Worker Dashboard")navigate("/worker-dashboard"); + if (arg === "Dashboard") navigate("/dashboard"); + else if (arg === "AI Analysis") navigate("/ai-dashboard"); + else if (arg === "Rooms") navigate("/rooms"); + else if (arg === "Notifications") navigate("/notifications"); + else if (arg === "Settings") navigate("/settings"); + else if (arg === "Logout") navigate("/"); + else if (arg === "Reports") navigate("/reports"); + else if (arg === "Help") navigate("/faq"); + else if (arg === "Booking Statistics") navigate("/bookingStats"); + else if (arg === "Worker Dashboard") navigate("/worker-dashboard"); + else if (arg === "Employees") navigate("/bookings"); else; } From df7a7b7780f924c2984fb80c07c3229695fa6db5 Mon Sep 17 00:00:00 2001 From: Tinashe-Austin Date: Thu, 26 Sep 2024 09:25:49 +0200 Subject: [PATCH 2/5] Refactor code to import Employee icon and add Employee route to SideNav component --- frontend/occupi-web/src/components/index.ts | 5 +- .../topBookings/TopBookingsBento.tsx | 109 ++++++++ .../src/pages/visitations/Visitations.tsx | 259 +++++++----------- 3 files changed, 212 insertions(+), 161 deletions(-) create mode 100644 frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx diff --git a/frontend/occupi-web/src/components/index.ts b/frontend/occupi-web/src/components/index.ts index 10d80553..ce52b859 100644 --- a/frontend/occupi-web/src/components/index.ts +++ b/frontend/occupi-web/src/components/index.ts @@ -61,7 +61,7 @@ import BookingLevelCalendar from "./aiDashboard/aiDashGraphs/BookingLevelCalen import BuildingTower from "./aiDashboard/aiDashGraphs/BuildingTower"; import OccupancyRecommendationEngine from "./occupancyRecommendationEngine/OccupancyRecommendationEngine"; import GlobalSearch from './globalSearch/GlobalSearch'; - +import TopBookingsBento from './topBookings/TopBookingsBento'; export { @@ -127,6 +127,7 @@ export { UserStatsReport, BookingLevelCalendar, BuildingTower, - OccupancyRecommendationEngine + OccupancyRecommendationEngine, + TopBookingsBento }; diff --git a/frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx b/frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx new file mode 100644 index 00000000..1b484be3 --- /dev/null +++ b/frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardBody, CardHeader, Spinner, Button, Popover, PopoverTrigger, PopoverContent } from "@nextui-org/react"; +import { Users, Calendar, Building, ChevronDown } from 'lucide-react'; + +interface Booking { + _id: string; + count: number; + creators: string[]; + emails: string[][]; + floorNo: string; + roomName: string; +} + +interface ApiResponse { + data: Booking[]; + message: string; + meta: { + currentPage: number; + totalPages: number; + totalResults: number; + }; + status: number; +} + +const TopBookingsBento = () => { + const [bookings, setBookings] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('/analytics/top-bookings'); + const data: ApiResponse = await response.json(); + setBookings(data.data); + setIsLoading(false); + } catch (err) { + setError('Failed to fetch data'); + setIsLoading(false); + } + }; + + fetchData(); + }, []); + + if (isLoading) return ; + if (error) return
Error: {error}
; + + return ( +
+ {bookings.map((booking, index) => ( + + +

{booking.roomName}

+ Floor {booking.floorNo} +
+ +
+
+ + Bookings: {booking.count} +
+
+ + Unique creators: {new Set(booking.creators).size} +
+
+ + Total participants: {booking.emails.flat().length} +
+ + + + + +
+

Booking Creators:

+
    + {Array.from(new Set(booking.creators)).map((creator, i) => ( +
  • {creator}
  • + ))} +
+

All Participants:

+
    + {Array.from(new Set(booking.emails.flat())).map((email, i) => ( +
  • {email}
  • + ))} +
+
+
+
+
+
+
+ ))} +
+ ); +}; + +export default TopBookingsBento; \ No newline at end of file diff --git a/frontend/occupi-web/src/pages/visitations/Visitations.tsx b/frontend/occupi-web/src/pages/visitations/Visitations.tsx index 8df6e674..1b484be3 100644 --- a/frontend/occupi-web/src/pages/visitations/Visitations.tsx +++ b/frontend/occupi-web/src/pages/visitations/Visitations.tsx @@ -1,168 +1,109 @@ -// import { useState, useEffect } from 'react'; -// import axios from 'axios'; -// import { motion } from 'framer-motion'; -// import { Card, CardHeader, CardBody, Grid, Text, Spacer, Avatar } from '@nextui-org/react'; -// import { Calendar, Lock, UserCircle, Mail } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { Card, CardBody, CardHeader, Spinner, Button, Popover, PopoverTrigger, PopoverContent } from "@nextui-org/react"; +import { Users, Calendar, Building, ChevronDown } from 'lucide-react'; -// interface BookingData { -// occupiID: string; -// roomName: string; -// floorNo: string; -// start: string; -// end: string; -// creators: string; -// emails: string[]; -// checkedIn: boolean; -// } - -// interface UserDetails { -// email: string; -// name: string; -// dob: string; -// gender: string; -// employeeid: string; -// number: string; -// pronouns: string; -// } - -// const BookingsCard = () => { -// const [bookings, setBookings] = useState([]); -// const [userDetails, setUserDetails] = useState(null); - -// useEffect(() => { -// const fetchBookings = async () => { -// try { -// const response = await axios.get('/analytics/bookings-historical'); -// setBookings(response.data.data); -// } catch (error) { -// console.error('Error fetching bookings:', error); -// } -// }; +interface Booking { + _id: string; + count: number; + creators: string[]; + emails: string[][]; + floorNo: string; + roomName: string; +} -// const fetchUserDetails = async () => { -// try { -// const response = await axios.get('/api/user-details?email=tintinaustin12345@gmail.com'); -// setUserDetails(response.data.data); -// } catch (error) { -// console.error('Error fetching user details:', error); -// } -// }; +interface ApiResponse { + data: Booking[]; + message: string; + meta: { + currentPage: number; + totalPages: number; + totalResults: number; + }; + status: number; +} -// fetchBookings(); -// fetchUserDetails(); -// }, []); +const TopBookingsBento = () => { + const [bookings, setBookings] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); -// return ( -// -// -// -// Bookings and User Details -// -// -// {userDetails && ( -//
-// -// -// -// -// -// {userDetails.name} -// -// {userDetails.email} -// -// -// -// {userDetails.dob} -// -// -// -// {userDetails.pronouns} -// -// -// -// {userDetails.employeeid} -// -// -// -// {userDetails.number} -// -// -// -// -//
-// )} -// Bookings -// {bookings.map((booking, index) => ( -// -// -// -// -// {booking.roomName} -// -// -// {booking.start} - {booking.end} -// -// -// -// Floor {booking.floorNo} -// -// -// -// Booking ID -// -// {booking.occupiID} -// -// Creators -// -// {booking.creators} -// -// Checked In -// -// {booking.checkedIn ? 'Yes' : 'No'} -// -// -// -// -// -// -// Attendees -// {booking.emails.map((email, i) => ( -// -// -// -// -// -// -// {email} -// -// -// -// ))} -// -// -// -// -// ))} -//
-//
-//
-// ); -// }; + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('/analytics/top-bookings'); + const data: ApiResponse = await response.json(); + setBookings(data.data); + setIsLoading(false); + } catch (err) { + setError('Failed to fetch data'); + setIsLoading(false); + } + }; -// export default BookingsCard; + fetchData(); + }, []); + if (isLoading) return ; + if (error) return
Error: {error}
; -const Visitations = () => { return ( -
Visitations
- ) -} +
+ {bookings.map((booking, index) => ( + + +

{booking.roomName}

+ Floor {booking.floorNo} +
+ +
+
+ + Bookings: {booking.count} +
+
+ + Unique creators: {new Set(booking.creators).size} +
+
+ + Total participants: {booking.emails.flat().length} +
+ + + + + +
+

Booking Creators:

+
    + {Array.from(new Set(booking.creators)).map((creator, i) => ( +
  • {creator}
  • + ))} +
+

All Participants:

+
    + {Array.from(new Set(booking.emails.flat())).map((email, i) => ( +
  • {email}
  • + ))} +
+
+
+
+
+
+
+ ))} +
+ ); +}; -export default Visitations \ No newline at end of file +export default TopBookingsBento; \ No newline at end of file From 8d1ff4fc522fb6fbb3ae32449c1482f57fcba75c Mon Sep 17 00:00:00 2001 From: Tinashe-Austin Date: Thu, 26 Sep 2024 09:27:50 +0200 Subject: [PATCH 3/5] Refactor code to import Employee icon and add Employee route to SideNav component --- frontend/occupi-web/src/components/index.ts | 34 ++++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/frontend/occupi-web/src/components/index.ts b/frontend/occupi-web/src/components/index.ts index ce52b859..97452994 100644 --- a/frontend/occupi-web/src/components/index.ts +++ b/frontend/occupi-web/src/components/index.ts @@ -57,12 +57,11 @@ import UserPeakOfficeHoursChart from "./modal/UserPeakOfficeHoursChart"; import UserWorkRatioChart from "./modal/UserWorkRatioChart"; import AvgArrDep from "./modal/AvgArrDep"; import UserStatsReport from "./modal/UserStatsReport"; -import BookingLevelCalendar from "./aiDashboard/aiDashGraphs/BookingLevelCalendar"; +import BookingLevelCalendar from "./aiDashboard/aiDashGraphs/BookingLevelCalendar"; import BuildingTower from "./aiDashboard/aiDashGraphs/BuildingTower"; import OccupancyRecommendationEngine from "./occupancyRecommendationEngine/OccupancyRecommendationEngine"; -import GlobalSearch from './globalSearch/GlobalSearch'; -import TopBookingsBento from './topBookings/TopBookingsBento'; - +import GlobalSearch from "./globalSearch/GlobalSearch"; +import TopBookingsBento from "./topBookings/TopBookingsBento"; export { DrawerComponent, @@ -108,7 +107,7 @@ export { Security, EditRoomModal, AddRoomModal, - GlobalSearch, + GlobalSearch, ActiveEmployeeCard, AverageHoursChart, HoursDashboard, @@ -117,17 +116,16 @@ export { WorkRatioChart, MostActiveEmployeeCard, UserStatsComponent, - BarChartComponent, - PieChartComponent, - PieChartPeakHoursComponent, - UserHoursCharts, - UserPeakOfficeHoursChart, - UserWorkRatioChart, - AvgArrDep, - UserStatsReport, - BookingLevelCalendar, - BuildingTower, - OccupancyRecommendationEngine, - TopBookingsBento - + BarChartComponent, + PieChartComponent, + PieChartPeakHoursComponent, + UserHoursCharts, + UserPeakOfficeHoursChart, + UserWorkRatioChart, + AvgArrDep, + UserStatsReport, + BookingLevelCalendar, + BuildingTower, + OccupancyRecommendationEngine, + TopBookingsBento, }; From b4ab22cd48663b260ca3485cff67cfec4d8813b9 Mon Sep 17 00:00:00 2001 From: Tinashe-Austin Date: Thu, 26 Sep 2024 14:26:13 +0200 Subject: [PATCH 4/5] Web Fixes and Bookings Component --- frontend/occupi-web/src/App.tsx | 14 +- .../bookingComponent/BookingComponent.tsx | 2 +- .../BookingsDashboard.tsx | 39 ++++ .../currentBookings/CurrentBookingsBento.tsx | 174 +++++++++++++++++ .../HistoricalBookingsBento.tsx | 181 ++++++++++++++++++ frontend/occupi-web/src/components/index.ts | 6 + .../components/tabComponent/TabComponent.tsx | 72 ++++--- .../topBookings/TopBookingsBento.tsx | 4 +- .../src/components/topNav/TopNav.tsx | 98 +++++----- .../src/pages/Dashboard/Dashboard.tsx | 15 +- .../src/pages/bookingStats/bookingStats.tsx | 9 +- .../bookingsDash/BookingsDashboardPage.tsx | 43 +++++ frontend/occupi-web/src/pages/index.ts | 5 +- .../src/pages/visitations/Visitations.tsx | 131 +++---------- .../src/pages/worker-dash/WorkerDashboard.tsx | 14 +- 15 files changed, 606 insertions(+), 201 deletions(-) create mode 100644 frontend/occupi-web/src/components/bookingsDashboardComponent/BookingsDashboard.tsx create mode 100644 frontend/occupi-web/src/components/currentBookings/CurrentBookingsBento.tsx create mode 100644 frontend/occupi-web/src/components/historicalBookings/HistoricalBookingsBento.tsx create mode 100644 frontend/occupi-web/src/pages/bookingsDash/BookingsDashboardPage.tsx diff --git a/frontend/occupi-web/src/App.tsx b/frontend/occupi-web/src/App.tsx index 690a8b67..0ee3d7b7 100644 --- a/frontend/occupi-web/src/App.tsx +++ b/frontend/occupi-web/src/App.tsx @@ -1,4 +1,4 @@ -import { LoginForm, OtpPage, Settings, Dashboard, Analysis, Visitation, Faq, AiDashboard, Rooms, AboutPage, SecurityPage,BookingStats ,WorkerStatsDashboard} from "@pages/index"; +import { LoginForm, OtpPage, Settings, Dashboard, Faq, AiDashboard, Rooms, AboutPage, SecurityPage, BookingStats, WorkerStatsDashboard, BookingsDashboardPage } from "@pages/index"; import { Appearance, OverviewComponent, BookingComponent, PDFReport, ProfileView } from "@components/index"; import { Layout } from "@layouts/index"; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; @@ -36,10 +36,10 @@ function App() { } > } /> - } />{/**attach appropriate component */} - } />{} + + } />{/**attach appropriate component */} } />{/**attach appropriate component */} } />{/**consider making ths its own page */} @@ -48,6 +48,14 @@ function App() { } />{/**attach appropriate component */} } />{/**attach appropriate component */} } />*attach appropriate component + {/* } />*attach appropriate component */} + + + } > + {/* } />*attach appropriate component */} + + + diff --git a/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx b/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx index 45276bb2..4b5815d8 100644 --- a/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx +++ b/frontend/occupi-web/src/components/bookingComponent/BookingComponent.tsx @@ -379,7 +379,7 @@ export default function App() { Manage your Employees, and view their occupancy statistics - } searchQuery={""} onChange={function (e: React.ChangeEvent): void { + } searchQuery={""} onChange={function (): void { throw new Error("Function not implemented."); } } diff --git a/frontend/occupi-web/src/components/bookingsDashboardComponent/BookingsDashboard.tsx b/frontend/occupi-web/src/components/bookingsDashboardComponent/BookingsDashboard.tsx new file mode 100644 index 00000000..ce02ecec --- /dev/null +++ b/frontend/occupi-web/src/components/bookingsDashboardComponent/BookingsDashboard.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { Tabs, Tab } from "@nextui-org/react"; + +import { + HistoricalBookingsBento, + CurrentBookingsBento, + TopBookingsBento, +} from "@components/index"; + +const BookingsDashboard = () => { + const [selected, setSelected] = useState("top"); + + return ( +
+ + + setSelected(key as string)} + + > + + + + + + + + + + + +
+ ); +}; + +export default BookingsDashboard; diff --git a/frontend/occupi-web/src/components/currentBookings/CurrentBookingsBento.tsx b/frontend/occupi-web/src/components/currentBookings/CurrentBookingsBento.tsx new file mode 100644 index 00000000..a80c6fa0 --- /dev/null +++ b/frontend/occupi-web/src/components/currentBookings/CurrentBookingsBento.tsx @@ -0,0 +1,174 @@ +import { useState, useEffect } from 'react'; +import { Card, CardBody, CardHeader, Input, Button, Popover, PopoverTrigger, PopoverContent, Spinner } from "@nextui-org/react"; +import { Calendar, Clock, Users, Search, ChevronDown } from 'lucide-react'; + +interface Booking { + checkedIn: boolean; + creators: string; + date: string; + emails: string[]; + end: string; + floorNo: string; + occupiID: string; + roomId: string; + roomName: string; + start: string; +} + +interface ApiResponse { + data: Booking[] | null; + message: string; + meta: { + currentPage: number; + totalPages: number; + totalResults: number; + }; + status: number; +} + +const CurrentBookingsBento = () => { + const [bookings, setBookings] = useState([]); + const [filteredBookings, setFilteredBookings] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [searchRoom, setSearchRoom] = useState(''); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('/analytics/bookings-current'); + const data: ApiResponse = await response.json(); + if (data.data) { + setBookings(data.data); + setFilteredBookings(data.data); + } else { + setBookings([]); + setFilteredBookings([]); + } + setIsLoading(false); + } catch (err) { + setError('Failed to fetch data'); + setIsLoading(false); + } + }; + + fetchData(); + }, []); + + useEffect(() => { + if (searchRoom) { + const filtered = bookings.filter(booking => + booking.roomName.toLowerCase().includes(searchRoom.toLowerCase()) + ); + setFilteredBookings(filtered); + } else { + setFilteredBookings(bookings); + } + }, [searchRoom, bookings]); + + const formatTime = (timeString: string) => { + return new Date(timeString).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit' + }); + }; + + if (isLoading) return ; + if (error) return
Error: {error}
; + + return ( +
+ setSearchRoom(e.target.value)} + startContent={} + /> + {filteredBookings.length > 0 ? ( +
+ {filteredBookings.slice(0, 3).map((booking) => ( + + +

{booking.roomName}

+ Floor {booking.floorNo} +
+ +
+
+ + {new Date(booking.date).toLocaleDateString()} +
+
+ + {formatTime(booking.start)} - {formatTime(booking.end)} +
+
+ + {booking.emails.length} participant(s) +
+ + + + + +
+

Creator:

+

{booking.creators}

+

Participants:

+
    + {booking.emails.map((email, i) => ( +
  • {email}
  • + ))} +
+

+ Checked In: {booking.checkedIn ? 'Yes' : 'No'} +

+
+
+
+
+
+
+ ))} +
+ ) : ( + + +

No current bookings available.

+
+
+ )} + {filteredBookings.length > 3 && ( + + + + + +
+ {filteredBookings.slice(3).map((booking) => ( +
+

{booking.roomName}

+

{new Date(booking.date).toLocaleDateString()}

+

{formatTime(booking.start)} - {formatTime(booking.end)}

+
+ ))} +
+
+
+ )} +
+ ); +}; + +export default CurrentBookingsBento; \ No newline at end of file diff --git a/frontend/occupi-web/src/components/historicalBookings/HistoricalBookingsBento.tsx b/frontend/occupi-web/src/components/historicalBookings/HistoricalBookingsBento.tsx new file mode 100644 index 00000000..3b6468b6 --- /dev/null +++ b/frontend/occupi-web/src/components/historicalBookings/HistoricalBookingsBento.tsx @@ -0,0 +1,181 @@ +import { useState, useEffect } from 'react'; +import { Card, CardBody, CardHeader, Input, Button, Popover, PopoverTrigger, PopoverContent, Spinner } from "@nextui-org/react"; +import { Calendar, Clock, Users, Search, ChevronDown } from 'lucide-react'; + +interface Booking { + checkedIn: boolean; + creators: string; + date: string; + emails: string[]; + end: string; + floorNo: string; + occupiID: string; + roomId: string; + roomName: string; + start: string; +} + +interface ApiResponse { + data: Booking[]; + message: string; + meta: { + currentPage: number; + totalPages: number; + totalResults: number; + }; + status: number; +} + +const HistoricalBookingsBento = () => { + const [bookings, setBookings] = useState([]); + const [filteredBookings, setFilteredBookings] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [searchDate, setSearchDate] = useState(''); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch('/analytics/bookings-historical'); + const data: ApiResponse = await response.json(); + setBookings(data.data); + setFilteredBookings(data.data); + setIsLoading(false); + } catch (err) { + if (err instanceof Error) { + setError('Failed to fetch data: ' + err.message); + } else { + setError('Failed to fetch data'); + } + setIsLoading(false); + } + }; + + fetchData(); + }, []); + + useEffect(() => { + if (searchDate) { + const filtered = bookings.filter(booking => + booking.date.startsWith(searchDate) + ); + setFilteredBookings(filtered); + } else { + setFilteredBookings(bookings); + } + }, [searchDate, bookings]); + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const formatTime = (timeString: string) => { + return new Date(timeString).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit' + }); + }; + + if (isLoading) return ; + if (error) return
Error: {error}
; + + return ( +
+ setSearchDate(e.target.value)} + startContent={} + /> + + {/* Display a message if no bookings match the search criteria */} + {filteredBookings.length === 0 && ( +
No bookings found for the selected date.
+ )} + +
+ {filteredBookings.slice(0, 3).map((booking) => ( + + +

{booking.roomName}

+ Floor {booking.floorNo} +
+ +
+
+ + {formatDate(booking.date)} +
+
+ + {formatTime(booking.start)} - {formatTime(booking.end)} +
+
+ + {booking.emails.length} participant(s) +
+ + + + + +
+

Creator:

+

{booking.creators}

+

Participants:

+
    + {booking.emails.map((email, i) => ( +
  • {email}
  • + ))} +
+

+ Checked In: {booking.checkedIn ? 'Yes' : 'No'} +

+
+
+
+
+
+
+ ))} +
+ + {/* Only show "View More Bookings" if there are more than 3 bookings */} + {filteredBookings.length > 3 && ( + + + + + +
+ {filteredBookings.slice(3).map((booking) => ( +
+

{booking.roomName}

+

{formatDate(booking.date)}

+

{formatTime(booking.start)} - {formatTime(booking.end)}

+
+ ))} +
+
+
+ )} +
+ ); +}; + +export default HistoricalBookingsBento; diff --git a/frontend/occupi-web/src/components/index.ts b/frontend/occupi-web/src/components/index.ts index 97452994..ddfab870 100644 --- a/frontend/occupi-web/src/components/index.ts +++ b/frontend/occupi-web/src/components/index.ts @@ -62,6 +62,9 @@ import BuildingTower from "./aiDashboard/aiDashGraphs/BuildingTower"; import OccupancyRecommendationEngine from "./occupancyRecommendationEngine/OccupancyRecommendationEngine"; import GlobalSearch from "./globalSearch/GlobalSearch"; import TopBookingsBento from "./topBookings/TopBookingsBento"; +import HistoricalBookingsBento from "./historicalBookings/HistoricalBookingsBento"; +import CurrentBookingsBento from "./currentBookings/CurrentBookingsBento"; +import BookingsDashboard from "./bookingsDashboardComponent/BookingsDashboard"; export { DrawerComponent, @@ -128,4 +131,7 @@ export { BuildingTower, OccupancyRecommendationEngine, TopBookingsBento, + HistoricalBookingsBento, + CurrentBookingsBento, + BookingsDashboard, }; diff --git a/frontend/occupi-web/src/components/tabComponent/TabComponent.tsx b/frontend/occupi-web/src/components/tabComponent/TabComponent.tsx index 9b59e2f3..87498c2d 100644 --- a/frontend/occupi-web/src/components/tabComponent/TabComponent.tsx +++ b/frontend/occupi-web/src/components/tabComponent/TabComponent.tsx @@ -1,46 +1,70 @@ -import { useState } from "react"; +import React, { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { FaChevronDown } from "react-icons/fa"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; + +type Tab = { + name: string; + path: string; + index: number; +}; type TabComponentProps = { setSelectedTab: (arg: string) => void; }; -const TabComponent = (props: TabComponentProps) => { - const [activeTab, setActiveTab] = useState(1); +const TabComponent: React.FC = (props) => { + const [activeTab, setActiveTab] = useState(null); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const navigate = useNavigate(); + const location = useLocation(); - const tabs = [ - { name: "Overview", path: "overview", index: 1 }, - { name: "Employees", path: "bookings", index: 2 }, - { name: "Visitations", path: "visitations", index: 3 } + const tabs: Tab[] = [ + { name: "Overview", path: "", index: 1 }, + { name: "Bookings", path: "bookingsDashboard", index: 2 }, + // { name: "Visitations", path: "visitations", index: 3 } ]; - const handleTabClick = (tab: { name: string; path: string; index: number }) => { - setActiveTab(tab.index); + useEffect(() => { + const currentPath = location.pathname.split('/').pop() || ''; + const currentTab = tabs.find(tab => tab.path === currentPath) || tabs[0]; + setActiveTab(currentTab); + }, [location]); + + const handleTabClick = (tab: Tab) => { + setActiveTab(tab); setIsDropdownOpen(false); props.setSelectedTab(tab.path); navigate(tab.path); }; + const tabWidth = 95; // Width of each tab + const tabSpacing = 5; // Spacing between tabs + const backgroundPadding = 20; // Additional padding on each side of the background + const totalWidth = tabs.length * tabWidth + (tabs.length - 1) * tabSpacing + 2 * backgroundPadding; + return (
{/* Desktop view */} -
- {tabs.map((tab) => ( - handleTabClick(tab)} - > -

{tab.name}

-
- ))} +
+
+ {tabs.map((tab, index) => ( + handleTabClick(tab)} + > +

{tab.name}

+
+ ))} +
{/* Mobile view */} @@ -50,7 +74,7 @@ const TabComponent = (props: TabComponentProps) => { className="flex items-center justify-between w-[120px] h-[36px] rounded-[10px] bg-secondary px-3" onClick={() => setIsDropdownOpen(!isDropdownOpen)} > - {tabs.find(tab => tab.index === activeTab)?.name} + {activeTab?.name} diff --git a/frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx b/frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx index 1b484be3..c294aeb9 100644 --- a/frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx +++ b/frontend/occupi-web/src/components/topBookings/TopBookingsBento.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Card, CardBody, CardHeader, Spinner, Button, Popover, PopoverTrigger, PopoverContent } from "@nextui-org/react"; import { Users, Calendar, Building, ChevronDown } from 'lucide-react'; @@ -47,7 +47,7 @@ const TopBookingsBento = () => { if (error) return
Error: {error}
; return ( -
+
{bookings.map((booking, index) => ( { const [isSearchVisible, setIsSearchVisible] = useState(false); return ( -
- {props.mainComponent} + {props.mainComponent} - {/*
- -
*/} +
+
+ +
-
- -
+ {/* Mobile search visibility and animated search box */} + + {isSearchVisible && ( + +
+ + setIsSearchVisible(false)} + > + + +
+
+ )} +
- {/* Mobile search visibility and animated search box */} - - {isSearchVisible && ( - + setIsSearchVisible(!isSearchVisible)} > -
- - setIsSearchVisible(false)} - > - - -
-
- )} -
- - {/* Action icons */} -
- setIsSearchVisible(!isSearchVisible)} - > - - - {/* Future ProfileDropDown component can go here */} + + + {/* Future ProfileDropDown component can go here */} +
); }; -export default TopNav; +export default TopNav; \ No newline at end of file diff --git a/frontend/occupi-web/src/pages/Dashboard/Dashboard.tsx b/frontend/occupi-web/src/pages/Dashboard/Dashboard.tsx index 5d158cea..6049a3f9 100644 --- a/frontend/occupi-web/src/pages/Dashboard/Dashboard.tsx +++ b/frontend/occupi-web/src/pages/Dashboard/Dashboard.tsx @@ -1,8 +1,5 @@ import { TopNav } from "@components/index"; import { useState, useEffect } from "react"; -import { - TabComponent, -} from "@components/index"; import { useNavigate, Outlet } from "react-router-dom"; const Dashboard = () => { @@ -26,8 +23,16 @@ const Dashboard = () => { return (
- } + + Overview + + See your Statistics at a glance + +
+ + } searchQuery={searchQuery} onChange={handleInputChange} /> diff --git a/frontend/occupi-web/src/pages/bookingStats/bookingStats.tsx b/frontend/occupi-web/src/pages/bookingStats/bookingStats.tsx index 80be0d40..f6fdb9c7 100644 --- a/frontend/occupi-web/src/pages/bookingStats/bookingStats.tsx +++ b/frontend/occupi-web/src/pages/bookingStats/bookingStats.tsx @@ -3,7 +3,7 @@ import { BuildingTower, BookingLevelCalendar, OccupancyRecommendationEngine, - TopNav, + TopNav,TabComponent } from "@components/index"; import { motion, AnimatePresence } from "framer-motion"; import { Info } from "lucide-react"; @@ -78,12 +78,7 @@ const BookingStats: React.FC = () => { > - Booking Statistics{" "} - - Use the Recomendations and view Predicted occupancy at a glance.{" "} - -
+ {}} /> } searchQuery={searchQuery} onChange={handleInputChange} diff --git a/frontend/occupi-web/src/pages/bookingsDash/BookingsDashboardPage.tsx b/frontend/occupi-web/src/pages/bookingsDash/BookingsDashboardPage.tsx new file mode 100644 index 00000000..dfab642e --- /dev/null +++ b/frontend/occupi-web/src/pages/bookingsDash/BookingsDashboardPage.tsx @@ -0,0 +1,43 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, Outlet } from 'react-router-dom'; +import { TopNav, TabComponent, BookingsDashboard } from '@components/index'; + +const BookingsDashboardPage = () => { + const [searchQuery, setSearchQuery] = useState(''); + const navigate = useNavigate(); + + const handleClick = (path: string) => { + navigate('/bookings' + path); + }; + + const handleInputChange = (e: { target: { value: React.SetStateAction; }; }) => { + setSearchQuery(e.target.value); + }; + + useEffect(() => { + const path = window.location.pathname; + if (path === '/bookings') { + handleClick('/top'); + } + }, []); + + + + + return ( +
+ } + searchQuery={searchQuery} + onChange={handleInputChange} + /> +
+ + +
+ +
+ ); +}; + +export default BookingsDashboardPage; \ No newline at end of file diff --git a/frontend/occupi-web/src/pages/index.ts b/frontend/occupi-web/src/pages/index.ts index d4b9d257..8d7d2b46 100644 --- a/frontend/occupi-web/src/pages/index.ts +++ b/frontend/occupi-web/src/pages/index.ts @@ -21,7 +21,7 @@ import BuildingTower from "@components/aiDashboard/aiDashGraphs/BuildingTower"; import BookingLevelCalendar from "@components/aiDashboard/aiDashGraphs/BookingLevelCalendar"; import BookingStats from "./bookingStats/bookingStats"; import WorkerStatsDashboard from "./worker-dash/WorkerDashboard"; - +import BookingsDashboardPage from "./bookingsDash/BookingsDashboardPage"; export { LoginForm, OtpPage, @@ -45,7 +45,8 @@ export { BuildingTower, BookingLevelCalendar, BookingStats, - WorkerStatsDashboard + WorkerStatsDashboard, + BookingsDashboardPage }; diff --git a/frontend/occupi-web/src/pages/visitations/Visitations.tsx b/frontend/occupi-web/src/pages/visitations/Visitations.tsx index 1b484be3..790ee177 100644 --- a/frontend/occupi-web/src/pages/visitations/Visitations.tsx +++ b/frontend/occupi-web/src/pages/visitations/Visitations.tsx @@ -1,109 +1,38 @@ -import React, { useState, useEffect } from 'react'; -import { Card, CardBody, CardHeader, Spinner, Button, Popover, PopoverTrigger, PopoverContent } from "@nextui-org/react"; -import { Users, Calendar, Building, ChevronDown } from 'lucide-react'; +import { useState } from "react"; +import { Tabs, Tab } from "@nextui-org/react"; -interface Booking { - _id: string; - count: number; - creators: string[]; - emails: string[][]; - floorNo: string; - roomName: string; -} +import { + HistoricalBookingsBento, + CurrentBookingsBento, + TopBookingsBento, +} from "@components/index"; -interface ApiResponse { - data: Booking[]; - message: string; - meta: { - currentPage: number; - totalPages: number; - totalResults: number; - }; - status: number; -} - -const TopBookingsBento = () => { - const [bookings, setBookings] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await fetch('/analytics/top-bookings'); - const data: ApiResponse = await response.json(); - setBookings(data.data); - setIsLoading(false); - } catch (err) { - setError('Failed to fetch data'); - setIsLoading(false); - } - }; - - fetchData(); - }, []); - - if (isLoading) return ; - if (error) return
Error: {error}
; +const BookingsDashboard = () => { + const [selected, setSelected] = useState("top"); return ( -
- {bookings.map((booking, index) => ( - - -

{booking.roomName}

- Floor {booking.floorNo} -
- -
-
- - Bookings: {booking.count} -
-
- - Unique creators: {new Set(booking.creators).size} -
-
- - Total participants: {booking.emails.flat().length} -
- - - - - -
-

Booking Creators:

-
    - {Array.from(new Set(booking.creators)).map((creator, i) => ( -
  • {creator}
  • - ))} -
-

All Participants:

-
    - {Array.from(new Set(booking.emails.flat())).map((email, i) => ( -
  • {email}
  • - ))} -
-
-
-
-
-
-
- ))} +
+ + + setSelected(key as string)} + > + + + + + + + + + + +
); }; -export default TopBookingsBento; \ No newline at end of file +export default BookingsDashboard; diff --git a/frontend/occupi-web/src/pages/worker-dash/WorkerDashboard.tsx b/frontend/occupi-web/src/pages/worker-dash/WorkerDashboard.tsx index 9f1a1400..5d01ef23 100644 --- a/frontend/occupi-web/src/pages/worker-dash/WorkerDashboard.tsx +++ b/frontend/occupi-web/src/pages/worker-dash/WorkerDashboard.tsx @@ -256,11 +256,15 @@ const WorkerStatsDashboard: React.FC = () => { return (
- { - console.log("Search query changed"); - }} + + Employee Statistics + + See all Your Employee Statistics from Occubot + +
} searchQuery={""} onChange={function (): void { + throw new Error("Function not implemented."); + } } />