From 36364e7a6dc144631fc17cc559f9551a82c17a4e Mon Sep 17 00:00:00 2001 From: margoglvz Date: Thu, 30 Jan 2025 12:40:37 -0800 Subject: [PATCH 01/41] testing home --- client/src/App.jsx | 5 ++++ client/src/components/home/Home.jsx | 13 +++++++++++ client/src/components/home/HomeComponents.jsx | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 client/src/components/home/Home.jsx create mode 100644 client/src/components/home/HomeComponents.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index ccda6c8..1bd5c97 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -17,6 +17,7 @@ import { AuthProvider } from "./contexts/AuthContext"; import { BackendProvider } from "./contexts/BackendContext"; import { RoleProvider } from "./contexts/RoleContext"; import { ForgotPassword } from "./components/login/ForgotPassword"; +import { Home } from "./components/home/Home"; import { PDFViewer } from "@react-pdf/renderer"; import PDFButton from "./components/PDFButton"; @@ -44,6 +45,10 @@ const App = () => { path="/dashboard" element={} />} /> + } + /> { + return ( + + + + ); +}; + +// export default Home; \ No newline at end of file diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx new file mode 100644 index 0000000..30a4d3d --- /dev/null +++ b/client/src/components/home/HomeComponents.jsx @@ -0,0 +1,23 @@ +import { Table, Thead, Tbody, Tfoot, Tr, Th, Td } from "@chakra-ui/react"; + +export const ProgramsTable = () => { + return ( + + + + {/* Add column names */} + + + + + {/* Add data inside + + + + + + +
Header
Data */} +
Footer
+ ); +}; From b463bb9188da83a0820391d3b3776c512415f31a Mon Sep 17 00:00:00 2001 From: margoglvz Date: Thu, 30 Jan 2025 13:21:41 -0800 Subject: [PATCH 02/41] updated home --- client/src/components/home/Home.jsx | 4 +- client/src/components/home/HomeComponents.jsx | 92 +++++++++++++++---- 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/client/src/components/home/Home.jsx b/client/src/components/home/Home.jsx index e6d3550..c4e3294 100644 --- a/client/src/components/home/Home.jsx +++ b/client/src/components/home/Home.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Box } from '@chakra-ui/react'; -import ProgramsTable from './HomeComponents'; +import { ProgramsTable } from './HomeComponents'; export const Home = () => { return ( @@ -9,5 +9,3 @@ export const Home = () => { ); }; - -// export default Home; \ No newline at end of file diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 30a4d3d..e615e3b 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -1,23 +1,79 @@ -import { Table, Thead, Tbody, Tfoot, Tr, Th, Td } from "@chakra-ui/react"; +import React, { useState, useEffect } from 'react'; +import { Table, Thead, Tbody, Tr, Th, Td, TableContainer } from "@chakra-ui/react"; +import { useNavigate } from 'react-router-dom'; +import { useBackendContext } from "../../contexts/hooks/useBackendContext"; export const ProgramsTable = () => { + const [programs, setPrograms] = useState([]); + const { backend } = useBackendContext(); + const navigate = useNavigate(); + + const fetchPrograms = async () => { + try { + const eventsResponse = await backend.get('/events'); + const eventsData = eventsResponse.data; + + const programsData = eventsData.map(event => ({ + id: event.id, + name: event.name, + description: event.description, + // Placeholder values for fields we don't have yet + status: 'Active', + upcomingDate: 'TBD', + upcomingTime: 'TBD', + room: 'TBD', + instructor: 'TBD', + payee: 'TBD' + })); + + setPrograms(programsData); + } catch (error) { + console.error('Failed to fetch programs:', error); + } + }; + + useEffect(() => { + fetchPrograms(); + }, []); + + const handleRowClick = (id) => { + navigate(`/programs/${id}`); + }; + return ( - - - - {/* Add column names */} - - - - - {/* Add data inside - - - - - - -
Header
Data */} -
Footer
+ + + + + + + + + + + + + + + {programs.map((program) => ( + handleRowClick(program.id)} cursor="pointer"> + + + + + + + + + ))} + +
Program NameStatusUpcoming DateUpcoming TimeRoomInstructorPayee
{program.name}{program.status}{program.upcomingDate}{program.upcomingTime}{program.room || 'N/A'}{program.instructor}{program.payee}
+
); }; + +// What We Need From Tables + +// Assignment (client_id, role), Events (id, name, archived), Bookings (date, start_time, end_time, room_id), Rooms (name), Client (name) + + From 5bfd758a7b0f04eb43dd9d3fef6654f566470009 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Thu, 30 Jan 2025 14:22:24 -0800 Subject: [PATCH 03/41] feat: include active status and upcoming time/date of next booking for programs table --- client/src/components/home/HomeComponents.jsx | 157 +++++++++++++----- 1 file changed, 115 insertions(+), 42 deletions(-) diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index e615e3b..34e34cc 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -1,31 +1,60 @@ import React, { useState, useEffect } from 'react'; -import { Table, Thead, Tbody, Tr, Th, Td, TableContainer } from "@chakra-ui/react"; +import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Menu, MenuButton, MenuList, MenuItem, IconButton, Input, Button } from "@chakra-ui/react"; import { useNavigate } from 'react-router-dom'; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; +import { FiMoreVertical, FiFilter } from 'react-icons/fi'; export const ProgramsTable = () => { const [programs, setPrograms] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); const { backend } = useBackendContext(); const navigate = useNavigate(); const fetchPrograms = async () => { try { const eventsResponse = await backend.get('/events'); + const bookingsResponse = await backend.get('/bookings'); const eventsData = eventsResponse.data; - - const programsData = eventsData.map(event => ({ - id: event.id, - name: event.name, - description: event.description, - // Placeholder values for fields we don't have yet - status: 'Active', - upcomingDate: 'TBD', - upcomingTime: 'TBD', - room: 'TBD', - instructor: 'TBD', - payee: 'TBD' - })); - + const bookingsData = bookingsResponse.data; + + const currentDate = new Date(); + + const formatDate = (dateString) => { + const date = new Date(dateString); + const options = { weekday: 'short', month: '2-digit', day: '2-digit', year: 'numeric' }; + return date.toLocaleDateString('en-US', options).replace(/,/g, '.'); + }; + + const formatTime = (timeString) => { + const [hours, minutes] = timeString.split(':'); + const date = new Date(2000, 0, 1, hours, minutes); + return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }).toLowerCase(); + }; + + const programsData = eventsData.map(event => { + const eventBookings = bookingsData.filter(booking => booking.eventId === event.id); + const upcomingBookings = eventBookings.filter(booking => new Date(booking.date) > currentDate); + const upcomingBooking = upcomingBookings.length > 0 + ? upcomingBookings.reduce((earliest, current) => + new Date(current.date) < new Date(earliest.date) ? current : earliest + ) + : null; + + return { + id: event.id, + name: event.name, + description: event.description, + status: upcomingBooking ? 'Active' : (eventBookings.length > 0 ? 'Past' : 'No bookings'), + upcomingDate: upcomingBooking ? formatDate(upcomingBooking.date) : 'No upcoming bookings', + upcomingTime: upcomingBooking + ? `${formatTime(upcomingBooking.startTime)} - ${formatTime(upcomingBooking.endTime)}` + : 'N/A', + room: 'TBD', + instructor: 'TBD', + payee: 'TBD' + }; + }); + setPrograms(programsData); } catch (error) { console.error('Failed to fetch programs:', error); @@ -40,35 +69,79 @@ export const ProgramsTable = () => { navigate(`/programs/${id}`); }; + const handleEdit = (id, e) => { + e.stopPropagation(); + navigate(`/programs/${id}`); + }; + + const handleDelete = async (id, e) => { + e.stopPropagation(); + try { + await backend.delete(`/events/${id}`); + setPrograms(programs.filter(program => program.id !== id)); + } catch (error) { + console.error('Failed to delete program:', error); + } + }; + + const filteredPrograms = programs.filter(program => + program.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + return ( - - - - - - - - - - - - - - - {programs.map((program) => ( - handleRowClick(program.id)} cursor="pointer"> - - - - - - - + <> +
+ + setSearchTerm(e.target.value)} + /> +
+ +
Program NameStatusUpcoming DateUpcoming TimeRoomInstructorPayee
{program.name}{program.status}{program.upcomingDate}{program.upcomingTime}{program.room || 'N/A'}{program.instructor}{program.payee}
+ + + + + + + + + + - ))} - -
Program NameStatusUpcoming DateUpcoming TimeRoomInstructorPayee
-
+ + + {filteredPrograms.map((program) => ( + handleRowClick(program.id)} cursor="pointer"> + {program.name} + {program.status} + {program.upcomingDate} + {program.upcomingTime} + {program.room} + {program.instructor} + {program.payee} + e.stopPropagation()}> + + } + variant='outline' + /> + + handleEdit(program.id, e)}>Edit + handleDelete(program.id, e)}>Delete + + + + + ))} + + + + ); }; From 1bfe3cdb8406e509871d6b105b5ddf99cbf2c82c Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Thu, 30 Jan 2025 16:03:12 -0800 Subject: [PATCH 04/41] feat: add rooms, instructors/payees to programs table and ensure most recent date is still included if "past" status --- client/src/components/home/HomeComponents.jsx | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 34e34cc..1f084c2 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -1,3 +1,9 @@ +// notes:: +// idk if we ned to remove a program if theres no upcoming bookings +// also if there's multiple instructors this only does 1 unfortunately +// also do fuzzy search on name or nah? + + import React, { useState, useEffect } from 'react'; import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Menu, MenuButton, MenuList, MenuItem, IconButton, Input, Button } from "@chakra-ui/react"; import { useNavigate } from 'react-router-dom'; @@ -14,8 +20,15 @@ export const ProgramsTable = () => { try { const eventsResponse = await backend.get('/events'); const bookingsResponse = await backend.get('/bookings'); + const roomsResponse = await backend.get('/rooms'); + const assignmentsResponse = await backend.get('/assignments'); + const clientsResponse = await backend.get('/clients'); + const eventsData = eventsResponse.data; const bookingsData = bookingsResponse.data; + const roomsData = roomsResponse.data; + const assignmentsData = assignmentsResponse.data; + const clientsData = clientsResponse.data; const currentDate = new Date(); @@ -34,24 +47,53 @@ export const ProgramsTable = () => { const programsData = eventsData.map(event => { const eventBookings = bookingsData.filter(booking => booking.eventId === event.id); const upcomingBookings = eventBookings.filter(booking => new Date(booking.date) > currentDate); - const upcomingBooking = upcomingBookings.length > 0 - ? upcomingBookings.reduce((earliest, current) => - new Date(current.date) < new Date(earliest.date) ? current : earliest - ) + const pastBookings = eventBookings.filter(booking => new Date(booking.date) <= currentDate); + + let relevantBooking; + let status; + + if (upcomingBookings.length > 0) { + relevantBooking = upcomingBookings.reduce((earliest, current) => + new Date(current.date) < new Date(earliest.date) ? current : earliest + ); + status = 'Active'; + } else if (pastBookings.length > 0) { + relevantBooking = pastBookings.reduce((latest, current) => + new Date(current.date) > new Date(latest.date) ? current : latest + ); + status = 'Past'; + } else { + status = 'No bookings'; + } + + const room = relevantBooking + ? roomsData.find(room => room.id === relevantBooking.roomId) + : null; + + const eventAssignments = assignmentsData.filter(assignment => assignment.eventId === event.id); + const instructorAssignment = eventAssignments.find(assignment => assignment.role === 'instructor'); + const payeeAssignment = eventAssignments.find(assignment => assignment.role === 'payee'); + + const instructor = instructorAssignment + ? clientsData.find(client => client.id === instructorAssignment.clientId) + : null; + + const payee = payeeAssignment + ? clientsData.find(client => client.id === payeeAssignment.clientId) : null; return { id: event.id, name: event.name, description: event.description, - status: upcomingBooking ? 'Active' : (eventBookings.length > 0 ? 'Past' : 'No bookings'), - upcomingDate: upcomingBooking ? formatDate(upcomingBooking.date) : 'No upcoming bookings', - upcomingTime: upcomingBooking - ? `${formatTime(upcomingBooking.startTime)} - ${formatTime(upcomingBooking.endTime)}` + status: status, + upcomingDate: relevantBooking ? formatDate(relevantBooking.date) : 'No bookings', + upcomingTime: relevantBooking + ? `${formatTime(relevantBooking.startTime)} - ${formatTime(relevantBooking.endTime)}` : 'N/A', - room: 'TBD', - instructor: 'TBD', - payee: 'TBD' + room: room ? room.name : 'N/A', + instructor: instructor ? instructor.name : 'N/A', + payee: payee ? payee.name : 'N/A' }; }); @@ -84,6 +126,7 @@ export const ProgramsTable = () => { } }; + // Filter programs by search term with the includes method const filteredPrograms = programs.filter(program => program.name.toLowerCase().includes(searchTerm.toLowerCase()) ); From d35fe495318abfb3ff4878036a23b7423276fca7 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Thu, 30 Jan 2025 22:32:26 -0800 Subject: [PATCH 05/41] feat: make event delete endpoint delete related records and implement delete/filter functionality on programs table --- client/src/components/home/HomeComponents.jsx | 163 ++++++++++++++++-- .../components/home/ProgramFiltersModal.jsx | 148 ++++++++++++++++ server/routes/events.js | 23 ++- server/routes/rooms.js | 2 +- 4 files changed, 313 insertions(+), 23 deletions(-) create mode 100644 client/src/components/home/ProgramFiltersModal.jsx diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 1f084c2..138d51e 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -4,19 +4,28 @@ // also do fuzzy search on name or nah? -import React, { useState, useEffect } from 'react'; -import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Menu, MenuButton, MenuList, MenuItem, IconButton, Input, Button } from "@chakra-ui/react"; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Menu, MenuButton, MenuList, MenuItem, IconButton, Input, Button, AlertDialog, AlertDialogBody, AlertDialogFooter, AlertDialogHeader, AlertDialogContent, AlertDialogOverlay, useDisclosure, useToast } from "@chakra-ui/react"; import { useNavigate } from 'react-router-dom'; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { FiMoreVertical, FiFilter } from 'react-icons/fi'; +import { ProgramFiltersModal } from './ProgramFiltersModal'; export const ProgramsTable = () => { const [programs, setPrograms] = useState([]); + const [filteredPrograms, setFilteredPrograms] = useState([]); const [searchTerm, setSearchTerm] = useState(''); + const [programToDelete, setProgramToDelete] = useState(null); + const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false); + const [filters, setFilters] = useState({}); + const { backend } = useBackendContext(); + const { isOpen, onOpen, onClose } = useDisclosure(); const navigate = useNavigate(); + const toast = useToast(); + const cancelRef = useRef(); - const fetchPrograms = async () => { + const fetchPrograms = useCallback(async () => { try { const eventsResponse = await backend.get('/events'); const bookingsResponse = await backend.get('/bookings'); @@ -101,11 +110,78 @@ export const ProgramsTable = () => { } catch (error) { console.error('Failed to fetch programs:', error); } - }; + }, [backend]); useEffect(() => { fetchPrograms(); - }, []); + }, [fetchPrograms]); + + const applyFilters = useCallback(() => { + let result = programs; + + if (filters.dateRange && (filters.dateRange.start || filters.dateRange.end)) { + result = result.filter(program => { + if (program.upcomingDate === 'No bookings') return false; + const bookingDate = new Date(program.upcomingDate); + return (!filters.dateRange.start || bookingDate >= new Date(filters.dateRange.start)) && + (!filters.dateRange.end || bookingDate <= new Date(filters.dateRange.end)); + }); + } + + if (filters.timeRange && (filters.timeRange.start || filters.timeRange.end)) { + result = result.filter(program => { + if (program.upcomingTime === 'N/A') return false; + const [startTime] = program.upcomingTime.split(' - '); + return (!filters.timeRange.start || startTime >= filters.timeRange.start) && + (!filters.timeRange.end || startTime <= filters.timeRange.end); + }); + } + + if (filters.status && filters.status !== 'all') { + result = result.filter(program => program.status.toLowerCase() === filters.status.toLowerCase()); + } + + if (filters.room && filters.room !== 'all') { + console.log('Filtering by room:', filters.room); + result = result.filter(program => { + console.log(`Program ${program.id} room:`, program.room); + return program.room === filters.room; + }); + console.log('Filtered programs by room:', result); + } + + if (filters.instructor && filters.instructor !== 'all') { + console.log('Filtering by instructor:', filters.instructor); + result = result.filter(program => { + console.log(`Program ${program.id} instructor:`, program.instructor); + return program.instructor && program.instructor.toLowerCase() === filters.instructor.toLowerCase(); + }); + console.log('Filtered programs by instructor:', result); + } + + if (filters.payee && filters.payee !== 'all') { + console.log('Filtering by payee:', filters.payee); + result = result.filter(program => { + console.log(`Program ${program.id} payee:`, program.payee); + return program.payee && program.payee.toLowerCase() === filters.payee.toLowerCase(); + }); + console.log('Filtered programs by payee:', result); + } + + result = result.filter(program => + program.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + setFilteredPrograms(result); + }, [programs, filters, searchTerm]); + + useEffect(() => { + applyFilters(); + }, [applyFilters]); + + const handleApplyFilters = (newFilters) => { + setFilters(newFilters); + }; const handleRowClick = (id) => { navigate(`/programs/${id}`); @@ -116,25 +192,51 @@ export const ProgramsTable = () => { navigate(`/programs/${id}`); }; - const handleDelete = async (id, e) => { + const handleDeleteClick = (id, e) => { e.stopPropagation(); - try { - await backend.delete(`/events/${id}`); - setPrograms(programs.filter(program => program.id !== id)); - } catch (error) { - console.error('Failed to delete program:', error); + setProgramToDelete(id); + onOpen(); + }; + + const handleDelete = async () => { + if (programToDelete) { + try { + const response = await backend.delete(`/events/${programToDelete}`); + if (response.data.result === "success") { + setPrograms(programs.filter(program => program.id !== programToDelete)); + toast({ + title: "Program deleted", + description: "The program and all related records have been successfully deleted.", + status: "success", + duration: 5000, + isClosable: true, + }); + } else { + throw new Error("Failed to delete program"); + } + } catch (error) { + console.error('Failed to delete program:', error); + toast({ + title: "Delete failed", + description: error.response?.data?.message || "An error occurred while deleting the program.", + status: "error", + duration: 5000, + isClosable: true, + }); + } } + onClose(); }; // Filter programs by search term with the includes method - const filteredPrograms = programs.filter(program => - program.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + // const filteredPrograms = programs.filter(program => + // program.name.toLowerCase().includes(searchTerm.toLowerCase()) + // ); return ( <>
- + { /> handleEdit(program.id, e)}>Edit - handleDelete(program.id, e)}>Delete + handleDeleteClick(program.id, e)}>Delete @@ -184,6 +286,35 @@ export const ProgramsTable = () => { + + + + + Delete Program + + + Are you sure? You can't undo this action afterwards. + + + + + + + + + setIsFiltersModalOpen(false)} + onApplyFilters={handleApplyFilters} + /> ); }; diff --git a/client/src/components/home/ProgramFiltersModal.jsx b/client/src/components/home/ProgramFiltersModal.jsx new file mode 100644 index 0000000..3aac47d --- /dev/null +++ b/client/src/components/home/ProgramFiltersModal.jsx @@ -0,0 +1,148 @@ +// i needa fix indendation and other stuff on this its mainly chatgptd lol +// also TODO FIX THE TIME FILTERS FOR SOME REASON THEHY DONT WORK YETGA + +import React, { useState, useEffect } from 'react'; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + FormControl, + FormLabel, + Input, + Select, + VStack, +} from "@chakra-ui/react"; +import { useBackendContext } from "../../contexts/hooks/useBackendContext"; + +export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [startTime, setStartTime] = useState(''); + const [endTime, setEndTime] = useState(''); + const [status, setStatus] = useState('all'); + const [room, setRoom] = useState('all'); + const [instructor, setInstructor] = useState('all'); + const [payee, setPayee] = useState('all'); + + const [rooms, setRooms] = useState([]); + const [clients, setClients] = useState([]); + + const { backend } = useBackendContext(); + + useEffect(() => { + const fetchData = async () => { + try { + const roomsResponse = await backend.get('/rooms'); + setRooms(roomsResponse.data); + + const clientsResponse = await backend.get('/clients'); + setClients(clientsResponse.data); + } catch (error) { + console.error('Failed to fetch filter data:', error); + } + }; + + fetchData(); + }, [backend]); + + const handleApply = () => { + onApplyFilters({ + dateRange: { start: startDate, end: endDate }, + timeRange: { start: startTime, end: endTime }, + status, + room, + instructor, + payee, + }); + onClose(); + }; + + return ( + + + + Filter Programs + + + + + Date Range + setStartDate(e.target.value)} + placeholder="Start Date" + /> + setEndDate(e.target.value)} + placeholder="End Date" + /> + + + Time Range + setStartTime(e.target.value)} + /> + setEndTime(e.target.value)} + /> + + + Status + + + + Room + + + + Instructor + + + + + Payee + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/server/routes/events.js b/server/routes/events.js index b15f641..64b9aee 100644 --- a/server/routes/events.js +++ b/server/routes/events.js @@ -89,12 +89,23 @@ eventsRouter.put("/:id", async (req, res) => { eventsRouter.delete("/:id", async (req, res) => { try { const { id } = req.params; - const data = await db.query(`DELETE FROM events WHERE id = $1 RETURNING *`, [id]); - - if(data.length > 0) - res.status(200).json({"result" : "success", "deletedData" : keysToCamel(data)}); - else - res.status(404).json({"result" : "error"}); + + // Start a transaction + await db.tx(async t => { + // Delete related records first + await t.none('DELETE FROM bookings WHERE event_id = $1', [id]); + await t.none('DELETE FROM assignments WHERE event_id = $1', [id]); + await t.none('DELETE FROM invoices WHERE event_id = $1', [id]); + + // Then delete the event + const data = await t.any('DELETE FROM events WHERE id = $1 RETURNING *', [id]); + + if (data.length > 0) { + res.status(200).json({"result": "success", "deletedData": keysToCamel(data)}); + } else { + res.status(404).json({"result": "error", "message": "Event not found"}); + } + }); } catch (err) { res.status(500).send(err.message); } diff --git a/server/routes/rooms.js b/server/routes/rooms.js index 8aacb7b..ad28419 100644 --- a/server/routes/rooms.js +++ b/server/routes/rooms.js @@ -94,7 +94,7 @@ roomsRouter.delete("/:id", async (req, res) => { return res.status(404).json({ result: "error" }); } res.status(200).json({ result: "success" }); - } catch (err) { + } catch { res.status(500).json({ result: "error" }); } }); From bf4c591c71fead354b24cda89766bef8b8304589 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 31 Jan 2025 17:25:44 -0800 Subject: [PATCH 06/41] feat: import icons (probably shoulda just done .svg files but oh well --- client/src/components/home/Home.jsx | 8 +- client/src/components/home/HomeComponents.jsx | 338 ++++++++++++------ .../components/home/ProgramFiltersModal.jsx | 126 ++++--- client/src/components/home/StatusIcons.jsx | 17 + client/src/components/home/TableIcons.jsx | 259 ++++++++++++++ 5 files changed, 593 insertions(+), 155 deletions(-) create mode 100644 client/src/components/home/StatusIcons.jsx create mode 100644 client/src/components/home/TableIcons.jsx diff --git a/client/src/components/home/Home.jsx b/client/src/components/home/Home.jsx index c4e3294..6d4408b 100644 --- a/client/src/components/home/Home.jsx +++ b/client/src/components/home/Home.jsx @@ -1,6 +1,8 @@ -import React from 'react'; -import { Box } from '@chakra-ui/react'; -import { ProgramsTable } from './HomeComponents'; +import React from "react"; + +import { Box } from "@chakra-ui/react"; + +import { ProgramsTable } from "./HomeComponents"; export const Home = () => { return ( diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 138d51e..5f1fe1d 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -3,18 +3,53 @@ // also if there's multiple instructors this only does 1 unfortunately // also do fuzzy search on name or nah? +import React, { useCallback, useEffect, useRef, useState } from "react"; + +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, + IconButton, + Input, + Menu, + MenuButton, + MenuItem, + MenuList, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + useDisclosure, + useToast, +} from "@chakra-ui/react"; + +import { FiFilter, FiMoreVertical } from "react-icons/fi"; +import { useNavigate } from "react-router-dom"; -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Menu, MenuButton, MenuList, MenuItem, IconButton, Input, Button, AlertDialog, AlertDialogBody, AlertDialogFooter, AlertDialogHeader, AlertDialogContent, AlertDialogOverlay, useDisclosure, useToast } from "@chakra-ui/react"; -import { useNavigate } from 'react-router-dom'; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; -import { FiMoreVertical, FiFilter } from 'react-icons/fi'; -import { ProgramFiltersModal } from './ProgramFiltersModal'; +import { ProgramFiltersModal } from "./ProgramFiltersModal"; +import { StatusKey } from './StatusIcons'; +import { + FiltersIcon, + ThreeDotsIcon, + EditIcon, + CancelIcon, + SearchIcon, + PastStatusIcon, + ActiveStatusIcon +} from "./TableIcons"; export const ProgramsTable = () => { const [programs, setPrograms] = useState([]); const [filteredPrograms, setFilteredPrograms] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, setSearchTerm] = useState(""); const [programToDelete, setProgramToDelete] = useState(null); const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false); const [filters, setFilters] = useState({}); @@ -27,88 +62,113 @@ export const ProgramsTable = () => { const fetchPrograms = useCallback(async () => { try { - const eventsResponse = await backend.get('/events'); - const bookingsResponse = await backend.get('/bookings'); - const roomsResponse = await backend.get('/rooms'); - const assignmentsResponse = await backend.get('/assignments'); - const clientsResponse = await backend.get('/clients'); - + const eventsResponse = await backend.get("/events"); + const bookingsResponse = await backend.get("/bookings"); + const roomsResponse = await backend.get("/rooms"); + const assignmentsResponse = await backend.get("/assignments"); + const clientsResponse = await backend.get("/clients"); + const eventsData = eventsResponse.data; const bookingsData = bookingsResponse.data; const roomsData = roomsResponse.data; const assignmentsData = assignmentsResponse.data; const clientsData = clientsResponse.data; - + const currentDate = new Date(); - + const formatDate = (dateString) => { const date = new Date(dateString); - const options = { weekday: 'short', month: '2-digit', day: '2-digit', year: 'numeric' }; - return date.toLocaleDateString('en-US', options).replace(/,/g, '.'); + const options = { + weekday: "short", + month: "2-digit", + day: "2-digit", + year: "numeric", + }; + return date.toLocaleDateString("en-US", options).replace(/,/g, "."); }; - + const formatTime = (timeString) => { - const [hours, minutes] = timeString.split(':'); + const [hours, minutes] = timeString.split(":"); const date = new Date(2000, 0, 1, hours, minutes); - return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }).toLowerCase(); + return date + .toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }) + .toLowerCase(); }; - - const programsData = eventsData.map(event => { - const eventBookings = bookingsData.filter(booking => booking.eventId === event.id); - const upcomingBookings = eventBookings.filter(booking => new Date(booking.date) > currentDate); - const pastBookings = eventBookings.filter(booking => new Date(booking.date) <= currentDate); - + + const programsData = eventsData.map((event) => { + const eventBookings = bookingsData.filter( + (booking) => booking.eventId === event.id + ); + const upcomingBookings = eventBookings.filter( + (booking) => new Date(booking.date) > currentDate + ); + const pastBookings = eventBookings.filter( + (booking) => new Date(booking.date) <= currentDate + ); + let relevantBooking; let status; - + if (upcomingBookings.length > 0) { - relevantBooking = upcomingBookings.reduce((earliest, current) => - new Date(current.date) < new Date(earliest.date) ? current : earliest + relevantBooking = upcomingBookings.reduce((earliest, current) => + new Date(current.date) < new Date(earliest.date) + ? current + : earliest ); - status = 'Active'; + status = "Active"; } else if (pastBookings.length > 0) { - relevantBooking = pastBookings.reduce((latest, current) => + relevantBooking = pastBookings.reduce((latest, current) => new Date(current.date) > new Date(latest.date) ? current : latest ); - status = 'Past'; + status = "Past"; } else { - status = 'No bookings'; + status = "No bookings"; } - - const room = relevantBooking - ? roomsData.find(room => room.id === relevantBooking.roomId) + + const room = relevantBooking + ? roomsData.find((room) => room.id === relevantBooking.roomId) : null; - - const eventAssignments = assignmentsData.filter(assignment => assignment.eventId === event.id); - const instructorAssignment = eventAssignments.find(assignment => assignment.role === 'instructor'); - const payeeAssignment = eventAssignments.find(assignment => assignment.role === 'payee'); - + + const eventAssignments = assignmentsData.filter( + (assignment) => assignment.eventId === event.id + ); + const instructorAssignment = eventAssignments.find( + (assignment) => assignment.role === "instructor" + ); + const payeeAssignment = eventAssignments.find( + (assignment) => assignment.role === "payee" + ); + const instructor = instructorAssignment - ? clientsData.find(client => client.id === instructorAssignment.clientId) + ? clientsData.find( + (client) => client.id === instructorAssignment.clientId + ) : null; - + const payee = payeeAssignment - ? clientsData.find(client => client.id === payeeAssignment.clientId) + ? clientsData.find((client) => client.id === payeeAssignment.clientId) : null; - + return { id: event.id, name: event.name, description: event.description, status: status, - upcomingDate: relevantBooking ? formatDate(relevantBooking.date) : 'No bookings', - upcomingTime: relevantBooking - ? `${formatTime(relevantBooking.startTime)} - ${formatTime(relevantBooking.endTime)}` - : 'N/A', - room: room ? room.name : 'N/A', - instructor: instructor ? instructor.name : 'N/A', - payee: payee ? payee.name : 'N/A' + upcomingDate: relevantBooking + ? formatDate(relevantBooking.date) + : "No bookings", + upcomingTime: relevantBooking + ? `${formatTime(relevantBooking.startTime)} - ${formatTime(relevantBooking.endTime)}` + : "N/A", + room: room ? room.name : "N/A", + instructor: instructor ? instructor.name : "N/A", + payee: payee ? payee.name : "N/A", }; }); - + setPrograms(programsData); } catch (error) { - console.error('Failed to fetch programs:', error); + console.error("Failed to fetch programs:", error); } }, [backend]); @@ -118,60 +178,81 @@ export const ProgramsTable = () => { const applyFilters = useCallback(() => { let result = programs; - - if (filters.dateRange && (filters.dateRange.start || filters.dateRange.end)) { - result = result.filter(program => { - if (program.upcomingDate === 'No bookings') return false; + + if ( + filters.dateRange && + (filters.dateRange.start || filters.dateRange.end) + ) { + result = result.filter((program) => { + if (program.upcomingDate === "No bookings") return false; const bookingDate = new Date(program.upcomingDate); - return (!filters.dateRange.start || bookingDate >= new Date(filters.dateRange.start)) && - (!filters.dateRange.end || bookingDate <= new Date(filters.dateRange.end)); + return ( + (!filters.dateRange.start || + bookingDate >= new Date(filters.dateRange.start)) && + (!filters.dateRange.end || + bookingDate <= new Date(filters.dateRange.end)) + ); }); } - - if (filters.timeRange && (filters.timeRange.start || filters.timeRange.end)) { - result = result.filter(program => { - if (program.upcomingTime === 'N/A') return false; - const [startTime] = program.upcomingTime.split(' - '); - return (!filters.timeRange.start || startTime >= filters.timeRange.start) && - (!filters.timeRange.end || startTime <= filters.timeRange.end); + + if ( + filters.timeRange && + (filters.timeRange.start || filters.timeRange.end) + ) { + result = result.filter((program) => { + if (program.upcomingTime === "N/A") return false; + const [startTime] = program.upcomingTime.split(" - "); + return ( + (!filters.timeRange.start || startTime >= filters.timeRange.start) && + (!filters.timeRange.end || startTime <= filters.timeRange.end) + ); }); } - - if (filters.status && filters.status !== 'all') { - result = result.filter(program => program.status.toLowerCase() === filters.status.toLowerCase()); + + if (filters.status && filters.status !== "all") { + result = result.filter( + (program) => + program.status.toLowerCase() === filters.status.toLowerCase() + ); } - - if (filters.room && filters.room !== 'all') { - console.log('Filtering by room:', filters.room); - result = result.filter(program => { + + if (filters.room && filters.room !== "all") { + console.log("Filtering by room:", filters.room); + result = result.filter((program) => { console.log(`Program ${program.id} room:`, program.room); return program.room === filters.room; }); - console.log('Filtered programs by room:', result); + console.log("Filtered programs by room:", result); } - - if (filters.instructor && filters.instructor !== 'all') { - console.log('Filtering by instructor:', filters.instructor); - result = result.filter(program => { + + if (filters.instructor && filters.instructor !== "all") { + console.log("Filtering by instructor:", filters.instructor); + result = result.filter((program) => { console.log(`Program ${program.id} instructor:`, program.instructor); - return program.instructor && program.instructor.toLowerCase() === filters.instructor.toLowerCase(); + return ( + program.instructor && + program.instructor.toLowerCase() === filters.instructor.toLowerCase() + ); }); - console.log('Filtered programs by instructor:', result); + console.log("Filtered programs by instructor:", result); } - - if (filters.payee && filters.payee !== 'all') { - console.log('Filtering by payee:', filters.payee); - result = result.filter(program => { + + if (filters.payee && filters.payee !== "all") { + console.log("Filtering by payee:", filters.payee); + result = result.filter((program) => { console.log(`Program ${program.id} payee:`, program.payee); - return program.payee && program.payee.toLowerCase() === filters.payee.toLowerCase(); + return ( + program.payee && + program.payee.toLowerCase() === filters.payee.toLowerCase() + ); }); - console.log('Filtered programs by payee:', result); + console.log("Filtered programs by payee:", result); } - - result = result.filter(program => + + result = result.filter((program) => program.name.toLowerCase().includes(searchTerm.toLowerCase()) ); - + setFilteredPrograms(result); }, [programs, filters, searchTerm]); @@ -203,10 +284,13 @@ export const ProgramsTable = () => { try { const response = await backend.delete(`/events/${programToDelete}`); if (response.data.result === "success") { - setPrograms(programs.filter(program => program.id !== programToDelete)); + setPrograms( + programs.filter((program) => program.id !== programToDelete) + ); toast({ title: "Program deleted", - description: "The program and all related records have been successfully deleted.", + description: + "The program and all related records have been successfully deleted.", status: "success", duration: 5000, isClosable: true, @@ -215,10 +299,12 @@ export const ProgramsTable = () => { throw new Error("Failed to delete program"); } } catch (error) { - console.error('Failed to delete program:', error); + console.error("Failed to delete program:", error); toast({ title: "Delete failed", - description: error.response?.data?.message || "An error occurred while deleting the program.", + description: + error.response?.data?.message || + "An error occurred while deleting the program.", status: "error", duration: 5000, isClosable: true, @@ -228,6 +314,17 @@ export const ProgramsTable = () => { onClose(); }; + const renderStatusIcon = (status) => { + switch (status.toLowerCase()) { + case 'active': + return ; + case 'past': + return ; + default: + return null; + } + }; + // Filter programs by search term with the includes method // const filteredPrograms = programs.filter(program => // program.name.toLowerCase().includes(searchTerm.toLowerCase()) @@ -236,13 +333,16 @@ export const ProgramsTable = () => { return ( <>
- - } onClick={() => setIsFiltersModalOpen(true)}> + Filters + + setSearchTerm(e.target.value)} />
+ @@ -259,9 +359,15 @@ export const ProgramsTable = () => { {filteredPrograms.map((program) => ( - handleRowClick(program.id)} cursor="pointer"> + handleRowClick(program.id)} + cursor="pointer" + > - + @@ -271,13 +377,19 @@ export const ProgramsTable = () => { } - variant='outline' + aria-label="Options" + icon={} // <== using your custom 3-dots + variant="outline" /> - handleEdit(program.id, e)}>Edit - handleDeleteClick(program.id, e)}>Delete + handleEdit(program.id, e)}> + Edit + + handleDeleteClick(program.id, e)} + > + Delete + @@ -293,17 +405,27 @@ export const ProgramsTable = () => { > - + Delete Program Are you sure? You can't undo this action afterwards. - - @@ -322,5 +444,3 @@ export const ProgramsTable = () => { // What We Need From Tables // Assignment (client_id, role), Events (id, name, archived), Bookings (date, start_time, end_time, room_id), Rooms (name), Client (name) - - diff --git a/client/src/components/home/ProgramFiltersModal.jsx b/client/src/components/home/ProgramFiltersModal.jsx index 3aac47d..8e0c0b1 100644 --- a/client/src/components/home/ProgramFiltersModal.jsx +++ b/client/src/components/home/ProgramFiltersModal.jsx @@ -1,33 +1,35 @@ -// i needa fix indendation and other stuff on this its mainly chatgptd lol -// also TODO FIX THE TIME FILTERS FOR SOME REASON THEHY DONT WORK YETGA +// i needa fix indentation and other stuff on this its mainly chatgptd lol +// also TODO FIX THE TIME FILTERS FOR SOME REASON THEY DONT WORK YETGA + +import React, { useEffect, useState } from "react"; -import React, { useState, useEffect } from 'react'; import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, Button, FormControl, FormLabel, Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, Select, VStack, } from "@chakra-ui/react"; + import { useBackendContext } from "../../contexts/hooks/useBackendContext"; export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - const [startTime, setStartTime] = useState(''); - const [endTime, setEndTime] = useState(''); - const [status, setStatus] = useState('all'); - const [room, setRoom] = useState('all'); - const [instructor, setInstructor] = useState('all'); - const [payee, setPayee] = useState('all'); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [status, setStatus] = useState("all"); + const [room, setRoom] = useState("all"); + const [instructor, setInstructor] = useState("all"); + const [payee, setPayee] = useState("all"); const [rooms, setRooms] = useState([]); const [clients, setClients] = useState([]); @@ -37,13 +39,13 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { useEffect(() => { const fetchData = async () => { try { - const roomsResponse = await backend.get('/rooms'); + const roomsResponse = await backend.get("/rooms"); setRooms(roomsResponse.data); - const clientsResponse = await backend.get('/clients'); + const clientsResponse = await backend.get("/clients"); setClients(clientsResponse.data); } catch (error) { - console.error('Failed to fetch filter data:', error); + console.error("Failed to fetch filter data:", error); } }; @@ -63,7 +65,10 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { }; return ( - + Filter Programs @@ -100,7 +105,10 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { Status - setStatus(e.target.value)} + > @@ -108,41 +116,73 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { Room - setRoom(e.target.value)} + > {rooms.map((room) => ( - + ))} - Instructor - + Instructor + - - Payee - + Payee + - - + ); -}; \ No newline at end of file +}; diff --git a/client/src/components/home/StatusIcons.jsx b/client/src/components/home/StatusIcons.jsx new file mode 100644 index 0000000..7544eeb --- /dev/null +++ b/client/src/components/home/StatusIcons.jsx @@ -0,0 +1,17 @@ +// statusKey.jsx + +import React from "react"; +import { ActiveStatusIcon, PastStatusIcon } from "./TableIcons"; + +export const StatusKey = () => ( +
+
+ + Active +
+
+ + Past +
+
+); diff --git a/client/src/components/home/TableIcons.jsx b/client/src/components/home/TableIcons.jsx new file mode 100644 index 0000000..79c3ad7 --- /dev/null +++ b/client/src/components/home/TableIcons.jsx @@ -0,0 +1,259 @@ +import React from "react"; + +// Program Up Arrow +export const ProgramUpIcon = (props) => ( + + + +); + +// Program Down Arrow (rotated 180° from the above) +export const ProgramDownIcon = (props) => ( + + + + + +); + +// Filters +export const FiltersIcon = (props) => ( + + + +); + +// Upcoming Date +export const UpcomingDateIcon = (props) => ( + + + +); + +// Upcoming Time +export const UpcomingTimeIcon = (props) => ( + + + +); + +// Room +export const RoomIcon = (props) => ( + + + +); + +// Payee Icon (same shape as the "user" or "instructor" icon below) +export const PayeeIcon = (props) => ( + + + +); + +// Instructor Icon (same design as above) +export const InstructorIcon = (props) => ( + + + +); + +// Three Dots (circular menu trigger) +export const ThreeDotsIcon = (props) => ( + + + + +); + +// Edit +export const EditIcon = (props) => ( + + + +); + +// Cancel (red circle with slash) +export const CancelIcon = (props) => ( + + + +); + +// Active (Green) Circle +export const ActiveStatusIcon = () => ( + + + +); + +// Past (Yellow) Circle +export const PastStatusIcon = () => ( + + + +); + +// Search Icon +export const SearchIcon = (props) => ( + + + +); From d414101042c2720f6ceb0ff75c3e27a484cbf00d Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 31 Jan 2025 21:34:08 -0800 Subject: [PATCH 07/41] feat: add in icons --- client/src/assets/icons/actions.svg | 8 + client/src/assets/icons/active.svg | 5 + client/src/assets/icons/archive.svg | 6 + client/src/assets/icons/calendar.svg | 5 + client/src/assets/icons/cancel.svg | 3 + client/src/assets/icons/clock.svg | 5 + client/src/assets/icons/edit.svg | 5 + client/src/assets/icons/filter.svg | 5 + client/src/assets/icons/location.svg | 5 + client/src/assets/icons/past.svg | 5 + client/src/assets/icons/person.svg | 5 + client/src/assets/icons/program.svg | 5 + client/src/assets/icons/search.svg | 5 + client/src/components/home/HomeComponents.jsx | 311 +++++++++++++----- .../components/home/ProgramStatusLegend.jsx | 30 ++ client/src/components/home/StatusIcons.jsx | 17 - client/src/components/home/TableIcons.jsx | 259 --------------- 17 files changed, 334 insertions(+), 350 deletions(-) create mode 100644 client/src/assets/icons/actions.svg create mode 100644 client/src/assets/icons/active.svg create mode 100644 client/src/assets/icons/archive.svg create mode 100644 client/src/assets/icons/calendar.svg create mode 100644 client/src/assets/icons/cancel.svg create mode 100644 client/src/assets/icons/clock.svg create mode 100644 client/src/assets/icons/edit.svg create mode 100644 client/src/assets/icons/filter.svg create mode 100644 client/src/assets/icons/location.svg create mode 100644 client/src/assets/icons/past.svg create mode 100644 client/src/assets/icons/person.svg create mode 100644 client/src/assets/icons/program.svg create mode 100644 client/src/assets/icons/search.svg create mode 100644 client/src/components/home/ProgramStatusLegend.jsx delete mode 100644 client/src/components/home/StatusIcons.jsx delete mode 100644 client/src/components/home/TableIcons.jsx diff --git a/client/src/assets/icons/actions.svg b/client/src/assets/icons/actions.svg new file mode 100644 index 0000000..8c06b75 --- /dev/null +++ b/client/src/assets/icons/actions.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/client/src/assets/icons/active.svg b/client/src/assets/icons/active.svg new file mode 100644 index 0000000..e2eb6bb --- /dev/null +++ b/client/src/assets/icons/active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/archive.svg b/client/src/assets/icons/archive.svg new file mode 100644 index 0000000..cc7f5ec --- /dev/null +++ b/client/src/assets/icons/archive.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/src/assets/icons/calendar.svg b/client/src/assets/icons/calendar.svg new file mode 100644 index 0000000..fadbabd --- /dev/null +++ b/client/src/assets/icons/calendar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/cancel.svg b/client/src/assets/icons/cancel.svg new file mode 100644 index 0000000..e62508e --- /dev/null +++ b/client/src/assets/icons/cancel.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/clock.svg b/client/src/assets/icons/clock.svg new file mode 100644 index 0000000..6f1567f --- /dev/null +++ b/client/src/assets/icons/clock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/edit.svg b/client/src/assets/icons/edit.svg new file mode 100644 index 0000000..85f3d19 --- /dev/null +++ b/client/src/assets/icons/edit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/filter.svg b/client/src/assets/icons/filter.svg new file mode 100644 index 0000000..b8b28bf --- /dev/null +++ b/client/src/assets/icons/filter.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/location.svg b/client/src/assets/icons/location.svg new file mode 100644 index 0000000..50e3d24 --- /dev/null +++ b/client/src/assets/icons/location.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/past.svg b/client/src/assets/icons/past.svg new file mode 100644 index 0000000..e766b77 --- /dev/null +++ b/client/src/assets/icons/past.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/person.svg b/client/src/assets/icons/person.svg new file mode 100644 index 0000000..0579c41 --- /dev/null +++ b/client/src/assets/icons/person.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/program.svg b/client/src/assets/icons/program.svg new file mode 100644 index 0000000..b28aebc --- /dev/null +++ b/client/src/assets/icons/program.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/search.svg b/client/src/assets/icons/search.svg new file mode 100644 index 0000000..93bbbf6 --- /dev/null +++ b/client/src/assets/icons/search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 5f1fe1d..fb5418a 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -1,8 +1,4 @@ -// notes:: -// idk if we ned to remove a program if theres no upcoming bookings -// also if there's multiple instructors this only does 1 unfortunately -// also do fuzzy search on name or nah? - +// ProgramsTable.jsx import React, { useCallback, useEffect, useRef, useState } from "react"; import { @@ -15,6 +11,8 @@ import { Button, IconButton, Input, + InputGroup, + InputRightElement, Menu, MenuButton, MenuItem, @@ -30,22 +28,141 @@ import { useToast, } from "@chakra-ui/react"; -import { FiFilter, FiMoreVertical } from "react-icons/fi"; import { useNavigate } from "react-router-dom"; +// ============================= +// === 1. Icon Imports ==== +// ============================= +import actionsSvg from "../../assets/icons/actions.svg"; +import activeSvg from "../../assets/icons/active.svg"; +import calendarSvg from "../../assets/icons/calendar.svg"; +import cancelSvg from "../../assets/icons/cancel.svg"; +import clockSvg from "../../assets/icons/clock.svg"; +import editSvg from "../../assets/icons/edit.svg"; +import filterSvg from "../../assets/icons/filter.svg"; +import locationSvg from "../../assets/icons/location.svg"; +import pastSvg from "../../assets/icons/past.svg"; +import personSvg from "../../assets/icons/person.svg"; +import programSvg from "../../assets/icons/program.svg"; +import searchSvg from "../../assets/icons/search.svg"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { ProgramFiltersModal } from "./ProgramFiltersModal"; -import { StatusKey } from './StatusIcons'; -import { - FiltersIcon, - ThreeDotsIcon, - EditIcon, - CancelIcon, - SearchIcon, - PastStatusIcon, - ActiveStatusIcon -} from "./TableIcons"; - +import { ProgramStatusLegend } from "./ProgramStatusLegend"; + +// ============================= +// === 2. Tiny Icon Components +// ============================= +const ActiveStatusIcon = (props) => ( + Active Status +); + +const PastStatusIcon = (props) => ( + Past Status +); + +const FiltersIcon = (props) => ( + Filters +); + +const ActionsIcon = (props) => ( + Actions Menu +); + +const EditIcon = (props) => ( + Edit +); + +const CancelIcon = (props) => ( + Cancel +); + +const SearchIcon = (props) => ( + Search +); + +const ProgramIcon = (props) => ( + Program +); + +const CalendarIcon = (props) => ( + Calendar +); + +const ClockIcon = (props) => ( + Clock +); + +const LocationIcon = (props) => ( + Location +); + +const PersonIcon = (props) => ( + Person +); + +// ============================= +// === ProgramsTable ===== +// ============================= export const ProgramsTable = () => { const [programs, setPrograms] = useState([]); const [filteredPrograms, setFilteredPrograms] = useState([]); @@ -60,6 +177,9 @@ export const ProgramsTable = () => { const toast = useToast(); const cancelRef = useRef(); + // ============================= + // === Fetch Program Data === + // ============================= const fetchPrograms = useCallback(async () => { try { const eventsResponse = await backend.get("/events"); @@ -126,7 +246,7 @@ export const ProgramsTable = () => { } const room = relevantBooking - ? roomsData.find((room) => room.id === relevantBooking.roomId) + ? roomsData.find((r) => r.id === relevantBooking.roomId) : null; const eventAssignments = assignmentsData.filter( @@ -153,12 +273,14 @@ export const ProgramsTable = () => { id: event.id, name: event.name, description: event.description, - status: status, + status, upcomingDate: relevantBooking ? formatDate(relevantBooking.date) : "No bookings", upcomingTime: relevantBooking - ? `${formatTime(relevantBooking.startTime)} - ${formatTime(relevantBooking.endTime)}` + ? `${formatTime(relevantBooking.startTime)} - ${formatTime( + relevantBooking.endTime + )}` : "N/A", room: room ? room.name : "N/A", instructor: instructor ? instructor.name : "N/A", @@ -176,6 +298,9 @@ export const ProgramsTable = () => { fetchPrograms(); }, [fetchPrograms]); + // ============================= + // === Filtering Logic === + // ============================= const applyFilters = useCallback(() => { let result = programs; @@ -217,38 +342,26 @@ export const ProgramsTable = () => { } if (filters.room && filters.room !== "all") { - console.log("Filtering by room:", filters.room); - result = result.filter((program) => { - console.log(`Program ${program.id} room:`, program.room); - return program.room === filters.room; - }); - console.log("Filtered programs by room:", result); + result = result.filter((program) => program.room === filters.room); } if (filters.instructor && filters.instructor !== "all") { - console.log("Filtering by instructor:", filters.instructor); - result = result.filter((program) => { - console.log(`Program ${program.id} instructor:`, program.instructor); - return ( + result = result.filter( + (program) => program.instructor && program.instructor.toLowerCase() === filters.instructor.toLowerCase() - ); - }); - console.log("Filtered programs by instructor:", result); + ); } if (filters.payee && filters.payee !== "all") { - console.log("Filtering by payee:", filters.payee); - result = result.filter((program) => { - console.log(`Program ${program.id} payee:`, program.payee); - return ( + result = result.filter( + (program) => program.payee && program.payee.toLowerCase() === filters.payee.toLowerCase() - ); - }); - console.log("Filtered programs by payee:", result); + ); } + // Filter by search term result = result.filter((program) => program.name.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -260,6 +373,9 @@ export const ProgramsTable = () => { applyFilters(); }, [applyFilters]); + // ============================= + // === Event Handlers === + // ============================= const handleApplyFilters = (newFilters) => { setFilters(newFilters); }; @@ -284,9 +400,7 @@ export const ProgramsTable = () => { try { const response = await backend.delete(`/events/${programToDelete}`); if (response.data.result === "success") { - setPrograms( - programs.filter((program) => program.id !== programToDelete) - ); + setPrograms(programs.filter((p) => p.id !== programToDelete)); toast({ title: "Program deleted", description: @@ -314,46 +428,92 @@ export const ProgramsTable = () => { onClose(); }; + // Returns the proper status icon const renderStatusIcon = (status) => { - switch (status.toLowerCase()) { - case 'active': + switch (status?.toLowerCase()) { + case "active": return ; - case 'past': + case "past": return ; default: return null; } }; - // Filter programs by search term with the includes method - // const filteredPrograms = programs.filter(program => - // program.name.toLowerCase().includes(searchTerm.toLowerCase()) - // ); - + // ============================= + // === RENDERING === + // ============================= return ( <> -
- - setSearchTerm(e.target.value)} - /> + + {/* Input with Search Icon on the right */} + + setSearchTerm(e.target.value)} + /> + + + +
- + + {/* Status legend (Active/Past) */} + + + {/* The table */}
{program.name}{program.status} + {renderStatusIcon(program.status)} + {program.upcomingDate} {program.upcomingTime} {program.room}
- + {/* Program Name with icon to the RIGHT */} + - - - - - + {/* Upcoming Date with icon to the LEFT */} + + {/* Upcoming Time with icon to the LEFT */} + + {/* Room with icon to the LEFT */} + + {/* Instructor with icon to the LEFT */} + + {/* Payee with icon to the LEFT */} + + {/* Actions column */} @@ -365,29 +525,30 @@ export const ProgramsTable = () => { cursor="pointer" > - + + {/* Actions menu */}
Program Name + Program Name + + StatusUpcoming DateUpcoming TimeRoomInstructorPayee + + Upcoming Date + + + Upcoming Time + + + Room + + + Instructor + + + Payee +
{program.name} - {renderStatusIcon(program.status)} - {renderStatusIcon(program.status)} {program.upcomingDate} {program.upcomingTime} {program.room} {program.instructor} {program.payee} e.stopPropagation()}> } // <== using your custom 3-dots + icon={} // was ThreeDotsIcon variant="outline" /> handleEdit(program.id, e)}> + Edit handleDeleteClick(program.id, e)} > + Delete @@ -398,6 +559,8 @@ export const ProgramsTable = () => {
+ + {/* Delete Confirmation Dialog */} { > Delete Program + - Are you sure? You can't undo this action afterwards. + Are you sure? You can't undo this action afterwards. + - {/* Input with Search Icon on the right */} + { +
{/* Status legend (Active/Past) */} @@ -518,49 +431,53 @@ export const ProgramsTable = () => { - {filteredPrograms.map((program) => ( - handleRowClick(program.id)} - cursor="pointer" - > - {program.name} - {renderStatusIcon(program.status)} - {program.upcomingDate} - {program.upcomingTime} - {program.room} - {program.instructor} - {program.payee} - {/* Actions menu */} - e.stopPropagation()}> - - } // was ThreeDotsIcon - variant="outline" - /> - - handleEdit(program.id, e)}> - - Edit - - handleDeleteClick(program.id, e)} - > - - Delete - - - + {filteredPrograms.length === 0 ? ( + + + No programs available. - ))} + ) : ( + filteredPrograms.map((program) => ( + handleRowClick(program.id)} cursor="pointer"> + {program.name} + {renderStatusIcon(program.status)} + {program.upcomingDate} + {program.upcomingTime} + {program.room} + {program.instructor} + {program.payee} + e.stopPropagation()}> + + } // Uses your existing ActionsIcon + variant="outline" + /> + + handleEdit(program.id, e)}> + + Edit + + handleDeleteClick(program.id, e)} + color="red.500" + > + + Delete + + + + + + )) + )} + - {/* Delete Confirmation Dialog */} { isOpen={isFiltersModalOpen} onClose={() => setIsFiltersModalOpen(false)} onApplyFilters={handleApplyFilters} + filters={filters} + setFilters={setFilters} /> ); diff --git a/client/src/components/home/ProgramFiltersModal.jsx b/client/src/components/home/ProgramFiltersModal.jsx index 8e0c0b1..e57238d 100644 --- a/client/src/components/home/ProgramFiltersModal.jsx +++ b/client/src/components/home/ProgramFiltersModal.jsx @@ -1,8 +1,4 @@ -// i needa fix indentation and other stuff on this its mainly chatgptd lol -// also TODO FIX THE TIME FILTERS FOR SOME REASON THEY DONT WORK YETGA - import React, { useEffect, useState } from "react"; - import { Button, FormControl, @@ -17,8 +13,12 @@ import { ModalOverlay, Select, VStack, + HStack, + Box, } from "@chakra-ui/react"; +import calendarSvg from "../../assets/icons/calendar.svg"; + import { useBackendContext } from "../../contexts/hooks/useBackendContext"; export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { @@ -65,91 +65,100 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { }; return ( - + Filter Programs - + + {/* Date Filters */} - Date Range - setStartDate(e.target.value)} - placeholder="Start Date" - /> - setEndDate(e.target.value)} - placeholder="End Date" - /> + + + Date + + + setStartDate(e.target.value)} + placeholder="Start Date" + /> + setEndDate(e.target.value)} + placeholder="End Date" + /> + + + {/* Time Filters */} - Time Range - setStartTime(e.target.value)} - /> - setEndTime(e.target.value)} - /> + Time + + setStartTime(e.target.value)} + /> + setEndTime(e.target.value)} + /> + + + {/* Status Buttons */} Status - + + {["all", "active", "past"].map((s) => ( + + ))} + + + {/* Room Selection as Pills/Chips */} Room - + + + {/* Instructor Dropdown */} - Instructor - setInstructor(e.target.value)}> {clients.map((client) => ( - ))} + + {/* Payee Dropdown */} - Payee + Payee { {/* Time Filters */} - Time + + + Time + { {/* Status Buttons */} Status - - {["all", "active", "past"].map((s) => ( - - ))} + + + + @@ -131,10 +140,10 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { Room - {["all", "Community", "Lounge", "Theater"].map((r) => ( + {["All", "Community", "Lounge", "Theater"].map((r) => ( - From 36c6a57497fcc8c77ea8688819109f36921e595a Mon Sep 17 00:00:00 2001 From: margoglvz Date: Sun, 2 Feb 2025 04:30:30 -0800 Subject: [PATCH 13/41] program icon updated and implemented --- client/src/components/home/HomeComponents.jsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 94cef6b..3bbe95d 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -8,6 +8,8 @@ import { AlertDialogHeader, AlertDialogOverlay, Button, + Box, + HStack, IconButton, Input, InputGroup, @@ -20,11 +22,13 @@ import { TableContainer, Tbody, Td, + Text, Th, Thead, Tr, useDisclosure, useToast, + VStack } from "@chakra-ui/react"; import { useNavigate } from "react-router-dom"; @@ -109,15 +113,6 @@ const SearchIcon = (props) => ( /> ); -const ProgramIcon = (props) => ( - Program -); - const CalendarIcon = (props) => ( { {/* Program Name with icon to the RIGHT */} - - Program Name - + + + Program + + + + + Status {/* Upcoming Date with icon to the LEFT */} From a513c766f3d5ceea1b2fa4b778e7fc952dbbdb80 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sun, 2 Feb 2025 07:09:05 -0800 Subject: [PATCH 14/41] fix: fix functionality for program filters modal --- .../components/home/ProgramFiltersModal.jsx | 162 ++++++++++++++---- 1 file changed, 127 insertions(+), 35 deletions(-) diff --git a/client/src/components/home/ProgramFiltersModal.jsx b/client/src/components/home/ProgramFiltersModal.jsx index 4eee55c..d3b2e69 100644 --- a/client/src/components/home/ProgramFiltersModal.jsx +++ b/client/src/components/home/ProgramFiltersModal.jsx @@ -1,8 +1,13 @@ +// ProgramFiltersModal.jsx + import React, { useEffect, useState } from "react"; + import { + Box, Button, FormControl, FormLabel, + HStack, Input, Modal, ModalBody, @@ -14,18 +19,17 @@ import { Select, Text, VStack, - HStack, - Box, } from "@chakra-ui/react"; +import activeSvg from "../../assets/icons/active.svg"; +// Icons for styling import calendarSvg from "../../assets/icons/calendar.svg"; import clockSvg from "../../assets/icons/clock.svg"; -import activeSvg from "../../assets/icons/active.svg"; import pastSvg from "../../assets/icons/past.svg"; - import { useBackendContext } from "../../contexts/hooks/useBackendContext"; export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { + // State for filters const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [startTime, setStartTime] = useState(""); @@ -35,11 +39,13 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { const [instructor, setInstructor] = useState("all"); const [payee, setPayee] = useState("all"); + // State for data fetching const [rooms, setRooms] = useState([]); const [clients, setClients] = useState([]); const { backend } = useBackendContext(); + // Fetch rooms & clients on mount useEffect(() => { const fetchData = async () => { try { @@ -56,6 +62,7 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { fetchData(); }, [backend]); + // Called when user clicks "Apply" const handleApply = () => { onApplyFilters({ dateRange: { start: startDate, end: endDate }, @@ -69,17 +76,33 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { }; return ( - + Filter Programs - - {/* Date Filters */} + + {/* === DATE FILTERS === */} - - + + Date @@ -98,10 +121,19 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { - {/* Time Filters */} + {/* === TIME FILTERS === */} - - + + Time @@ -118,56 +150,109 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { - {/* Status Buttons */} + {/* === STATUS FILTER (BUTTON GROUP) === */} Status - - - - - {/* Room Selection as Pills/Chips */} + {/* === ROOM FILTER (PILL BUTTONS) === */} Room - - {["All", "Community", "Lounge", "Theater"].map((r) => ( + + {/* "All" pill */} + + + {/* Each room from the backend */} + {rooms.map((r) => ( ))} - {/* Instructor Dropdown */} + {/* === INSTRUCTOR DROPDOWN === */} Instructor(s) - setInstructor(e.target.value)} + > {clients.map((client) => ( - ))} - {/* Payee Dropdown */} + {/* === PAYEE DROPDOWN === */} - Payee + Payee setInstructor(e.target.value)} @@ -252,7 +253,10 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { {/* === PAYEE DROPDOWN === */} - Payee + + + Payee + setSearchTerm(e.target.value)} - /> - - - - - - - - {/* Status legend (Active/Past) */} - - {/* The table */} - - - - - {/* Program Name with icon to the RIGHT */} - - - {/* Upcoming Date with icon to the LEFT */} - - {/* Upcoming Time with icon to the LEFT */} - - {/* Room with icon to the LEFT */} - - {/* Instructor with icon to the LEFT */} - - {/* Payee with icon to the LEFT */} - - {/* Actions column */} - - - - - {filteredPrograms.length === 0 ? ( + + + + + setSearchTerm(e.target.value)} + /> + + + + + + +
- - Program - - - - - - Status - - - Upcoming Date - - - - - - - - - Upcoming Time - - - - - Room - - - - - Instructor - - - - - Payee - -
+ - + {/* Program Name with icon to the RIGHT */} + + + {/* Upcoming Date with icon to the LEFT */} + + {/* Upcoming Time with icon to the LEFT */} + + {/* Room with icon to the LEFT */} + + {/* Instructor with icon to the LEFT */} + + {/* Payee with icon to the LEFT */} + + {/* Actions column */} + - ) : ( - filteredPrograms.map((program) => ( - handleRowClick(program.id)} cursor="pointer"> - - - - - - - - + + {filteredPrograms.length === 0 ? ( + + - )) - )} - - -
- No programs available. - + + Program + + + + + + + + + Upcoming Date + + + + + + + + + Upcoming Time + + + + + Room + + + + + Instructor + + + + + Payee + +
{program.name}{renderStatusIcon(program.status)}{program.upcomingDate}{program.upcomingTime}{program.room}{program.instructor}{program.payee} e.stopPropagation()}> - - } // Uses your existing ActionsIcon - variant="outline" - /> - - handleEdit(program.id, e)}> - - Edit - - handleDeleteClick(program.id, e)} - color="red.500" - > - - Delete - - - +
+ No programs available.
-
+ ) : ( + filteredPrograms.map((program) => ( + handleRowClick(program.id)} + cursor="pointer" + sx={{ _odd: { bg: "gray.100" }}} + > + {program.name} + {renderStatusIcon(program.status)} + {program.upcomingDate} + {program.upcomingTime} + {program.room} + {program.instructor} + {program.payee} + e.stopPropagation()}> + + } // Uses your existing ActionsIcon + /> + + handleEdit(program.id, e)}> + + Edit + + handleDeleteClick(program.id, e)} + color="red.500" + > + + Delete + + + + + + )) + )} + + + + + Date: Sun, 2 Feb 2025 22:49:17 -0800 Subject: [PATCH 21/41] new status icon for status hover --- client/src/components/home/StatusIcon.jsx | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 client/src/components/home/StatusIcon.jsx diff --git a/client/src/components/home/StatusIcon.jsx b/client/src/components/home/StatusIcon.jsx new file mode 100644 index 0000000..103c149 --- /dev/null +++ b/client/src/components/home/StatusIcon.jsx @@ -0,0 +1,32 @@ +import React from "react"; + +import activeSvg from "../../assets/icons/active.svg"; +import pastSvg from "../../assets/icons/past.svg"; + +import { Box, HStack, Popover, PopoverTrigger, PopoverContent, PopoverBody, Text } from "@chakra-ui/react"; + +const StatusTooltip = () => { + return ( + + + + Status + + + + + + + Active + + + + Past + + + + + ); +}; + +export default StatusTooltip; From 091865190b9844e4f8a283498258fbc01898700f Mon Sep 17 00:00:00 2001 From: margoglvz Date: Sun, 2 Feb 2025 22:56:11 -0800 Subject: [PATCH 22/41] removed background of edit/delete button --- client/src/components/home/HomeComponents.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 8525a7a..6fb5f75 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -49,7 +49,6 @@ import searchSvg from "../../assets/icons/search.svg"; import StatusTooltip from "./StatusIcon"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { ProgramFiltersModal } from "./ProgramFiltersModal"; -import { ProgramStatusLegend } from "./ProgramStatusLegend"; const ActiveStatusIcon = (props) => ( @@ -461,7 +460,10 @@ export const ProgramsTable = () => { } // Uses your existing ActionsIcon + icon={} + variant="ghost" + bg="transparent" + _hover={{ bg: "purple.100" }} /> handleEdit(program.id, e)}> From 73907f1cbf4f5767c0c6abbb8242f4af09b4a5a0 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 3 Feb 2025 08:49:51 -0800 Subject: [PATCH 23/41] feat: add header row component for home page + navbar --- client/index.html | 4 + client/src/assets/icons/google-calendar.svg | 12 + client/src/assets/icons/plus.svg | 5 + client/src/components/AddClassModal.jsx | 338 ++++++++++-------- .../components/home/HeaderRowComponent.jsx | 148 ++++++++ client/src/components/home/Home.jsx | 27 +- 6 files changed, 379 insertions(+), 155 deletions(-) create mode 100644 client/src/assets/icons/google-calendar.svg create mode 100644 client/src/assets/icons/plus.svg create mode 100644 client/src/components/home/HeaderRowComponent.jsx diff --git a/client/index.html b/client/index.html index 96ebf54..94b6299 100644 --- a/client/index.html +++ b/client/index.html @@ -1,6 +1,10 @@ + + + + + + + + + + + + diff --git a/client/src/assets/icons/plus.svg b/client/src/assets/icons/plus.svg new file mode 100644 index 0000000..ea91dfc --- /dev/null +++ b/client/src/assets/icons/plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/components/AddClassModal.jsx b/client/src/components/AddClassModal.jsx index ffa0ef7..0e85e24 100644 --- a/client/src/components/AddClassModal.jsx +++ b/client/src/components/AddClassModal.jsx @@ -1,28 +1,26 @@ +import React, { useEffect, useState } from "react"; + import { - useDisclosure, - Button, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalCloseButton, - FormLabel, - Input, - FormControl, - ModalBody, - Select, - Flex, - IconButton, - Box + Box, + Button, + Flex, + FormControl, + FormLabel, + IconButton, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Select, } from "@chakra-ui/react"; -import React, { useState, useEffect } from 'react'; import { useBackendContext } from "../contexts/hooks/useBackendContext"; - -export const AddClassModal = () => { - const { isOpen, onOpen, onClose } = useDisclosure() +export const AddClassModal = ({ isOpen, onClose }) => { const { backend } = useBackendContext(); const [rooms, setRooms] = useState([]); @@ -30,26 +28,25 @@ export const AddClassModal = () => { const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); - //bookings part - //events part - + // Bookings part const [bookingsForm, setBookingsForm] = useState({ - event_id : "", - room_id : "", - start_time : "", - end_time : "", - date : "", - archived : false - }) - + event_id: "", + room_id: "", + start_time: "", + end_time: "", + date: "", + archived: false, + }); + + // Events part const [eventsForm, setEventsForm] = useState({ - name : "", - description : "", - archived: false - }) - + name: "", + description: "", + archived: false, + }); + const getDatesForDays = (startDate, endDate, selectedDays) => { - const daysMap = { 'Su': 0, 'M': 1, 'Tu': 2, 'W': 3, 'Th': 4, 'F': 5, 'S': 6 }; + const daysMap = { Su: 0, M: 1, Tu: 2, W: 3, Th: 4, F: 5, S: 6 }; const daysIndices = selectedDays.map((day) => daysMap[day]); const dates = []; @@ -67,133 +64,168 @@ export const AddClassModal = () => { const handleSubmit = async () => { try { + // 1) Create the event + const eventResponse = await backend.post("/events", eventsForm); + const eventId = eventResponse.data.id; + + // 2) Generate all relevant dates + const dates = getDatesForDays(startDate, endDate, selectedDays); + + // 3) Post each booking + for (const date of dates) { + const bookingData = { + ...bookingsForm, + event_id: eventId, + date: date, + }; + const bookingsResponse = await backend.post("/bookings", bookingData); + console.log("Created booking:", bookingsResponse.data); + } + } catch (error) { + console.error("Error submitting form:", error); + } + }; - const eventResponse = await backend.post("/events", eventsForm); - - const eventId = eventResponse.data.id; - - const dates = getDatesForDays(startDate, endDate, selectedDays); + useEffect(() => { + const fetchData = async () => { + try { + const response = await backend.get("/rooms"); + setRooms(response.data); + } catch (error) { + console.error("Error fetching rooms:", error); + } + }; - for (const date of dates) { - const bookingData = { - ...bookingsForm, - event_id : eventId, - date: date, - }; - - const bookingsResponse = await backend.post("/bookings", bookingData); - console.log(bookingsResponse.data); - } + fetchData(); + }, [backend]); - - } catch (error) { - console.error("Error submitting form:", error); - } - } - - useEffect(() => { - const fetchData = async () => { - try { - const response = await backend.get("/rooms"); - setRooms(response.data); - } catch (error) { - console.error("Error fetching rooms:", error); - } - }; - - fetchData(); - }, [backend]); - return ( - <> - - - - - Add Class - - - - - Name - (setEventsForm({...eventsForm, "name" : e.target.value}))}/> - - - - - Description - (setEventsForm({...eventsForm, "description" : e.target.value}))}/> - - - - Room - - - - - - Days - - {["Su", "M", "Tu", "W", "Th", "F", "S"].map((day) => ( - {day}} - value={day} - onClick={() => { - if (selectedDays.includes(day)) { - setSelectedDays(selectedDays.filter((d) => d !== day)); - } else { - setSelectedDays([...selectedDays, day]); - } - }} - /> - ))} - - + + + + Add Class + + + + Name + + setEventsForm({ ...eventsForm, name: e.target.value }) + } + /> + + + Description + + setEventsForm({ ...eventsForm, description: e.target.value }) + } + /> + + + + Room + + + + + Days + + {["Su", "M", "Tu", "W", "Th", "F", "S"].map((day) => ( + {day}} + value={day} + onClick={() => { + if (selectedDays.includes(day)) { + setSelectedDays((prev) => prev.filter((d) => d !== day)); + } else { + setSelectedDays((prev) => [...prev, day]); + } + }} + /> + ))} + + Start Date - setStartDate(e.target.value)}/> + setStartDate(e.target.value)} + /> End Date - setEndDate(e.target.value)}/> + setEndDate(e.target.value)} + /> Start Time - setBookingsForm({...bookingsForm, start_time : e.target.value})}/> - End Date - setBookingsForm({...bookingsForm, end_time : e.target.value})}/> + + setBookingsForm({ ...bookingsForm, start_time: e.target.value }) + } + /> + End Time + + setBookingsForm({ ...bookingsForm, end_time: e.target.value }) + } + /> + - - - - - - - - - - ) -} \ No newline at end of file + }} + > + Submit + + +
+
+ ); +}; diff --git a/client/src/components/home/HeaderRowComponent.jsx b/client/src/components/home/HeaderRowComponent.jsx new file mode 100644 index 0000000..5f8ac77 --- /dev/null +++ b/client/src/components/home/HeaderRowComponent.jsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; + +import archiveSvg from "../../assets/icons/archive.svg"; +import googleCalendarSvg from "../../assets/icons/google-calendar.svg"; +import plusSvg from "../../assets/icons/plus.svg"; +import { AddClassModal } from "../AddClassModal"; + +export const HeaderRowComponent = () => { + // State to control AddClassModal visibility + const [isAddClassOpen, setIsAddClassOpen] = useState(false); + + return ( +
+ {/* Google Calendar on the left */} +
{ + // handle google calendar logic uhhhhh + }} + > + Google Calendar + + Google Calendar + +
+ + {/* Archived + New Program on right*/} +
+ {/* Archived button */} +
{ + // handle archive logic uhhhhhhh + }} + > + Archived + + Archived + +
+ + {/* New Program Button */} +
{ + // Open the AddClassModal + setIsAddClassOpen(true); + }} + > + New Program + + New Program + +
+
+ + setIsAddClassOpen(false)} + /> +
+ ); +}; diff --git a/client/src/components/home/Home.jsx b/client/src/components/home/Home.jsx index 6d4408b..0870b8f 100644 --- a/client/src/components/home/Home.jsx +++ b/client/src/components/home/Home.jsx @@ -2,12 +2,35 @@ import React from "react"; import { Box } from "@chakra-ui/react"; +import { Navbar } from "../Navbar"; +import { HeaderRowComponent } from "./HeaderRowComponent"; import { ProgramsTable } from "./HomeComponents"; export const Home = () => { return ( - - + + + + + + + ); }; From dbabcd06b193b2911d9aa6cc7d6e5e51a785f0c8 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 3 Feb 2025 10:32:59 -0800 Subject: [PATCH 24/41] style: move home page styling to css file --- .../components/home/HeaderRowComponent.jsx | 116 ++-------------- client/src/components/home/Home.css | 128 ++++++++++++++++++ client/src/components/home/Home.jsx | 23 +--- 3 files changed, 147 insertions(+), 120 deletions(-) create mode 100644 client/src/components/home/Home.css diff --git a/client/src/components/home/HeaderRowComponent.jsx b/client/src/components/home/HeaderRowComponent.jsx index 5f8ac77..7d9e330 100644 --- a/client/src/components/home/HeaderRowComponent.jsx +++ b/client/src/components/home/HeaderRowComponent.jsx @@ -5,137 +5,51 @@ import googleCalendarSvg from "../../assets/icons/google-calendar.svg"; import plusSvg from "../../assets/icons/plus.svg"; import { AddClassModal } from "../AddClassModal"; +// Remove inline styles; use classNames from home.css export const HeaderRowComponent = () => { - // State to control AddClassModal visibility const [isAddClassOpen, setIsAddClassOpen] = useState(false); return ( -
- {/* Google Calendar on the left */} +
{ - // handle google calendar logic uhhhhh + // handle google calendar logic }} > Google Calendar - - Google Calendar - + Google Calendar
- {/* Archived + New Program on right*/} -
- {/* Archived button */} +
{ - // handle archive logic uhhhhhhh + // handle archive logic }} > Archived - - Archived - + Archived
- {/* New Program Button */}
{ - // Open the AddClassModal - setIsAddClassOpen(true); - }} + className="new-program" + onClick={() => setIsAddClassOpen(true)} > New Program - - New Program - + New Program
diff --git a/client/src/components/home/Home.css b/client/src/components/home/Home.css new file mode 100644 index 0000000..eafdffe --- /dev/null +++ b/client/src/components/home/Home.css @@ -0,0 +1,128 @@ +/* home.css */ + +/* 1) Color Variables */ +:root { + --light-grey: #F6F6F6; + --medium-grey: #767778; + --medium-light-grey: #D2D2D2; + --white: #FFF; + --indigo: #4E4AE7; + --font-inter: "Inter", sans-serif; +} + +/* 2) Outer Home Container */ +.home { + display: flex; + padding: 32px 32px 0px 32px; + align-items: flex-start; + gap: 32px; + align-self: stretch; +} + +/* 3) Inner Container for header + table */ +.home-inner { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + align-self: stretch; +} + +/* 4) Header Row */ +.header-row { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; + gap: 24px; + align-self: stretch; +} + +/* === Google Calendar (left) === */ +.google-calendar { + display: flex; + height: 54.795px; + padding: 4px 16px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 15px; + border: 1px solid var(--medium-grey); + background: var(--light-grey); + cursor: pointer; +} + +.google-calendar-icon { + width: 20px; + height: 20px; +} + +.google-calendar-text { + color: var(--medium-grey); + font-family: var(--font-inter); + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +/* === Archive + New Program on the right === */ +.header-right { + display: flex; + justify-content: center; + align-items: center; + gap: 24px; +} + +/* === Archive button (no border / radius) === */ +.archive { + display: flex; + height: 54.795px; + padding: 4px 16px; + justify-content: center; + align-items: center; + gap: 4px; + border: none; + border-radius: 0; + cursor: pointer; +} + +.archive-icon { + width: 20px; + height: 20px; +} + +.archive-text { + color: var(--medium-grey); + font-family: var(--font-inter); + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +/* === New Program button === */ +.new-program { + display: flex; + padding: 12px 16px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 30px; + background: var(--indigo); + cursor: pointer; +} + +.new-program-icon { + width: 20px; + height: 20px; +} + +.new-program-text { + color: var(--white); + font-family: var(--font-inter); + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: normal; +} diff --git a/client/src/components/home/Home.jsx b/client/src/components/home/Home.jsx index 0870b8f..b458d6c 100644 --- a/client/src/components/home/Home.jsx +++ b/client/src/components/home/Home.jsx @@ -2,32 +2,17 @@ import React from "react"; import { Box } from "@chakra-ui/react"; +import "./home.css"; + import { Navbar } from "../Navbar"; import { HeaderRowComponent } from "./HeaderRowComponent"; import { ProgramsTable } from "./HomeComponents"; export const Home = () => { return ( - + - - + From af9c04472b44d77455c6bc86f18f3579ddee26dc Mon Sep 17 00:00:00 2001 From: margoglvz Date: Mon, 3 Feb 2025 12:13:38 -0800 Subject: [PATCH 25/41] fixed gap --- client/src/components/home/HomeComponents.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 1756457..43999ea 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -356,7 +356,7 @@ export const ProgramsTable = () => { <> {/* The table */} - + From 96cf92ca220279f4bc47217acd8187e4e32d981d Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 3 Feb 2025 14:46:13 -0800 Subject: [PATCH 29/41] style: match up home page to figma mockup --- .../components/home/HeaderRowComponent.jsx | 1 - client/src/components/home/Home.css | 185 ++++++- client/src/components/home/HomeComponents.jsx | 505 +++++++++--------- .../components/home/ProgramStatusLegend.jsx | 30 -- client/src/components/home/StatusIcon.jsx | 59 +- 5 files changed, 477 insertions(+), 303 deletions(-) delete mode 100644 client/src/components/home/ProgramStatusLegend.jsx diff --git a/client/src/components/home/HeaderRowComponent.jsx b/client/src/components/home/HeaderRowComponent.jsx index 7d9e330..4eda481 100644 --- a/client/src/components/home/HeaderRowComponent.jsx +++ b/client/src/components/home/HeaderRowComponent.jsx @@ -5,7 +5,6 @@ import googleCalendarSvg from "../../assets/icons/google-calendar.svg"; import plusSvg from "../../assets/icons/plus.svg"; import { AddClassModal } from "../AddClassModal"; -// Remove inline styles; use classNames from home.css export const HeaderRowComponent = () => { const [isAddClassOpen, setIsAddClassOpen] = useState(false); diff --git a/client/src/components/home/Home.css b/client/src/components/home/Home.css index eafdffe..5ab1fbd 100644 --- a/client/src/components/home/Home.css +++ b/client/src/components/home/Home.css @@ -1,16 +1,13 @@ -/* home.css */ - -/* 1) Color Variables */ :root { --light-grey: #F6F6F6; --medium-grey: #767778; --medium-light-grey: #D2D2D2; --white: #FFF; --indigo: #4E4AE7; + --deep-red: #90080F; --font-inter: "Inter", sans-serif; } -/* 2) Outer Home Container */ .home { display: flex; padding: 32px 32px 0px 32px; @@ -19,7 +16,6 @@ align-self: stretch; } -/* 3) Inner Container for header + table */ .home-inner { display: flex; flex-direction: column; @@ -28,7 +24,6 @@ align-self: stretch; } -/* 4) Header Row */ .header-row { display: flex; width: 100%; @@ -38,7 +33,6 @@ align-self: stretch; } -/* === Google Calendar (left) === */ .google-calendar { display: flex; height: 54.795px; @@ -61,12 +55,9 @@ color: var(--medium-grey); font-family: var(--font-inter); font-size: 16px; - font-style: normal; font-weight: 600; - line-height: normal; } -/* === Archive + New Program on the right === */ .header-right { display: flex; justify-content: center; @@ -74,7 +65,6 @@ gap: 24px; } -/* === Archive button (no border / radius) === */ .archive { display: flex; height: 54.795px; @@ -96,12 +86,9 @@ color: var(--medium-grey); font-family: var(--font-inter); font-size: 16px; - font-style: normal; font-weight: 600; - line-height: normal; } -/* === New Program button === */ .new-program { display: flex; padding: 12px 16px; @@ -122,7 +109,173 @@ color: var(--white); font-family: var(--font-inter); font-size: 16px; - font-style: normal; font-weight: 600; - line-height: normal; +} + +.programs-table { + width: 100%; + margin: 40px auto 0 auto; + border: 1px solid var(--medium-light-grey); + border-radius: 20px; + padding: 16px; +} + +.programs-table__filter-row { + width: 95%; + margin: 40px auto 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.programs-table__container { + max-width: 100%; + margin: 0 auto; + padding: 16px; +} + +.programs-table__table { + border-spacing: 0 10px !important; + border-collapse: separate !important; + margin-top: 20px; +} + +.programs-table__row--odd { + background-color: var(--light-grey); +} + +.programs-table__row--even { + background-color: var(--white); +} + +.programs-table__row--no-booking { + background-color: #e0e0e0 !important; +} + +.filter-box { + border-radius: 15px; + border: 1px solid var(--medium-light-grey); + background: var(--white); + display: flex; + height: 54.795px; + padding: 8px 16px; + justify-content: center; + align-items: center; + gap: 4px; + cursor: pointer; +} + +.filter-box-text { + color: var(--medium-grey); + font-family: var(--font-inter); + font-size: 16px; + font-weight: 600; +} + +.table-header-text { + color: var(--medium-grey); + font-family: var(--font-inter); + font-size: 16px; + font-weight: 600; + line-height: 20px; + text-transform: none; +} + +.actions-container { + display: flex; + width: 48px; + height: 48px; + border-radius: 50%; + background-color: transparent; + justify-content: center; + align-items: center; + gap: 10px; +} + +.actions-container:hover, +.actions-container:focus-within { + background: transparent !important; +} + +.menu-list-custom { + border-radius: 15px; + border: 1px solid var(--medium-light-grey, #D2D2D2); + background: var(--light-grey, #F6F6F6); + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); + padding: 8px; +} + +.menu-list-custom { + border-radius: 15px !important; + border: 1px solid var(--medium-light-grey, #D2D2D2) !important; + background: var(--light-grey, #F6F6F6) !important; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25) !important; + padding: 8px !important; +} + +.menu-list-custom .chakra-menu__menuitem { + background: var(--light-grey, #F6F6F6) !important; + display: flex !important; + padding: 8px 12px !important; + align-items: center !important; + gap: 8px !important; + border-radius: 10px !important; + font-family: var(--font-inter) !important; + font-size: 16px !important; + font-weight: 600 !important; + cursor: pointer !important; +} + +.menu-list-custom .chakra-menu__menuitem:hover { + background-color: rgba(0, 0, 0, 0.05) !important; +} + +.menu-item--edit { + color: var(--medium-grey, #767778) !important; +} + +.menu-item--delete { + color: var(--deep-red, #90080F) !important; +} + +.search-wrapper { + display: flex; + align-items: center; + gap: 0; + height: 54.795px; +} + +.searchbar-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + flex: 1 0 0; + align-self: stretch; + padding: 0px 16px; + border-radius: 15px 0px 0px 15px; + border: 2px solid var(--medium-light-grey); + background: var(--white); +} + +.searchbar-container input { + border: none; + outline: none; + width: 400px; + font-family: var(--font-inter); + font-size: 16px; +} + +.searchbar-icon-container { + display: flex; + align-self: stretch; + width: 48px; + padding: 0px 8px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 0px 15px 15px 0px; + background: var(--medium-light-grey); } diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index e243713..426fd66 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -7,14 +7,11 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, - Button, Box, + Button, Flex, HStack, IconButton, - Input, - InputGroup, - InputRightElement, Menu, MenuButton, MenuItem, @@ -29,11 +26,12 @@ import { Tr, useDisclosure, useToast, - VStack + VStack, } from "@chakra-ui/react"; import { useNavigate } from "react-router-dom"; +// Icon imports import actionsSvg from "../../assets/icons/actions.svg"; import activeSvg from "../../assets/icons/active.svg"; import calendarSvg from "../../assets/icons/calendar.svg"; @@ -46,109 +44,24 @@ import pastSvg from "../../assets/icons/past.svg"; import personSvg from "../../assets/icons/person.svg"; import programSvg from "../../assets/icons/program.svg"; import searchSvg from "../../assets/icons/search.svg"; -import StatusTooltip from "./StatusIcon"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { ProgramFiltersModal } from "./ProgramFiltersModal"; +import StatusTooltip from "./StatusIcon"; - -const ActiveStatusIcon = (props) => ( - Active Status -); - -const PastStatusIcon = (props) => ( - Past Status -); - -const FiltersIcon = (props) => ( - Filters -); - -const ActionsIcon = (props) => ( - Actions Menu -); - -const EditIcon = (props) => ( - Edit -); - -const CancelIcon = (props) => ( - Cancel -); - -const SearchIcon = (props) => ( - Search -); - -const CalendarIcon = (props) => ( - Calendar -); - -const ClockIcon = (props) => ( - Clock -); - -const LocationIcon = (props) => ( - Location -); - -const PersonIcon = (props) => ( - Person -); +import "./home.css"; + +// Icon components +const ActiveStatusIcon = () => ; +const PastStatusIcon = () => ; +const FiltersIcon = () => ; +const ActionsIcon = () => ; +const EditIcon = () => ; +const CancelIcon = () => ; +const SearchIcon = () => ; +const CalendarIcon = () => ; +const ClockIcon = () => ; +const LocationIcon = () => ; +const PersonIcon = () => ; export const ProgramsTable = () => { const [programs, setPrograms] = useState([]); @@ -171,26 +84,29 @@ export const ProgramsTable = () => { const toast = useToast(); const cancelRef = useRef(); + // Fetch Programs const fetchPrograms = useCallback(async () => { try { const response = await backend.get("/programs"); - console.log("API Response:", response.data); - + console.log("API Response:", response.data); + if (!response.data || response.data.length === 0) { console.warn("No programs received from API."); setPrograms([]); return; } - + + // Format date and time const formatDate = (dateString) => { const date = new Date(dateString); - const options = { - weekday: "short", - month: "2-digit", - day: "2-digit", - year: "numeric", - }; - return date.toLocaleDateString("en-US", options).replace(/,/g, "."); + return date + .toLocaleDateString("en-US", { + weekday: "short", + month: "2-digit", + day: "2-digit", + year: "numeric", + }) + .replace(/,/g, "."); }; const formatTime = (timeString) => { @@ -200,118 +116,142 @@ export const ProgramsTable = () => { .toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }) .toLowerCase(); }; - - const programsData = response.data.map((program) => ({ + + // Format programs data for display + const programsData = response.data.map((program) => ({ id: program.id, - name: program.eventName, - status: program.date && new Date(program.date) > new Date() ? "Active" : "Past", + name: program.eventName, + status: + program.date && new Date(program.date) > new Date() + ? "Active" + : "Past", upcomingDate: program.date ? formatDate(program.date) : "No bookings", - upcomingTime: program.startTime && program.endTime - ? `${formatTime(program.startTime)} - ${formatTime(program.endTime)}` - : "N/A", + upcomingTime: + program.startTime && program.endTime + ? `${formatTime(program.startTime)} - ${formatTime(program.endTime)}` + : "N/A", room: program.roomName || "N/A", instructor: program.instructorName || "N/A", payee: program.payeeName || "N/A", })); - - console.log("Formatted Programs:", programsData); + + console.log("Formatted Programs:", programsData); setPrograms(programsData); } catch (error) { console.error("Failed to fetch programs:", error); } }, [backend]); - useEffect(() => { fetchPrograms(); }, [fetchPrograms]); - /////////////// - // Filtering // - /////////////// - + // Apply Filters const applyFilters = useCallback(() => { let result = programs; - - // Date Range Filtering + + // Date Range filter if (filters.dateRange.start || filters.dateRange.end) { result = result.filter((program) => { if (!program.date) return false; const bookingDate = new Date(program.date); return ( - (!filters.dateRange.start || bookingDate >= new Date(filters.dateRange.start)) && - (!filters.dateRange.end || bookingDate <= new Date(filters.dateRange.end)) + (!filters.dateRange.start || + bookingDate >= new Date(filters.dateRange.start)) && + (!filters.dateRange.end || + bookingDate <= new Date(filters.dateRange.end)) ); }); } - - // Time Range Filtering + + // Time Range filter if (filters.timeRange.start || filters.timeRange.end) { result = result.filter((program) => { if (program.upcomingTime === "N/A") return false; const [startTime] = program.upcomingTime.split(" - "); const startTimeObj = new Date(`2000-01-01 ${startTime}`); return ( - (!filters.timeRange.start || startTimeObj >= new Date(`2000-01-01 ${filters.timeRange.start}`)) && - (!filters.timeRange.end || startTimeObj <= new Date(`2000-01-01 ${filters.timeRange.end}`)) + (!filters.timeRange.start || + startTimeObj >= + new Date(`2000-01-01 ${filters.timeRange.start}`)) && + (!filters.timeRange.end || + startTimeObj <= new Date(`2000-01-01 ${filters.timeRange.end}`)) ); }); } - - // Other Filters + + // Status filter if (filters.status !== "all") { - result = result.filter((program) => program.status.toLowerCase() === filters.status.toLowerCase()); + result = result.filter( + (program) => + program.status.toLowerCase() === filters.status.toLowerCase() + ); } + + // Room filter if (filters.room !== "all") { result = result.filter((program) => program.room === filters.room); } + + // Instructor filter if (filters.instructor !== "all") { - result = result.filter((program) => program.instructor.toLowerCase() === filters.instructor.toLowerCase()); + result = result.filter( + (program) => + program.instructor && + program.instructor.toLowerCase() === filters.instructor.toLowerCase() + ); } + + // Payee filter if (filters.payee !== "all") { - result = result.filter((program) => program.payee.toLowerCase() === filters.payee.toLowerCase()); + result = result.filter( + (program) => + program.payee && + program.payee.toLowerCase() === filters.payee.toLowerCase() + ); } + // Search filter if (searchTerm.trim() !== "") { result = result.filter((program) => program.name.toLowerCase().includes(searchTerm.toLowerCase()) ); } - + setFilteredPrograms(result); }, [programs, filters, searchTerm]); - - const handleApplyFilters = (newFilters) => { - setFilters(newFilters); - setIsFiltersModalOpen(false); -}; - useEffect(() => { applyFilters(); - }, [filters, programs, searchTerm, applyFilters]); - + }, [applyFilters]); + + const handleApplyFilters = (newFilters) => { + setFilters(newFilters); + setIsFiltersModalOpen(false); + }; + const handleRowClick = (id) => { navigate(`/programs/${id}`); }; - + const handleEdit = (id, e) => { e.stopPropagation(); navigate(`/programs/${id}`); }; - + const handleDeleteClick = (id, e) => { e.stopPropagation(); setProgramToDelete(id); onOpen(); }; - + + // Delete Program and all related records const handleDelete = async () => { if (programToDelete) { try { const response = await backend.delete(`/events/${programToDelete}`); if (response.data.result === "success") { - setPrograms(programs.filter((p) => p.id !== programToDelete)); + setPrograms((prev) => prev.filter((p) => p.id !== programToDelete)); toast({ title: "Program deleted", description: @@ -339,7 +279,6 @@ export const ProgramsTable = () => { onClose(); }; - // Returns the proper status icon const renderStatusIcon = (status) => { switch (status?.toLowerCase()) { case "active": @@ -351,162 +290,234 @@ export const ProgramsTable = () => { } }; - return ( <> - {/* The table */} - - - - - + + setIsFiltersModalOpen(true)} + > + + Filters + + + {/* Spacer to push search bar to the right */} + + +
+
+ setSearchTerm(e.target.value)} /> - - - - - - - + +
+ +
+ + + + {/* Table container */} + +
- {/* Program Name with icon to the RIGHT */} + {/* Program Name */} - - {/* Upcoming Date with icon to the LEFT */} + + {/* Status Column */} + + + {/* Upcoming Date */} - {/* Upcoming Time with icon to the LEFT */} + + {/* Upcoming Time */} - {/* Room with icon to the LEFT */} + + {/* Room */} - {/* Instructor with icon to the LEFT */} + + {/* Instructor */} - {/* Payee with icon to the LEFT */} + + {/* Payee */} - {/* Actions column */} + + {/* Actions Column */} + {filteredPrograms.length === 0 ? ( - ) : ( - filteredPrograms.map((program) => ( - handleRowClick(program.id)} - cursor="pointer" - sx={{ _odd: { bg: "gray.100" }}} - > - - - - - - - - - - )) + filteredPrograms.map((program, idx) => { + // Determine row classes (even/odd and grey-out if no bookings) + let rowClass = + idx % 2 === 0 + ? "programs-table__row--odd" + : "programs-table__row--even"; + if (program.upcomingDate === "No bookings") { + rowClass += " programs-table__row--no-booking"; + } + return ( + handleRowClick(program.id)} + cursor="pointer" + className={rowClass} + > + + + + + + + + + + ); + }) )} -
- - Program + + Program - - + + + + - - - Upcoming Date + + + Upcoming Date - - + + - - Upcoming Time + + Upcoming Time - - Room + + + Room + - - Instructor + + Instructor - - Payee + + Payee
+ No programs available.
{program.name}{renderStatusIcon(program.status)}{program.upcomingDate}{program.upcomingTime}{program.room}{program.instructor}{program.payee} e.stopPropagation()}> - - } - variant="ghost" - bg="transparent" - _hover={{ bg: "purple.100" }} - /> - - handleEdit(program.id, e)}> - - Edit - - handleDeleteClick(program.id, e)} - color="red.500" - > - - Delete - - - -
{program.name}{renderStatusIcon(program.status)}{program.upcomingDate}{program.upcomingTime}{program.room}{program.instructor}{program.payee} e.stopPropagation()} + > + + } + variant="ghost" + bg="transparent" + _hover={{ bg: "transparent" }} + className="actions-container" + /> + + handleEdit(program.id, e)} + className="menu-item menu-item--edit" + > + + Edit + + handleDeleteClick(program.id, e)} + className="menu-item menu-item--delete" + > + + Delete + + + +
+ {/* Delete Confirmation AlertDialog */} - + Delete Program - Are you sure? You can't undo this action afterwards. - - {/* ACTIVE */} - - - - - + {/* === TIME FILTERS === */} + + + + Time + + + setStartTime(e.target.value)} + /> + setEndTime(e.target.value)} + /> + + - {/* === ROOM FILTER (PILL BUTTONS) === */} - - - - Room - - + Status + + + + {/* ACTIVE */} + + + + + + + {/* === ROOM FILTER (PILL BUTTONS) === */} + + + + Room + + + {/* "All" pill */} + + + {/* Each room from the backend */} + {rooms.map((r) => ( + ))} + + - {/* Each room from the backend */} - {rooms.map((r) => ( - - ))} - - - - {/* === INSTRUCTOR DROPDOWN === */} - - - - Instructor(s) - - - - - {/* === PAYEE DROPDOWN === */} - - - - Payee - - - - - + {/* === INSTRUCTOR DROPDOWN === */} + + + + Instructor(s) + + + - - + + {/* + - - - - + */} + + ); }; diff --git a/client/src/components/home/temp.jsx b/client/src/components/home/temp.jsx new file mode 100644 index 0000000..edc1d71 --- /dev/null +++ b/client/src/components/home/temp.jsx @@ -0,0 +1,296 @@ +import React, { useEffect, useState } from "react"; + +import { + Box, + Button, + ButtonGroup, + FormControl, + FormLabel, + HStack, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Select, + Text, + VStack, +} from "@chakra-ui/react"; + +import activeSvg from "../../assets/icons/active.svg"; +// Icons for styling +import calendarSvg from "../../assets/icons/calendar.svg"; +import clockSvg from "../../assets/icons/clock.svg"; +import locationSvg from "../../assets/icons/location.svg"; +import pastSvg from "../../assets/icons/past.svg"; +import personSvg from "../../assets/icons/person.svg"; +import { useBackendContext } from "../../contexts/hooks/useBackendContext"; + +export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { + // State for filters + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [status, setStatus] = useState("all"); + const [room, setRoom] = useState("all"); + const [instructor, setInstructor] = useState("all"); + const [payee, setPayee] = useState("all"); + + // State for data fetching + const [rooms, setRooms] = useState([]); + const [clients, setClients] = useState([]); + + const { backend } = useBackendContext(); + + // Fetch rooms & clients on mount + useEffect(() => { + const fetchData = async () => { + try { + const roomsResponse = await backend.get("/rooms"); + setRooms(roomsResponse.data); + + const clientsResponse = await backend.get("/clients"); + setClients(clientsResponse.data); + } catch (error) { + console.error("Failed to fetch filter data:", error); + } + }; + + fetchData(); + }, [backend]); + + // Called when user clicks "Apply" + const handleApply = () => { + onApplyFilters({ + dateRange: { start: startDate, end: endDate }, + timeRange: { start: startTime, end: endTime }, + status, + room, + instructor, + payee, + }); + onClose(); + }; + + return ( + + + + Filter Programs + + + + {/* === DATE FILTERS === */} + + + + Date + + + setStartDate(e.target.value)} + placeholder="Start Date" + /> + setEndDate(e.target.value)} + placeholder="End Date" + /> + + + + {/* === TIME FILTERS === */} + + + + Time + + + setStartTime(e.target.value)} + /> + setEndTime(e.target.value)} + /> + + + + {/* === STATUS FILTER (BUTTON GROUP) === */} + + Status + + + + {/* ACTIVE */} + + + + + + + {/* === ROOM FILTER (PILL BUTTONS) === */} + + + + Room + + + {/* "All" pill */} + + + {/* Each room from the backend */} + {rooms.map((r) => ( + + ))} + + + + {/* === INSTRUCTOR DROPDOWN === */} + + + + Instructor(s) + + + + + {/* === PAYEE DROPDOWN === */} + + + + Payee + + + + + + + + + + + + + ); +}; + +export default ProgramFiltersModal; From b84b37df3accfada4f7c2962f2739b34e9803c01 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Tue, 4 Feb 2025 18:15:38 -0800 Subject: [PATCH 31/41] style: add in finalized touches --- client/src/components/home/Home.css | 162 +++++++++++------- client/src/components/home/HomeComponents.jsx | 5 +- .../components/home/ProgramFiltersModal.jsx | 141 ++++++++------- client/src/components/home/StatusIcon.jsx | 20 +-- client/src/components/home/temp.jsx | 52 +++++- 5 files changed, 224 insertions(+), 156 deletions(-) diff --git a/client/src/components/home/Home.css b/client/src/components/home/Home.css index 5ab1fbd..3452047 100644 --- a/client/src/components/home/Home.css +++ b/client/src/components/home/Home.css @@ -1,13 +1,17 @@ :root { - --light-grey: #F6F6F6; + --light-grey: #f6f6f6; --medium-grey: #767778; - --medium-light-grey: #D2D2D2; - --white: #FFF; - --indigo: #4E4AE7; - --deep-red: #90080F; + --medium-light-grey: #d2d2d2; + --white: #fff; + --indigo: #4e4ae7; + --deep-red: #90080f; --font-inter: "Inter", sans-serif; } +/* ========================= */ +/* Global / Page Layout CSS */ +/* ========================= */ + .home { display: flex; padding: 32px 32px 0px 32px; @@ -112,6 +116,10 @@ font-weight: 600; } +/* ========================= */ +/* Programs Table CSS */ +/* ========================= */ + .programs-table { width: 100%; margin: 40px auto 0 auto; @@ -153,15 +161,20 @@ background-color: #e0e0e0 !important; } +/* ========================= */ +/* Filter Box & Search CSS */ +/* ========================= */ + .filter-box { - border-radius: 15px; - border: 1px solid var(--medium-light-grey); - background: var(--white); - display: flex; - height: 54.795px; - padding: 8px 16px; - justify-content: center; - align-items: center; + height: 54.795px !important; + min-height: 54.795px !important; + padding: 8px 16px !important; + border-radius: 15px !important; + border: 1px solid var(--medium-light-grey) !important; + background: var(--white) !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; gap: 4px; cursor: pointer; } @@ -173,15 +186,51 @@ font-weight: 600; } -.table-header-text { - color: var(--medium-grey); +.search-wrapper { + display: flex; + align-items: center; + gap: 0; + height: 54.795px; +} + +.searchbar-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + flex: 1 0 0; + align-self: stretch; + padding: 0px 16px; + border-radius: 15px 0px 0px 15px; + border: 2px solid var(--medium-light-grey); + background: var(--white); +} + +.searchbar-container input { + border: none; + outline: none; + width: 400px; font-family: var(--font-inter); font-size: 16px; - font-weight: 600; - line-height: 20px; - text-transform: none; } +.searchbar-icon-container { + display: flex; + align-self: stretch; + width: 48px; + padding: 0px 8px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 0px 15px 15px 0px; + background: var(--medium-light-grey); +} + +/* ========================= */ +/* Menu & Actions CSS */ +/* ========================= */ + .actions-container { display: flex; width: 48px; @@ -198,24 +247,16 @@ background: transparent !important; } -.menu-list-custom { - border-radius: 15px; - border: 1px solid var(--medium-light-grey, #D2D2D2); - background: var(--light-grey, #F6F6F6); - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); - padding: 8px; -} - .menu-list-custom { border-radius: 15px !important; - border: 1px solid var(--medium-light-grey, #D2D2D2) !important; - background: var(--light-grey, #F6F6F6) !important; + border: 1px solid var(--medium-light-grey, #d2d2d2) !important; + background: var(--light-grey, #f6f6f6) !important; box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25) !important; padding: 8px !important; } .menu-list-custom .chakra-menu__menuitem { - background: var(--light-grey, #F6F6F6) !important; + background: var(--light-grey, #f6f6f6) !important; display: flex !important; padding: 8px 12px !important; align-items: center !important; @@ -236,46 +277,39 @@ } .menu-item--delete { - color: var(--deep-red, #90080F) !important; -} - -.search-wrapper { - display: flex; - align-items: center; - gap: 0; - height: 54.795px; + color: var(--deep-red, #90080f) !important; } -.searchbar-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 10px; - flex: 1 0 0; - align-self: stretch; - padding: 0px 16px; - border-radius: 15px 0px 0px 15px; - border: 2px solid var(--medium-light-grey); - background: var(--white); -} +/* ========================= */ +/* Table Header Text */ +/* ========================= */ -.searchbar-container input { - border: none; - outline: none; - width: 400px; +.table-header-text { + color: var(--medium-grey); font-family: var(--font-inter); font-size: 16px; + font-weight: 600; + line-height: 20px; + text-transform: none; } -.searchbar-icon-container { - display: flex; - align-self: stretch; - width: 48px; - padding: 0px 8px; - justify-content: center; - align-items: center; - gap: 10px; - border-radius: 0px 15px 15px 0px; - background: var(--medium-light-grey); +/* ========================= */ +/* Additional CSS for Filters and Tooltip */ +/* ========================= */ + +.program-filters-menu-list { + padding: 12px; + max-height: 500px; + max-width: 400px; + min-width: 100px; + overflow-y: auto; +} + +.status-tooltip-text { + font-size: 16px; + font-family: var(--font-inter); + font-weight: 400; + color: var(--medium-grey, #767778); + line-height: 20px; + text-transform: none; } diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 84e69c8..7fd3cb4 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -93,7 +93,7 @@ export const ProgramsTable = () => { setPrograms([]); return; } - + // Format date and time const formatDate = (dateString) => { const date = new Date(dateString); @@ -114,7 +114,7 @@ export const ProgramsTable = () => { .toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }) .toLowerCase(); }; - + // Format programs data for display const programsData = response.data.map((program) => ({ id: program.id, @@ -293,7 +293,6 @@ export const ProgramsTable = () => { {/* Container for the table + filter/search row */} - {/* Spacer to push search bar to the right */} diff --git a/client/src/components/home/ProgramFiltersModal.jsx b/client/src/components/home/ProgramFiltersModal.jsx index 44cb5c4..5c5980d 100644 --- a/client/src/components/home/ProgramFiltersModal.jsx +++ b/client/src/components/home/ProgramFiltersModal.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; + import { Box, Button, @@ -10,7 +11,6 @@ import { Menu, MenuButton, MenuList, - MenuItem, Select, Text, VStack, @@ -23,13 +23,18 @@ import filterSvg from "../../assets/icons/filter.svg"; import locationSvg from "../../assets/icons/location.svg"; import pastSvg from "../../assets/icons/past.svg"; import personSvg from "../../assets/icons/person.svg"; - -// Icons import -const FiltersIcon = () => ; - import { useBackendContext } from "../../contexts/hooks/useBackendContext"; +// Icon component for Filters +const FiltersIcon = () => ( + Filters +); + export const ProgramFiltersModal = ({ onApplyFilters }) => { + // Filter state variables const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [startTime, setStartTime] = useState(""); @@ -60,7 +65,8 @@ export const ProgramFiltersModal = ({ onApplyFilters }) => { fetchData(); }, [backend]); - const handleApply = () => { + // Automatically update filters whenever any filter state changes. + useEffect(() => { onApplyFilters({ dateRange: { start: startDate, end: endDate }, timeRange: { start: startTime, end: endTime }, @@ -69,28 +75,36 @@ export const ProgramFiltersModal = ({ onApplyFilters }) => { instructor, payee, }); - }; + }, [ + startDate, + endDate, + startTime, + endTime, + status, + room, + instructor, + payee, + onApplyFilters, + ]); return ( - {/* Dropdown Trigger Button (Using Unicode Arrow ▼) */} - } + } + _hover={{ bg: "transparent" }} _active={{ bg: "transparent" }} > Filters - {/* Dropdown Menu */} - - - {/* === DATE FILTERS === */} + + { - {/* === TIME FILTERS === */} { - {/* === STATUS FILTER (BUTTON GROUP) === */} Status - - - {/* ACTIVE */} - - {/* === ROOM FILTER (PILL BUTTONS) === */} - + Room - {/* "All" pill */} - - {/* Each room from the backend */} {rooms.map((r) => ( - - {/* - - - */} ); diff --git a/client/src/components/home/StatusIcon.jsx b/client/src/components/home/StatusIcon.jsx index 2485f49..5644fed 100644 --- a/client/src/components/home/StatusIcon.jsx +++ b/client/src/components/home/StatusIcon.jsx @@ -38,31 +38,19 @@ const StatusTooltip = () => { - - Active - + Active - - Past - + Past diff --git a/client/src/components/home/temp.jsx b/client/src/components/home/temp.jsx index edc1d71..60f7cd9 100644 --- a/client/src/components/home/temp.jsx +++ b/client/src/components/home/temp.jsx @@ -155,8 +155,12 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { Status - - {/* ACTIVE */} - @@ -191,7 +210,12 @@ export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { {/* === ROOM FILTER (PILL BUTTONS) === */} - + Room { {/* === INSTRUCTOR DROPDOWN === */} - + Instructor(s) Date: Tue, 4 Feb 2025 18:24:04 -0800 Subject: [PATCH 32/41] fix: fix to pass merge tests --- client/src/components/home/Home.jsx | 2 +- client/src/components/home/HomeComponents.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/home/Home.jsx b/client/src/components/home/Home.jsx index b458d6c..8d69839 100644 --- a/client/src/components/home/Home.jsx +++ b/client/src/components/home/Home.jsx @@ -2,7 +2,7 @@ import React from "react"; import { Box } from "@chakra-ui/react"; -import "./home.css"; +import "./Home.css"; import { Navbar } from "../Navbar"; import { HeaderRowComponent } from "./HeaderRowComponent"; diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 7fd3cb4..02599d8 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -47,7 +47,7 @@ import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { ProgramFiltersModal } from "./ProgramFiltersModal"; import StatusTooltip from "./StatusIcon"; -import "./home.css"; +import "./Home.css"; // Icon components const ActiveStatusIcon = () => ; From 035d7ddcacd87121b52fbf8bfb4b0ef7ae0825e3 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Thu, 6 Feb 2025 11:15:00 -0800 Subject: [PATCH 33/41] fix: filter by date + fix navbar + add multiple instructors/payees to frontend (forgot to do this my bad lol) --- client/src/components/home/Home.css | 7 ++- client/src/components/home/Home.jsx | 9 ++- client/src/components/home/HomeComponents.jsx | 55 +++++++++++++------ server/routes/programs.js | 29 ++++------ 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/client/src/components/home/Home.css b/client/src/components/home/Home.css index 3452047..e86ec48 100644 --- a/client/src/components/home/Home.css +++ b/client/src/components/home/Home.css @@ -22,10 +22,13 @@ .home-inner { display: flex; + width: 1230px; + height: 1024px; + padding: 64px 64px 0px 32px; flex-direction: column; align-items: flex-start; - gap: 24px; - align-self: stretch; + gap: 32px; + flex-shrink: 0; } .header-row { diff --git a/client/src/components/home/Home.jsx b/client/src/components/home/Home.jsx index 8d69839..750a56e 100644 --- a/client/src/components/home/Home.jsx +++ b/client/src/components/home/Home.jsx @@ -2,20 +2,19 @@ import React from "react"; import { Box } from "@chakra-ui/react"; -import "./Home.css"; - import { Navbar } from "../Navbar"; import { HeaderRowComponent } from "./HeaderRowComponent"; import { ProgramsTable } from "./HomeComponents"; +import "./Home.css"; + export const Home = () => { return ( - - + - + ); }; diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 02599d8..36f85e4 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -116,22 +116,35 @@ export const ProgramsTable = () => { }; // Format programs data for display - const programsData = response.data.map((program) => ({ - id: program.id, - name: program.eventName, - status: - program.date && new Date(program.date) > new Date() - ? "Active" - : "Past", - upcomingDate: program.date ? formatDate(program.date) : "No bookings", - upcomingTime: - program.startTime && program.endTime - ? `${formatTime(program.startTime)} - ${formatTime(program.endTime)}` - : "N/A", - room: program.roomName || "N/A", - instructor: program.instructorName || "N/A", - payee: program.payeeName || "N/A", - })); + const programsData = response.data.map((program) => { + // Handle multiple instructors or payees: + const instructor = Array.isArray(program.instructorName) + ? program.instructorName.join(", ") + : program.instructorName || "N/A"; + + const payee = Array.isArray(program.payeeName) + ? program.payeeName.join(", ") + : program.payeeName || "N/A"; + + return { + id: program.id, + name: program.eventName, + // Preserve the original date for filtering: + date: program.date, + status: + program.date && new Date(program.date) > new Date() + ? "Active" + : "Past", + upcomingDate: program.date ? formatDate(program.date) : "No bookings", + upcomingTime: + program.startTime && program.endTime + ? `${formatTime(program.startTime)} - ${formatTime(program.endTime)}` + : "N/A", + room: program.roomName || "N/A", + instructor, // possibly a comma-separated list + payee, // possibly a comma-separated list + }; + }); console.log("Formatted Programs:", programsData); setPrograms(programsData); @@ -144,6 +157,12 @@ export const ProgramsTable = () => { fetchPrograms(); }, [fetchPrograms]); + // Truncate long lists of instructors/payees + const truncateNames = (names, maxLength = 30) => { + if (names.length <= maxLength) return names; + return `${names.substring(0, maxLength)}...`; + }; + // Apply Filters const applyFilters = useCallback(() => { let result = programs; @@ -449,8 +468,8 @@ export const ProgramsTable = () => { {program.upcomingDate} {program.upcomingTime} {program.room} - {program.instructor} - {program.payee} + {truncateNames(program.instructor)} + {truncateNames(program.payee)} e.stopPropagation()} diff --git a/server/routes/programs.js b/server/routes/programs.js index f5756e0..44d551f 100644 --- a/server/routes/programs.js +++ b/server/routes/programs.js @@ -10,14 +10,15 @@ programsRouter.get("/", async (req, res) => { try { const programs = await db.any(` SELECT DISTINCT ON (e.id) - e.id, - e.name AS event_name, - b.date, - b.start_time, - b.end_time, - r.name AS room_name, - MAX(CASE WHEN a.role = 'instructor' THEN c.name END) AS instructor_name, - MAX(CASE WHEN a.role = 'payee' THEN c.name END) AS payee_name + e.id, + e.name AS event_name, + b.date, + b.start_time, + b.end_time, + r.name AS room_name, + -- Use string_agg to get all instructors concatenated, and trim extra commas if needed + COALESCE(string_agg(DISTINCT CASE WHEN a.role = 'instructor' THEN c.name END, ', '), 'N/A') AS instructor_name, + COALESCE(string_agg(DISTINCT CASE WHEN a.role = 'payee' THEN c.name END, ', '), 'N/A') AS payee_name FROM events AS e LEFT JOIN assignments AS a ON e.id = a.event_id LEFT JOIN bookings AS b ON e.id = b.event_id @@ -41,9 +42,9 @@ programsRouter.get("/:id", async (req, res) => { try { const { id } = req.params; const query = ` - SELECT e.id, e.name, b.date, b.start_time, b.end_time, r.name, - MAX(CASE WHEN a.role = 'instructor' THEN c.name END) AS instructor_name, - MAX(CASE WHEN a.role = 'payee' THEN c.name END) AS payee_name + SELECT e.id, e.name, b.date, b.start_time, b.end_time, r.name AS room_name, + COALESCE(string_agg(DISTINCT CASE WHEN a.role = 'instructor' THEN c.name END, ', '), 'N/A') AS instructor_name, + COALESCE(string_agg(DISTINCT CASE WHEN a.role = 'payee' THEN c.name END, ', '), 'N/A') AS payee_name FROM events AS e JOIN bookings AS b ON e.id = b.event_id JOIN rooms AS r ON b.room_id = r.id @@ -66,9 +67,3 @@ programsRouter.get("/:id", async (req, res) => { }); export { programsRouter }; - -// Assignment (client_id, role), -DONE- -// Events (id, name), -DONE- -// Bookings (date, start_time, end_time, room_id), -DONE- -// Rooms (name), -DONE- -// Client (name) From c61c82d6baa4d59b8f25cca0de685bbff67f90d3 Mon Sep 17 00:00:00 2001 From: Nathan Pietrantonio Date: Thu, 6 Feb 2025 16:00:10 -0800 Subject: [PATCH 34/41] Make /home protected --- client/src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 1bd5c97..af7bd19 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -47,7 +47,7 @@ const App = () => { /> } + element={} />} /> Date: Thu, 6 Feb 2025 16:06:05 -0800 Subject: [PATCH 35/41] deleted temp reference --- client/src/components/home/temp.jsx | 330 ---------------------------- 1 file changed, 330 deletions(-) delete mode 100644 client/src/components/home/temp.jsx diff --git a/client/src/components/home/temp.jsx b/client/src/components/home/temp.jsx deleted file mode 100644 index 60f7cd9..0000000 --- a/client/src/components/home/temp.jsx +++ /dev/null @@ -1,330 +0,0 @@ -import React, { useEffect, useState } from "react"; - -import { - Box, - Button, - ButtonGroup, - FormControl, - FormLabel, - HStack, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Select, - Text, - VStack, -} from "@chakra-ui/react"; - -import activeSvg from "../../assets/icons/active.svg"; -// Icons for styling -import calendarSvg from "../../assets/icons/calendar.svg"; -import clockSvg from "../../assets/icons/clock.svg"; -import locationSvg from "../../assets/icons/location.svg"; -import pastSvg from "../../assets/icons/past.svg"; -import personSvg from "../../assets/icons/person.svg"; -import { useBackendContext } from "../../contexts/hooks/useBackendContext"; - -export const ProgramFiltersModal = ({ isOpen, onClose, onApplyFilters }) => { - // State for filters - const [startDate, setStartDate] = useState(""); - const [endDate, setEndDate] = useState(""); - const [startTime, setStartTime] = useState(""); - const [endTime, setEndTime] = useState(""); - const [status, setStatus] = useState("all"); - const [room, setRoom] = useState("all"); - const [instructor, setInstructor] = useState("all"); - const [payee, setPayee] = useState("all"); - - // State for data fetching - const [rooms, setRooms] = useState([]); - const [clients, setClients] = useState([]); - - const { backend } = useBackendContext(); - - // Fetch rooms & clients on mount - useEffect(() => { - const fetchData = async () => { - try { - const roomsResponse = await backend.get("/rooms"); - setRooms(roomsResponse.data); - - const clientsResponse = await backend.get("/clients"); - setClients(clientsResponse.data); - } catch (error) { - console.error("Failed to fetch filter data:", error); - } - }; - - fetchData(); - }, [backend]); - - // Called when user clicks "Apply" - const handleApply = () => { - onApplyFilters({ - dateRange: { start: startDate, end: endDate }, - timeRange: { start: startTime, end: endTime }, - status, - room, - instructor, - payee, - }); - onClose(); - }; - - return ( - - - - Filter Programs - - - - {/* === DATE FILTERS === */} - - - - Date - - - setStartDate(e.target.value)} - placeholder="Start Date" - /> - setEndDate(e.target.value)} - placeholder="End Date" - /> - - - - {/* === TIME FILTERS === */} - - - - Time - - - setStartTime(e.target.value)} - /> - setEndTime(e.target.value)} - /> - - - - {/* === STATUS FILTER (BUTTON GROUP) === */} - - Status - - - - {/* ACTIVE */} - - - - - - - {/* === ROOM FILTER (PILL BUTTONS) === */} - - - - Room - - - {/* "All" pill */} - - - {/* Each room from the backend */} - {rooms.map((r) => ( - - ))} - - - - {/* === INSTRUCTOR DROPDOWN === */} - - - - Instructor(s) - - - - - {/* === PAYEE DROPDOWN === */} - - - - Payee - - - - - - - - - - - - - ); -}; - -export default ProgramFiltersModal; From ac5da4b1fa81273aacab519d6754b061882bcf90 Mon Sep 17 00:00:00 2001 From: Nathan Pietrantonio Date: Sat, 8 Feb 2025 18:26:00 -0800 Subject: [PATCH 36/41] Fix spacing and row color --- client/src/components/home/Home.css | 6 +++--- client/src/components/home/HomeComponents.jsx | 9 +-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/client/src/components/home/Home.css b/client/src/components/home/Home.css index e86ec48..562c0c2 100644 --- a/client/src/components/home/Home.css +++ b/client/src/components/home/Home.css @@ -14,6 +14,7 @@ .home { display: flex; + width: 100%; padding: 32px 32px 0px 32px; align-items: flex-start; gap: 32px; @@ -22,9 +23,8 @@ .home-inner { display: flex; - width: 1230px; - height: 1024px; - padding: 64px 64px 0px 32px; + width: 100%; + padding: 64px 32px 0px 32px; flex-direction: column; align-items: flex-start; gap: 32px; diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 36f85e4..de8b605 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -448,14 +448,7 @@ export const ProgramsTable = () => { ) : ( filteredPrograms.map((program, idx) => { - // Determine row classes (even/odd and grey-out if no bookings) - let rowClass = - idx % 2 === 0 - ? "programs-table__row--odd" - : "programs-table__row--even"; - if (program.upcomingDate === "No bookings") { - rowClass += " programs-table__row--no-booking"; - } + const rowClass = "programs-table__row--even"; return ( Date: Sat, 8 Feb 2025 18:30:07 -0800 Subject: [PATCH 37/41] Add grey icon for programs with no booking --- client/src/assets/icons/none.svg | 5 +++++ client/src/components/home/HomeComponents.jsx | 10 +++++++--- client/src/components/home/StatusIcon.jsx | 13 ++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 client/src/assets/icons/none.svg diff --git a/client/src/assets/icons/none.svg b/client/src/assets/icons/none.svg new file mode 100644 index 0000000..9bf7918 --- /dev/null +++ b/client/src/assets/icons/none.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index de8b605..bf7c027 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -43,6 +43,7 @@ import pastSvg from "../../assets/icons/past.svg"; import personSvg from "../../assets/icons/person.svg"; import programSvg from "../../assets/icons/program.svg"; import searchSvg from "../../assets/icons/search.svg"; +import noneSvg from "../../assets/icons/none.svg"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { ProgramFiltersModal } from "./ProgramFiltersModal"; import StatusTooltip from "./StatusIcon"; @@ -52,6 +53,7 @@ import "./Home.css"; // Icon components const ActiveStatusIcon = () => ; const PastStatusIcon = () => ; +const NoneStatusIcon = () => ; const ActionsIcon = () => ; const EditIcon = () => ; const CancelIcon = () => ; @@ -132,9 +134,11 @@ export const ProgramsTable = () => { // Preserve the original date for filtering: date: program.date, status: - program.date && new Date(program.date) > new Date() + program.date ? + (program.date && new Date(program.date) > new Date() ? "Active" - : "Past", + : "Past") : + "No Bookings", upcomingDate: program.date ? formatDate(program.date) : "No bookings", upcomingTime: program.startTime && program.endTime @@ -303,7 +307,7 @@ export const ProgramsTable = () => { case "past": return ; default: - return null; + return ; } }; diff --git a/client/src/components/home/StatusIcon.jsx b/client/src/components/home/StatusIcon.jsx index 5644fed..4198a60 100644 --- a/client/src/components/home/StatusIcon.jsx +++ b/client/src/components/home/StatusIcon.jsx @@ -12,6 +12,7 @@ import { import activeSvg from "../../assets/icons/active.svg"; import pastSvg from "../../assets/icons/past.svg"; +import noneSvg from "../../assets/icons/none.svg"; const StatusTooltip = () => { return ( @@ -28,7 +29,8 @@ const StatusTooltip = () => { { /> Past + + + No Bookings + From b850aceb7e911003ace2f0361e61b67721969eb3 Mon Sep 17 00:00:00 2001 From: Nathan Pietrantonio Date: Sat, 8 Feb 2025 18:35:20 -0800 Subject: [PATCH 38/41] Revert "Add grey icon for programs with no booking" This reverts commit 35d08c18fe87132d1308a33daa906bd971e7b9c6. --- client/src/assets/icons/none.svg | 5 ----- client/src/components/home/HomeComponents.jsx | 10 +++------- client/src/components/home/StatusIcon.jsx | 13 +------------ 3 files changed, 4 insertions(+), 24 deletions(-) delete mode 100644 client/src/assets/icons/none.svg diff --git a/client/src/assets/icons/none.svg b/client/src/assets/icons/none.svg deleted file mode 100644 index 9bf7918..0000000 --- a/client/src/assets/icons/none.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index bf7c027..de8b605 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -43,7 +43,6 @@ import pastSvg from "../../assets/icons/past.svg"; import personSvg from "../../assets/icons/person.svg"; import programSvg from "../../assets/icons/program.svg"; import searchSvg from "../../assets/icons/search.svg"; -import noneSvg from "../../assets/icons/none.svg"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { ProgramFiltersModal } from "./ProgramFiltersModal"; import StatusTooltip from "./StatusIcon"; @@ -53,7 +52,6 @@ import "./Home.css"; // Icon components const ActiveStatusIcon = () => ; const PastStatusIcon = () => ; -const NoneStatusIcon = () => ; const ActionsIcon = () => ; const EditIcon = () => ; const CancelIcon = () => ; @@ -134,11 +132,9 @@ export const ProgramsTable = () => { // Preserve the original date for filtering: date: program.date, status: - program.date ? - (program.date && new Date(program.date) > new Date() + program.date && new Date(program.date) > new Date() ? "Active" - : "Past") : - "No Bookings", + : "Past", upcomingDate: program.date ? formatDate(program.date) : "No bookings", upcomingTime: program.startTime && program.endTime @@ -307,7 +303,7 @@ export const ProgramsTable = () => { case "past": return ; default: - return ; + return null; } }; diff --git a/client/src/components/home/StatusIcon.jsx b/client/src/components/home/StatusIcon.jsx index 4198a60..5644fed 100644 --- a/client/src/components/home/StatusIcon.jsx +++ b/client/src/components/home/StatusIcon.jsx @@ -12,7 +12,6 @@ import { import activeSvg from "../../assets/icons/active.svg"; import pastSvg from "../../assets/icons/past.svg"; -import noneSvg from "../../assets/icons/none.svg"; const StatusTooltip = () => { return ( @@ -29,8 +28,7 @@ const StatusTooltip = () => { { /> Past - - - No Bookings - From 91f75191a1aa398b461040f31f5d531cc537e845 Mon Sep 17 00:00:00 2001 From: Nathan Pietrantonio Date: Sat, 8 Feb 2025 18:39:32 -0800 Subject: [PATCH 39/41] Fix no booking status icon --- client/src/assets/icons/none.svg | 5 +++++ client/src/components/home/HomeComponents.jsx | 12 ++++++++---- client/src/components/home/StatusIcon.jsx | 15 +++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 client/src/assets/icons/none.svg diff --git a/client/src/assets/icons/none.svg b/client/src/assets/icons/none.svg new file mode 100644 index 0000000..13689f1 --- /dev/null +++ b/client/src/assets/icons/none.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index de8b605..9d7777c 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -43,6 +43,7 @@ import pastSvg from "../../assets/icons/past.svg"; import personSvg from "../../assets/icons/person.svg"; import programSvg from "../../assets/icons/program.svg"; import searchSvg from "../../assets/icons/search.svg"; +import noneSvg from "../../assets/icons/none.svg"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; import { ProgramFiltersModal } from "./ProgramFiltersModal"; import StatusTooltip from "./StatusIcon"; @@ -52,6 +53,7 @@ import "./Home.css"; // Icon components const ActiveStatusIcon = () => ; const PastStatusIcon = () => ; +const NoneStatusIcon = () => ; const ActionsIcon = () => ; const EditIcon = () => ; const CancelIcon = () => ; @@ -132,9 +134,11 @@ export const ProgramsTable = () => { // Preserve the original date for filtering: date: program.date, status: - program.date && new Date(program.date) > new Date() + program.date ? + (program.date && new Date(program.date) > new Date() ? "Active" - : "Past", + : "Past") : + "No Bookings", upcomingDate: program.date ? formatDate(program.date) : "No bookings", upcomingTime: program.startTime && program.endTime @@ -303,7 +307,7 @@ export const ProgramsTable = () => { case "past": return ; default: - return null; + return ; } }; @@ -541,4 +545,4 @@ export const ProgramsTable = () => { ); -}; +}; \ No newline at end of file diff --git a/client/src/components/home/StatusIcon.jsx b/client/src/components/home/StatusIcon.jsx index 5644fed..ea1cf1c 100644 --- a/client/src/components/home/StatusIcon.jsx +++ b/client/src/components/home/StatusIcon.jsx @@ -12,6 +12,7 @@ import { import activeSvg from "../../assets/icons/active.svg"; import pastSvg from "../../assets/icons/past.svg"; +import noneSvg from "../../assets/icons/none.svg"; const StatusTooltip = () => { return ( @@ -28,7 +29,8 @@ const StatusTooltip = () => { { /> Past + + + No Bookings + ); }; -export default StatusTooltip; +export default StatusTooltip; \ No newline at end of file From 5510d3a54fb67c5b6902705b1c8bb9960369ebf9 Mon Sep 17 00:00:00 2001 From: margoglvz Date: Sun, 9 Feb 2025 17:35:58 -0800 Subject: [PATCH 40/41] filters can search sole instructor/payee when multiple instructors/payees in a single booking --- client/src/components/home/HomeComponents.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/home/HomeComponents.jsx b/client/src/components/home/HomeComponents.jsx index 9d7777c..87b3331 100644 --- a/client/src/components/home/HomeComponents.jsx +++ b/client/src/components/home/HomeComponents.jsx @@ -219,7 +219,7 @@ export const ProgramsTable = () => { result = result.filter( (program) => program.instructor && - program.instructor.toLowerCase() === filters.instructor.toLowerCase() + program.instructor.toLowerCase().includes(filters.instructor.toLowerCase()) ); } @@ -228,7 +228,7 @@ export const ProgramsTable = () => { result = result.filter( (program) => program.payee && - program.payee.toLowerCase() === filters.payee.toLowerCase() + program.payee.toLowerCase().includes(filters.payee.toLowerCase()) ); } From 02bc580242e489a2a413b5f3440be1e31f2e7db4 Mon Sep 17 00:00:00 2001 From: Nathan Pietrantonio Date: Mon, 10 Feb 2025 14:00:24 -0800 Subject: [PATCH 41/41] Remove duplicate import in App --- client/src/App.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index ae1616b..6c9701b 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -24,7 +24,6 @@ import { Signup } from "./components/signup/Signup"; import { AuthProvider } from "./contexts/AuthContext"; import { BackendProvider } from "./contexts/BackendContext"; import { RoleProvider } from "./contexts/RoleContext"; -import { ForgotPassword } from "./components/login/ForgotPassword"; import { Home } from "./components/home/Home"; const App = () => {