diff --git a/client/package.json b/client/package.json index b92ac2a..f3bba17 100644 --- a/client/package.json +++ b/client/package.json @@ -11,19 +11,18 @@ "build": "tsc -b && vite build", "lint": "eslint", "format": "npx prettier --write .", - "preview": "vite preview", - "postinstall": "patch-package" + "preview": "vite preview" }, "dependencies": { "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@hookform/resolvers": "^3.9.0", + "@react-pdf/renderer": "^4.1.6", "axios": "^1.7.4", "firebase": "10.12.5", "framer-motion": "^11.3.27", "patch-package": "^8.0.0", - "postinstall-postinstall": "^2.1.0", "react": "^18.3.1", "react-cookie": "^7.2.0", "react-dom": "^18.3.1", diff --git a/client/src/App.jsx b/client/src/App.jsx index be8d87c..ccda6c8 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -16,6 +16,9 @@ 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 { PDFViewer } from "@react-pdf/renderer"; +import PDFButton from "./components/PDFButton"; const App = () => { return ( @@ -50,7 +53,6 @@ const App = () => { /> } /> - { path="*" element={} />} /> + } + /> diff --git a/client/src/assets/logo/Logo.jsx b/client/src/assets/logo/Logo.jsx new file mode 100644 index 0000000..81ac98e --- /dev/null +++ b/client/src/assets/logo/Logo.jsx @@ -0,0 +1,9 @@ +import LogoImg from "./logo.png"; + +// NOTE: Do not import logo.png, use Logo component instead + +export const Logo = () => { + return ( + + ); +}; diff --git a/client/src/assets/logo/logo.png b/client/src/assets/logo/logo.png new file mode 100644 index 0000000..5c8cdf2 Binary files /dev/null and b/client/src/assets/logo/logo.png differ diff --git a/client/src/components/AddClassModal.jsx b/client/src/components/AddClassModal.jsx new file mode 100644 index 0000000..ffa0ef7 --- /dev/null +++ b/client/src/components/AddClassModal.jsx @@ -0,0 +1,199 @@ +import { + useDisclosure, + Button, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalCloseButton, + FormLabel, + Input, + FormControl, + ModalBody, + Select, + Flex, + IconButton, + Box +} from "@chakra-ui/react"; +import React, { useState, useEffect } from 'react'; + +import { useBackendContext } from "../contexts/hooks/useBackendContext"; + + +export const AddClassModal = () => { + const { isOpen, onOpen, onClose } = useDisclosure() + const { backend } = useBackendContext(); + + const [rooms, setRooms] = useState([]); + const [selectedDays, setSelectedDays] = useState([]); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + + //bookings part + //events part + + const [bookingsForm, setBookingsForm] = useState({ + event_id : "", + room_id : "", + start_time : "", + end_time : "", + date : "", + archived : false + }) + + const [eventsForm, setEventsForm] = useState({ + 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 daysIndices = selectedDays.map((day) => daysMap[day]); + + const dates = []; + const currentDate = new Date(startDate); + const lastDate = new Date(endDate); + + while (currentDate <= lastDate) { + if (daysIndices.includes(currentDate.getUTCDay())) { + dates.push(new Date(currentDate).toISOString().split("T")[0]); + } + currentDate.setDate(currentDate.getDate() + 1); + } + return dates; + }; + + const handleSubmit = async () => { + try { + + const eventResponse = await backend.post("/events", eventsForm); + + const eventId = eventResponse.data.id; + + const dates = getDatesForDays(startDate, endDate, selectedDays); + + for (const date of dates) { + const bookingData = { + ...bookingsForm, + event_id : eventId, + date: date, + }; + + const bookingsResponse = await backend.post("/bookings", bookingData); + console.log(bookingsResponse.data); + } + + + } 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]); + } + }} + /> + ))} + + + + + + Start Date + setStartDate(e.target.value)}/> + End Date + setEndDate(e.target.value)}/> + Start Time + setBookingsForm({...bookingsForm, start_time : e.target.value})}/> + End Date + setBookingsForm({...bookingsForm, end_time : e.target.value})}/> + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/client/src/components/EventsTable.jsx b/client/src/components/EventsTable.jsx new file mode 100644 index 0000000..909270b --- /dev/null +++ b/client/src/components/EventsTable.jsx @@ -0,0 +1,110 @@ +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Menu, + MenuButton, + MenuList, + MenuItem, + Button, +} from '@chakra-ui/react'; +import React, { useState, useEffect } from 'react'; +import { useBackendContext } from "../contexts/hooks/useBackendContext"; + +export const EventsTable = () => { + const { backend } = useBackendContext(); + const [ events, setEvents ] = useState([]); + + const fetchEvents = async () => { + try { + const eventsResponse = await backend.get('/events'); + const eventsData = eventsResponse.data; + const eventsWithAssignments = []; + + for (const event of eventsData) { + const instructors = []; + const payees = []; + + try { + // Attempt to fetch assignment information for each event + const assignmentsResponse = await backend.get('/assignments/event/' + event.id); + const assignmentsData = assignmentsResponse.data; + + // Process assignment information if available + if (Array.isArray(assignmentsData) && assignmentsData.length > 0) { + for (const assignment of assignmentsData) { + if (assignment.role.toLowerCase() === 'instructor') { + instructors.push(assignment.clientName); + } else if (assignment.role.toLowerCase() === 'payee') { + payees.push(assignment.clientName); + } + } + } + } catch { + console.error('Failed to fetch assignments for event', event.id); + } + + // Add event with assignments to list + eventsWithAssignments.push({ + id: event.id, + name: event.name, + description: event.description, + instructors: instructors.join(', '), + payees: payees.join(', ') + }); + } + + setEvents(eventsWithAssignments); + } catch { + console.error('Failed to fetch events'); + } + } + + useEffect(() => { + // call the fetch event here to fetch all events + fetchEvents(); + }, []); + + return ( + + + + + + + + + + + + {events.map((event) => ( + + + + + + + + ))} + +
ClassDescriptionInstructorPayeeActions
{event.name}{event.description}{event.instructors}{event.payees} + + + Actions + + + {}}>Edit + {}}>Delete + + +
+ ); + +}; \ No newline at end of file diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx new file mode 100644 index 0000000..f61b52d --- /dev/null +++ b/client/src/components/Navbar.jsx @@ -0,0 +1,72 @@ +import { Box, Flex, Link, Text, VStack } from "@chakra-ui/react"; + +export const Navbar = () => { + const menuItems = [ + { name: "Home", path: "/home" }, + { name: "Events", path: "/events" }, + { name: "Invoices", path: "/invoices" }, + { name: "Settings", path: "/settings" }, + { + name: "Google Calendar", + path: "/calendar", + external: true, + }, + ]; + + return ( + + {/* Logo Section */} + + + + LPA + + + + + {/* Navigation Links */} + + {menuItems.map((item) => ( + + {item.name} + + ))} + + + ); +}; + +export default Navbar; diff --git a/client/src/components/PDFButton.jsx b/client/src/components/PDFButton.jsx new file mode 100644 index 0000000..9c791c1 --- /dev/null +++ b/client/src/components/PDFButton.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { useState, useEffect } from 'react'; +import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer'; +import { PDFDownloadLink } from '@react-pdf/renderer'; +import { useBackendContext } from '../contexts/hooks/useBackendContext'; + + +const styles = StyleSheet.create({ + page: { + padding: 30, + backgroundColor: '#ffffff' + }, + section: { + margin: 10, + padding: 20, + borderRadius: 5, + backgroundColor: '#f8f9fa', + borderBottom: '1px solid #eee' + }, + header: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 10, + color: '#2c3e50' + }, + row: { + flexDirection: 'row', + marginBottom: 8, + alignItems: 'center' + }, + label: { + width: 100, + fontSize: 12, + color: '#666', + marginRight: 10 + }, + value: { + flex: 1, + fontSize: 12, + color: '#333' + }, + dateTime: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10 + }, + timeBlock: { + flex: 1 + } +}); + +const MyDocument = ({ bookingData }) => { + return ( + + + {bookingData && bookingData.map((element) => ( + + Archived: {element.archived} + Date: {element.date} + Event ID: {element.eventId} + DB ID: {element.id} + Room ID: {element.roomId} + Start Time: {element.startTime} + End Time: {element.endTime} + + ))} + + + ); +}; + +const PDFButton = () => { + const { backend } = useBackendContext(); + const [bookingData, setBookingData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await backend.get("/bookings"); + setBookingData(Array.isArray(response.data) ? response.data : []); + } catch (err) { + console.error("Error fetching bookings:", err); + } finally { + setIsLoading(false); + } + } + fetchData(); + }, [backend]); + + if (isLoading) return
Loading...
; + + return ( +
+ } + fileName="bookingdata.pdf" + > + + +
+ ); +}; + +export default PDFButton; \ No newline at end of file diff --git a/client/src/components/login/ForgotPassword.jsx b/client/src/components/login/ForgotPassword.jsx new file mode 100644 index 0000000..fb001f5 --- /dev/null +++ b/client/src/components/login/ForgotPassword.jsx @@ -0,0 +1,36 @@ + +import { + Button, + FormControl, + Heading, + Input, + VStack, +} from "@chakra-ui/react"; + + + +export const ForgotPassword = () => { + return ( + + Reset Password +

Email

+ + + + + + + +
+ + ); +}; + + + diff --git a/client/src/components/login/Login.jsx b/client/src/components/login/Login.jsx index b1844b1..31fa677 100644 --- a/client/src/components/login/Login.jsx +++ b/client/src/components/login/Login.jsx @@ -178,6 +178,15 @@ export const Login = () => { > Login with Google + + + + Forgot Password? + + ); }; diff --git a/client/src/components/playground/Playground.jsx b/client/src/components/playground/Playground.jsx index b9c39ce..11e102c 100644 --- a/client/src/components/playground/Playground.jsx +++ b/client/src/components/playground/Playground.jsx @@ -2,6 +2,9 @@ import { Heading, VStack, } from "@chakra-ui/react"; +import { EventsTable } from "../EventsTable"; +import React from 'react'; +import PDFButton from "../PDFButton"; export const Playground = () => { return ( @@ -10,6 +13,8 @@ export const Playground = () => { sx={{ width: 300, marginX: "auto" }} > Playground + + ); }; diff --git a/package.json b/package.json index b262c07..6e893a3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "prepare": "husky install", "lint": "eslint", "format": "npx prettier --write .", - "postinstall": "cd client && npx patch-package" + "postinstall": "cd client && patch-package" }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": "yarn run eslint" diff --git a/server/routes/assignments.js b/server/routes/assignments.js new file mode 100644 index 0000000..d1ec3a3 --- /dev/null +++ b/server/routes/assignments.js @@ -0,0 +1,114 @@ +import express from "express"; +import { Router } from "express"; +import { keysToCamel } from "../common/utils"; +import { db } from "../db/db-pgp"; + +export const assignmentsRouter = Router(); +assignmentsRouter.use(express.json()); + +// Get all assignments +assignmentsRouter.get("/", async (req, res) => { + try { + const users = await db.query(`SELECT * FROM assignments`); + + res.status(200).json(keysToCamel(users)); + } catch (err) { + res.status(400).send(err.message); + } +}); + +// Get an assignment by ID +assignmentsRouter.get("/:id", async (req, res) => { + try { + const { id } = req.params; + + const user = await db.query("SELECT * FROM assignments WHERE id = $1", [ + id, + ]); + + res.status(200).json(keysToCamel(user)); + } catch (err) { + res.status(400).send(err.message); + } +}); + +// Get assignments for a specific event +assignmentsRouter.get("/event/:event_id", async (req, res) => { + try { + const { event_id } = req.params; + + const assignments = await db.query( + `SELECT a.id, a.event_id, a.client_id, a.role, c.name as client_name, e.name as event_name + FROM assignments a, clients c, events e + WHERE a.client_id = c.id AND a.event_id = e.id AND a.event_id = $1 + ORDER BY a.id ASC`, + [event_id] + ); + + res.status(200).json(keysToCamel(assignments)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +assignmentsRouter.post("/", async (req, res) => { + try { + const { eventId, clientId, role } = req.body; + + console.log('Values:', { eventId, clientId, role }); // Add this line + const query = { + text: 'INSERT INTO assignments (event_id, client_id, role) VALUES ($1, $2, $3) RETURNING *', + values: [eventId, clientId, role], + }; + + const result = await db.query(query); + console.log('Query result:', result); // Add this line + res.status(201).json({id: result[0].id }); + } catch (err) { + res.status(500).send(err.message); + } + }); + +assignmentsRouter.get("/client/:client_id", async (req, res) => { + try { + const { client_id } = req.params; + const data = await db.query(`SELECT * FROM assignments WHERE client_id = $1`, [client_id]); + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}) + +assignmentsRouter.get("/search", async (req, res) => { + const {event, client} = req.query; + try { + const data = await db.query(`SELECT * FROM assignments WHERE event_id = $1 AND client_id = $2`, [event, client]); + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}) + +assignmentsRouter.put("/:id", async (req, res) => { + try { + const { id } = req.params; + const { eventId, clientId, role } = req.body; + const data = await db.query(`UPDATE assignments SET event_id = $1, client_id = $2, role = $3 WHERE id = $4 RETURNING *`, [eventId, clientId, role, id]); + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}) + +assignmentsRouter.delete("/:id", async (req, res) => { + try { + const {id} = req.params; + const data = await db.query("DELETE FROM assignments WHERE id = $1", [id]); + res.status(200).json({result: "success"}); + } catch (err) { + res.status(500).json({result: "error"}); + } +}); + + +export { assignmentsRouter }; diff --git a/server/routes/bookings.js b/server/routes/bookings.js index ead22bb..5b1e92a 100644 --- a/server/routes/bookings.js +++ b/server/routes/bookings.js @@ -87,7 +87,7 @@ bookingsRouter.post("/", async (req, res) => { // Insert new booking into database const data = await db.query( - `INSERT INTO bookings (event_id, room_id, start_time, end_time, date, archived) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, + `INSERT INTO bookings (event_id, room_id, start_time, end_time, date, archived) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, [ event_id, room_id, start_time, end_time, date, archived ] ); diff --git a/server/routes/clients.js b/server/routes/clients.js index d114f68..7592118 100644 --- a/server/routes/clients.js +++ b/server/routes/clients.js @@ -4,13 +4,90 @@ import { db } from "../db/db-pgp"; // TODO: replace this db with export const clientsRouter = Router(); -// Get all comments +// Get all clients clientsRouter.get("/", async (req, res) => { try { const users = await db.query(`SELECT * FROM clients ORDER BY id ASC`); + res.status(200).json(keysToCamel(users)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// Get client by id +clientsRouter.get("/:id", async (req, res) => { + try { + const { id } = req.params; + const user = await db.query(`SELECT * FROM clients WHERE id = $1`, [id]); + + // Case when client not found + if (user.length === 0) { + return res.status(404).json(keysToCamel("Client not found")); + } + + res.status(200).json(keysToCamel(user[0])); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// Put client by id +clientsRouter.put("/:id", async (req, res) => { + try { + const { id } = req.params; + const { name, email } = req.body; + + // Update just the name or just the email, without having to provide both fields + const updatedClient = await db.query( + ` + UPDATE clients + SET + name = COALESCE($1, name), + email = COALESCE($2, email) + WHERE id = $3 + RETURNING * + `, + [name, email, id]); + + if (updatedClient.length === 0) { + return res.status(404).json(keysToCamel("Client not found")); + } + + res.status(200).json(keysToCamel(updatedClient)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// Delete client by id +clientsRouter.delete("/:id", async (req, res) => { + try { + const { id } = req.params; + const deletedClient = await db.query('DELETE FROM clients WHERE id = $1 RETURNING *', [id]); + + if (deletedClient.length === 0) { + return res.status(404).json(keysToCamel({result: "error", message: "Client not found"})); + } + + res.status(200).json(keysToCamel({result: "success", deletedClient: deletedClient[0]})); + + } catch (err) { + res.status(500).send(err.message); + } +}); + +// Create new client +clientsRouter.post("/", async (req, res) => { + try { + const { name, email } = req.body; + const users = await db.query( + `INSERT INTO clients (name, email) VALUES ($1, $2) RETURNING id`, + [name, email] + ); res.status(200).json(keysToCamel(users)); } catch (err) { res.status(400).send(err.message); } }); + diff --git a/server/routes/invoices.js b/server/routes/invoices.js index d4e49cb..ce232d4 100644 --- a/server/routes/invoices.js +++ b/server/routes/invoices.js @@ -1,8 +1,10 @@ +import express from "express"; +import { db } from "../db/db-pgp"; import { Router } from "express"; import { keysToCamel } from "../common/utils"; -import { db } from "../db/db-pgp"; // TODO: replace this db with -const invoicesRouter = Router(); +const invoicesRouter = express.Router(); +invoicesRouter.use(express.json()); // Get all invoices invoicesRouter.get("/", async (req, res) => { @@ -17,18 +19,69 @@ invoicesRouter.get("/", async (req, res) => { // Get invoice by id invoicesRouter.get("/:id", async (req, res) => { + try { + const { id } = req.params; + + const invoice = await db.query("SELECT * FROM invoices WHERE id = $1", [ + id, + ]); + + if(invoice.length === 0){ + return res.status(404).json({ error: "Invoice does not exist." }); + } + + res.status(200).json(keysToCamel(invoice)); + catch (err) { + res.status(500).send(err.message); + } +}); + +invoicesRouter.delete("/:id", async (req, res) => { + try { + const { id } = req.params; + + // Delete booking from database + const data = db.query("DELETE FROM invoices WHERE id = $1 RETURNING *", + [ id ]); + + if (data.length === 0) { + return res.status(404).json({result: 'error'}); + } + + res.status(200).json({result: 'success'}); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// GET /invoices/event/:event_id?date=[val] +invoicesRouter.get("/event/:event_id", async (req, res) => { try { - const { id } = req.params; + const { event_id } = req.params; + const { date } = req.query; - const invoice = await db.query("SELECT * FROM invoices WHERE id = $1", [ - id, - ]); + let query = "SELECT * FROM invoices WHERE event_id = $1"; + const params = [event_id]; + + if (date) { + query += " AND start_date >= $2 AND end_date <= $3"; // Changed from date to start_date + const parsedDate = new Date(date); - if(invoice.length === 0){ - return res.status(404).json({ error: "Invoice does not exist." }); + if (isNaN(parsedDate.getTime())) { + res.status(400).send("Invalid date format. Please use ISO 8601 (YYYY-MM-DD) format."); + return; + } + + const startOfMonth = new Date(parsedDate.getFullYear(), parsedDate.getMonth(), 1); + const endOfMonth = new Date(parsedDate.getFullYear(), parsedDate.getMonth() + 1, 0); + + params.push(startOfMonth.toISOString().split("T")[0], endOfMonth.toISOString().split("T")[0]); } - res.status(200).json(keysToCamel(invoice)); + const invoices = await db.any(query, params); + + console.log(invoices); + res.status(200).json(keysToCamel(invoices)); } catch (err) { res.status(500).send(err.message); } @@ -67,4 +120,73 @@ invoicesRouter.get("/event/:event_id", async (req, res) => { } }); +// POST /invoices +invoicesRouter.post("/", async (req, res) => { + try { + const invoiceData = req.body; + + if (!invoiceData) { + return res.status(400).json({ error: "Invoice data is required" }); + } + + const result = await db.one( + `INSERT INTO invoices + (event_id, start_date, end_date, is_sent, payment_status) + VALUES ($1, $2, $3, $4, $5) + RETURNING id`, + [ + invoiceData.eventId, + invoiceData.startDate, + invoiceData.endDate, + invoiceData.isSent ?? false, + invoiceData.paymentStatus ?? 'none' + ] + ); + + res.status(201).json(result.id); + } catch (err) { + res.status(500).send(err.message); + } + }); + +// PUT /invoices/:id +invoicesRouter.put("/:id", async (req, res) => { + try { + const { id } = req.params; + const invoiceData = req.body; + + if (!invoiceData) { + return res.status(400).json({ error: "Invoice data is required" }); + } + + const result = await db.oneOrNone( + `UPDATE invoices + SET + event_id = COALESCE($1, event_id), + start_date = COALESCE($2, start_date), + end_date = COALESCE($3, end_date), + is_sent = COALESCE($4, is_sent), + payment_status = COALESCE($5, payment_status) + WHERE id = $6 + RETURNING *`, + [ + invoiceData.eventId, + invoiceData.startDate, + invoiceData.endDate, + invoiceData.isSent, + invoiceData.paymentStatus, + id + ] + ); + + if (!result) { + return res.status(404).json({ error: "Invoice not found" }); + } + + res.status(200).json(keysToCamel(result)); + } catch (err) { + res.status(500).send(err.message); + } + }); + export { invoicesRouter }; diff --git a/server/routes/rooms.js b/server/routes/rooms.js new file mode 100644 index 0000000..699ec49 --- /dev/null +++ b/server/routes/rooms.js @@ -0,0 +1,102 @@ +import express from "express"; +import { db } from "../db/db-pgp"; +import { keysToCamel } from "../common/utils"; + +const roomsRouter = express.Router(); +roomsRouter.use(express.json()); + +roomsRouter.get("/", async (req, res) => { + try { + const data = await db.query(`SELECT * FROM rooms`); + res.status(200).json(keysToCamel(data)); +} catch (err) { + res.status(500).send(err.message); +} +}); + +// Get room by ID +roomsRouter.get("/:id", async (req, res) => { + try { + const { id } = req.params; + const data = await db.query(`SELECT * FROM rooms WHERE id = $1`, [id]); + + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// Add new room, returning ID of new room +roomsRouter.post("/", async (req, res) => { + try { + const { name, description, rate } = req.body; + const data = await db.query( + `INSERT INTO rooms (name, description, rate) VALUES ($1, $2, $3) RETURNING id`, + [name, description || null, rate] + ); + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// Update room data +roomsRouter.put("/:id", async (req, res) => { + try { + const { id } = req.params; + const { name, description, rate} = req.body; + + const fields = []; + if (name) fields.push(`name = $(name)`); + if (description) fields.push(`description = $(description)`); + if (rate) fields.push(`rate = $(rate)`); + + // Join the fields to form the SET clause + const setClause = fields.join(", "); + + // If no fields are set, return a 400 error + if (!setClause) { + return res.status(400).send("No fields provided to update"); + } + + const data = await db.query( + `UPDATE rooms + SET ` + + setClause + + ` + WHERE id = $(id) + RETURNING *`, + {id, name, description, rate} + ); + + // Verify the Room is valid + if (data.length === 0) { + return res.status(404).send("Room not found"); + } + + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// Delete room with ID +roomsRouter.delete("/:id", async (req, res) => { + try { + const { id } = req.params; + + const data = await db.query( + `DELETE FROM rooms WHERE id = $1 RETURNING *`, + [id] + ); + + if (data.length === 0) { + return res.status(404).json({ result: "error" }); + } + res.status(200).json({ result: "success" }); + } catch (err) { + res.status(500).json({ result: "error" }); + } +}); + +export { roomsRouter }; diff --git a/server/src/app.js b/server/src/app.js index 3fc8e24..8ac3c94 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -6,10 +6,14 @@ import schedule from "node-schedule"; // TODO: Keep only if scheduling cronjobs import { usersRouter } from "../routes/users"; import { commentsRouter } from '../routes/comments'; +import { roomsRouter } from '../routes/rooms'; import { eventsRouter } from "../routes/events"; import { bookingsRouter } from "../routes/bookings"; +import { assignmentsRouter } from "../routes/assignments"; import { verifyToken } from "./middleware"; import { clientsRouter } from "../routes/clients"; +import { roomsRouter } from "../routes/rooms"; +import { assignmentsRouter } from "../routes/assignments"; import { invoicesRouter } from "../routes/invoices"; dotenv.config(); @@ -42,12 +46,15 @@ if (process.env.NODE_ENV === "production") { app.use("/users", usersRouter); app.use('/comments', commentsRouter); +app.use("/rooms", roomsRouter); app.use("/bookings", bookingsRouter); app.use("/events", eventsRouter); app.use("/bookings", bookingsRouter); app.use("/comments", commentsRouter); app.use("/clients", clientsRouter); app.use("/invoices", invoicesRouter); +app.use("/rooms", roomsRouter) +app.use("/assignments", assignmentsRouter) app.listen(SERVER_PORT, () => { console.info(`Server listening on ${SERVER_PORT}`);