diff --git a/client/src/App.jsx b/client/src/App.jsx index 6c9701b..53909f5 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -25,6 +25,7 @@ import { AuthProvider } from "./contexts/AuthContext"; import { BackendProvider } from "./contexts/BackendContext"; import { RoleProvider } from "./contexts/RoleContext"; import { Home } from "./components/home/Home"; +import { Settings } from "./components/settings/Settings"; const App = () => { return ( @@ -96,6 +97,10 @@ const App = () => { path="/programs/:id" element={} />} /> + } />} + /> } />} diff --git a/client/src/components/settings/Settings.jsx b/client/src/components/settings/Settings.jsx new file mode 100644 index 0000000..af1bb7e --- /dev/null +++ b/client/src/components/settings/Settings.jsx @@ -0,0 +1,246 @@ +import { useEffect, useState } from "react"; + +import { + Button, + Heading, + Input, + Popover, + PopoverBody, + PopoverContent, + PopoverFooter, + PopoverHeader, + PopoverTrigger, + Stack, + Table, + TableContainer, + Tbody, + Td, + Text, + Th, + Thead, + Tr, +} from "@chakra-ui/react"; + +import { useBackendContext } from "../../contexts/hooks/useBackendContext"; +import Navbar from "../navbar/Navbar"; + +export const Settings = () => { + const [users, setUsers] = useState([]); + const [rooms, setRooms] = useState([]); + const [selectedRoom, setSelectedRoom] = useState(null); + const [newRate, setNewRate] = useState(""); + const { backend } = useBackendContext(); + + useEffect(() => { + const fetchUserData = async () => { + try { + const response = await backend.get("/users"); + const pendingUsers = response.data.filter( + (user) => user.editPerms === false + ); + setUsers(pendingUsers); + console.log(pendingUsers); + } catch (error) { + console.log("Error fetching users:", error); + } + }; + + const fetchRoomsData = async () => { + try { + const response = await backend.get("/rooms"); + setRooms(response.data); + console.log(response.data); + } catch (error) { + console.log("Error fetching rooms: ", error); + } + }; + fetchUserData(); + fetchRoomsData(); + }, [backend]); + + // Approve users function: set edit_perms = True and update the rendering of the table + const handleApprove = async (user) => { + try { + await backend.put(`/users/${user.id}`, { + email: user.email, + firstName: user.first_name, + lastName: user.last_name, + editPerms: true, + }); + // Remove approved user from the list + setUsers(users.filter((u) => u.id !== user.id)); + } catch (error) { + console.log("Error approving user:", error); + } + }; + + // Remove a user and delete from DB and Firebase + const handleRemove = async (user) => { + try { + // await backend.delete(`/users/${user.id}`); + await backend.delete(`/users/${user.firebaseUid}`); + setUsers(users.filter((u) => u.id !== user.id)); + } catch (error) { + console.log("Error removing user:", error); + } + }; + + // Open the edit panel for a room + const handleEditRoom = (room) => { + setSelectedRoom(room); + setNewRate(room.rate); + }; + + // Save the updated room rate + const handleSaveRate = async () => { + try { + await backend.put(`/rooms/${selectedRoom.id}`, { + ...selectedRoom, + rate: newRate, + }); + setRooms( + rooms.map((room) => + room.id === selectedRoom.id ? { ...room, rate: newRate } : room + ) + ); + setSelectedRoom(null); + setNewRate(""); + } catch (error) { + console.log("Error updating room rate:", error); + } + }; + + // Cancel editing + const handleCancelEdit = () => { + setSelectedRoom(null); + setNewRate(""); + }; + + return ( + + + Approve Users + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
NameCreatedApproveDenyMake Admin
{`${user.firstName} ${user.lastName}`}January 20, 2025 + + + + + +
+
+
+ + + Edit Room Rate + + + + + + + + + + + {rooms.map((room) => ( + + + + + + ))} + +
RoomRateEdit
{room.name}{room.rate} + + {({ onClose }) => ( + <> + + + + + Edit Room Rate + + + Room: + {room.name} + setNewRate(e.target.value)} + /> + + + + + + + + + )} + +
+
+
+
+ ); +}; diff --git a/server/routes/invoices.js b/server/routes/invoices.js index 5a43d45..da66d0f 100644 --- a/server/routes/invoices.js +++ b/server/routes/invoices.js @@ -1,6 +1,7 @@ import express, { Router } from "express"; -import { db } from "../db/db-pgp"; + import { keysToCamel } from "../common/utils"; +import { db } from "../db/db-pgp"; const invoicesRouter = Router(); invoicesRouter.use(express.json()); @@ -9,7 +10,6 @@ invoicesRouter.use(express.json()); invoicesRouter.get("/", async (req, res) => { try { const { startDate, endDate } = req.query; - console.log("Query params:", startDate, endDate); if (startDate && endDate) { const invoices = await db.any( @@ -104,7 +104,7 @@ invoicesRouter.get("/:id", async (req, res) => { id, ]); - if(invoice.length === 0){ + if (invoice.length === 0) { return res.status(404).json({ error: "Invoice does not exist." }); } @@ -119,14 +119,15 @@ invoicesRouter.delete("/:id", async (req, res) => { const { id } = req.params; // Delete booking from database - const data = await db.query("DELETE FROM invoices WHERE id = $1 RETURNING *", - [ id ]); + const data = db.query("DELETE FROM invoices WHERE id = $1 RETURNING *", [ + id, + ]); if (data.length === 0) { - return res.status(404).json({result: 'error'}); + return res.status(404).json({ result: "error" }); } - res.status(200).json({result: 'success'}); + res.status(200).json({ result: "success" }); } catch (err) { res.status(500).send(err.message); } @@ -134,35 +135,50 @@ invoicesRouter.delete("/:id", async (req, res) => { // GET /invoices/event/:event_id?date=[val] invoicesRouter.get("/event/:event_id", async (req, res) => { - try { - const { event_id } = req.params; - const { date } = req.query; - - 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 (isNaN(parsedDate.getTime())) { - res.status(400).send("Invalid date format. Please use ISO 8601 (YYYY-MM-DD) format."); - return; - } + try { + const { event_id } = req.params; + const { date } = req.query; - const startOfMonth = new Date(parsedDate.getFullYear(), parsedDate.getMonth(), 1); - const endOfMonth = new Date(parsedDate.getFullYear(), parsedDate.getMonth() + 1, 0); + let query = "SELECT * FROM invoices WHERE event_id = $1"; + const params = [event_id]; - params.push(startOfMonth.toISOString().split("T")[0], endOfMonth.toISOString().split("T")[0]); + if (date) { + query += " AND start_date >= $2 AND end_date <= $3"; // Changed from date to start_date + const parsedDate = new Date(date); + + if (isNaN(parsedDate.getTime())) { + res + .status(400) + .send( + "Invalid date format. Please use ISO 8601 (YYYY-MM-DD) format." + ); + return; } - const invoices = await db.any(query, params); + const startOfMonth = new Date( + parsedDate.getFullYear(), + parsedDate.getMonth(), + 1 + ); + const endOfMonth = new Date( + parsedDate.getFullYear(), + parsedDate.getMonth() + 1, + 0 + ); - res.status(200).json(keysToCamel(invoices)); - } catch (err) { - res.status(500).send(err.message); + params.push( + startOfMonth.toISOString().split("T")[0], + endOfMonth.toISOString().split("T")[0] + ); } - }); + + const invoices = await db.any(query, params); + + res.status(200).json(keysToCamel(invoices)); + } catch (err) { + res.status(500).send(err.message); + } +}); // Get invoice for an event by the event id optionally between a start and end date invoicesRouter.get("/event/:event_id", async (req, res) => { @@ -192,7 +208,7 @@ invoicesRouter.get("/event/:event_id", async (req, res) => { const data = await db.query(query, params); res.status(200).json(keysToCamel(data)); -} catch (err) { + } catch (err) { res.status(500).send(err.message); } }); @@ -208,7 +224,8 @@ invoicesRouter.get("/payees/:id", async (req, res) => { JOIN assignments ON assignments.client_id = clients.id JOIN invoices ON assignments.event_id = invoices.event_id WHERE invoices.id = $1 AND assignments.role = 'payee';`, - [ id ]); + [id] + ); res.status(200).json(keysToCamel(data)); } catch (err) { @@ -226,10 +243,11 @@ invoicesRouter.get("/invoiceEvent/:id", async (req, res) => { FROM events JOIN invoices ON events.id = invoices.event_id WHERE invoices.id = $1;`, - [ id ]); + [id] + ); if (event.length === 0) { - return res.status(404).json({result: 'error'}); + return res.status(404).json({ result: "error" }); } res.status(200).json(keysToCamel(event[0])); @@ -238,48 +256,47 @@ invoicesRouter.get("/invoiceEvent/:id", async (req, res) => { } }); - // POST /invoices invoicesRouter.post("/", async (req, res) => { - try { - const invoiceData = req.body; + try { + const invoiceData = req.body; - if (!invoiceData) { - return res.status(400).json({ error: "Invoice data is required" }); - } + if (!invoiceData) { + return res.status(400).json({ error: "Invoice data is required" }); + } - const result = await db.one( - `INSERT INTO invoices + 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' - ] - ); + [ + 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); - } - }); + 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; + try { + const { id } = req.params; + const invoiceData = req.body; - if (!invoiceData) { - return res.status(400).json({ error: "Invoice data is required" }); - } + if (!invoiceData) { + return res.status(400).json({ error: "Invoice data is required" }); + } - const result = await db.oneOrNone( - `UPDATE invoices + const result = await db.oneOrNone( + `UPDATE invoices SET event_id = COALESCE($1, event_id), start_date = COALESCE($2, start_date), @@ -288,33 +305,18 @@ invoicesRouter.put("/:id", async (req, res) => { 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); - } - }); - -invoicesRouter.get("/total/:id", async (req, res) => { - // DUMMY ENDPONT for testing - try { - const { id } = req.params; + [ + invoiceData.eventId, + invoiceData.startDate, + invoiceData.endDate, + invoiceData.isSent, + invoiceData.paymentStatus, + id, + ] + ); - const result = { - total: 100 + if (!result) { + return res.status(404).json({ error: "Invoice not found" }); } res.status(200).json(keysToCamel(result)); @@ -331,9 +333,7 @@ invoicesRouter.get("/paid/:id", async (req, res) => { `SELECT SUM(c.adjustment_value) FROM invoices as i, comments as c WHERE i.id = $1 AND c.adjustment_type = 'paid';`, - [ - id - ] + [id] ); if (!result) { @@ -341,8 +341,8 @@ invoicesRouter.get("/paid/:id", async (req, res) => { } result = { - total: result.sum - } + total: result.sum, + }; res.status(200).json(keysToCamel(result)); } catch (err) { @@ -350,5 +350,96 @@ invoicesRouter.get("/paid/:id", async (req, res) => { } }); +invoicesRouter.get("/total/:id", async (req, res) => { + try { + const { id } = req.params; //invoice id + + const invoiceRes = await db.query("SELECT * FROM invoices WHERE id = $1", [ + id, + ]); + const invoice = invoiceRes[0]; + + // Use the event_id from the invoice record. + const eventRes = await db.query("SELECT * FROM events WHERE id = $1", [ + invoice.event_id, + ]); + const event = eventRes[0]; + + const comments = await db.query( + "SELECT * FROM comments WHERE adjustment_type IN ('rate_flat', 'rate_percent') AND booking_id IS NULL" + ); + + const bookings = await db.query( + "SELECT * FROM bookings WHERE event_id = $1 AND date BETWEEN $2 AND $3", + [event.id, invoice.start_date, invoice.end_date] + ); + + const bookingCosts = await Promise.all( + bookings.map(async (booking) => { + const roomRateBooking = await db.query( + "SELECT rooms.name, rooms.rate FROM rooms JOIN bookings ON rooms.id = bookings.room_id WHERE bookings.id = $1", + [booking.id] + ); + + if (!roomRateBooking.length) return 0; // if room not found, cost is 0 + + let totalRate = Number(roomRateBooking[0].rate); + + comments.forEach((adj) => { + if (adj.adjustment_type === "rate_percent") { + totalRate *= 1 + Number(adj.adjustment_value) / 100; + } else if (adj.adjustment_type === "rate_flat") { + totalRate += Number(adj.adjustment_value); + } + }); + + const commentsBooking = await db.query( + "SELECT * FROM comments WHERE adjustment_type IN ('rate_flat', 'rate_percent') AND booking_id = $1", + [booking.id] + ); + + commentsBooking.forEach((adj) => { + if (adj.adjustment_type === "rate_percent") { + totalRate *= 1 + Number(adj.adjustment_value) / 100; + } else if (adj.adjustment_type === "rate_flat") { + totalRate += Number(adj.adjustment_value); + } + }); + + // Calculate booking duration in hours. + const startTime = new Date( + `1970-01-01T${booking.start_time.substring(0, booking.start_time.length - 3)}Z` + ); + const endTime = new Date( + `1970-01-01T${booking.end_time.substring(0, booking.start_time.length - 3)}Z` + ); + const durationHours = (endTime - startTime) / (1000 * 60 * 60); + + // Calculate booking cost. + const bookingCost = totalRate * durationHours; + + return bookingCost; + }) + ); + + let totalCost = bookingCosts.reduce((acc, cost) => acc + cost, 0); + + const totalComments = await db.query( + "SELECT * FROM comments WHERE adjustment_type = 'total'" + ); + + totalComments.map((comment) => { + totalCost += Number(comment.adjustment_value); + }); + + const result = { + total: totalCost, + }; + + res.status(200).json(keysToCamel(result)); + } catch (err) { + res.status(500).send(err.message); + } +}); export { invoicesRouter };