diff --git a/client/src/App.jsx b/client/src/App.jsx index 7e41e66..b5db465 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -10,7 +10,8 @@ import { import { Admin } from "./components/admin/Admin"; import { CatchAll } from "./components/CatchAll"; import { Dashboard } from "./components/dashboard/Dashboard"; -import { Invoice } from "./components/invoices/Invoice"; +import { SingleInvoice } from "./components/invoices/SingleInvoice"; +import { EditInvoice } from "./components/invoices/EditInvoice"; import { InvoicesDashboard } from "./components/invoices/InvoicesDashboard"; import { ForgotPassword } from "./components/forgotpassword/ForgotPassword"; import { ForgotPasswordSent } from "./components/forgotpassword/ForgotPasswordSent"; @@ -107,7 +108,11 @@ const App = () => { /> } />} + element={} />} + /> + } />} /> { + const { id } = useParams() + const { backend } = useBackendContext(); + const navigate = useNavigate(); + + const [invoice, setInvoice] = useState([]); + const [comments, setComments] = useState([]); + const [instructors, setInstructors] = useState([]); + const [programName, setProgramName] = useState("") + const [payees, setPayees] = useState([]) + const [subtotal, setSubtotal] = useState(0) + const [pastDue, setPastDue] = useState(0) + + useEffect(() => { + const fetchData = async () => { + try { + // get current invoice + const currentInvoiceResponse = await backend.get("/invoices/" + id); + setInvoice(currentInvoiceResponse); + } catch (error) { + // Invoice/field does not exist + console.error("Error fetching data:", error); + } + }; + fetchData(); + }, [backend, id]); + + useEffect(() => { + const fetchData = async () => { + try { + // If no invoice is found, set everything to null + if (!invoice.data || invoice.status === 404) { + setComments([]); + setInstructors([]) + setProgramName("") + setPayees(null); + setPastDue(0); + return; + } + + // get instructors + const instructorResponse = await backend.get('/assignments/instructors/' + invoice.data[0].eventId); + setInstructors(instructorResponse.data); + + // get comments + const commentsResponse = await backend.get('/comments/details/' + id); + setComments(commentsResponse.data); + + // get program name + const programNameResponse = await backend.get('/events/' + invoice.data[0].eventId); + setProgramName(programNameResponse.data[0].name); + + // get payees + const payeesResponse = await backend.get("/invoices/payees/" + id); + setPayees(payeesResponse.data) + + // get subtotal + const subtotalResponse = await backend.get("/invoices/total/" + id); + setSubtotal(subtotalResponse.data.total) + + + // get the unpaid/remaining invoices + const unpaidInvoicesResponse = await backend.get("/events/remaining/" + invoice.data[0]["eventId"]); + + // calculate sum of unpaid/remaining invoices + const unpaidTotals = await Promise.all( + unpaidInvoicesResponse.data.map(invoice => backend.get(`/invoices/total/${invoice.id}`)) + ); + const partiallyPaidTotals = await Promise.all( + unpaidInvoicesResponse.data.map(invoice => backend.get(`/invoices/paid/${invoice.id}`)) + ); + const unpaidTotal = unpaidTotals.reduce((sum, res) => sum + res.data.total, 0); + const unpaidPartiallyPaidTotal = partiallyPaidTotals.reduce((sum, res) => sum + res.data.paid, 0); + const remainingBalance = unpaidTotal - unpaidPartiallyPaidTotal; + setPastDue(remainingBalance); + + + + } catch (error) { + // Invoice/field does not exist + console.error("Error fetching data:", error); + } + }; + fetchData(); + }, [invoice]); + + return ( + + + + + + {/* back button */} + } + onClick={() => { + navigate(`/invoices/${id}`); + }} + variant="link" + color="#474849" + fontSize="1.5em" + /> + + {/*Edit Area*/} + + + + + + + + + + + + {/*Save Changes Button*/} + + + + + + + + + ); +} diff --git a/client/src/components/invoices/EditInvoiceComponents.jsx b/client/src/components/invoices/EditInvoiceComponents.jsx new file mode 100644 index 0000000..f2fa3b2 --- /dev/null +++ b/client/src/components/invoices/EditInvoiceComponents.jsx @@ -0,0 +1,292 @@ +import { format } from 'date-fns'; +import React, { useState } from 'react'; +import { FaAngleLeft, FaAngleRight } from "react-icons/fa6"; +import { + Button, + Box, + Flex, + Select, + Table, + Tbody, + Td, + Text, + Thead, + Th, + Tr, + Link, + VStack, + Heading, + HStack, + Image, + SimpleGrid, + Input + } from '@chakra-ui/react' + +import logo from '../../assets/logo/logo.png'; + +const EditInvoiceTitle = () => { + return ( + + La Peña Logo + + + RENTAL INVOICE + La Peña Cultural Center + 3105 Shattuck Ave., Berkeley, CA 94705 + lapena.org + + + ) +} + +const EditInvoiceDetails = ({ instructors, programName, payees }) => { + return ( + + Classroom Rental Monthly Statement + + + {/* Left column */} + + Recurring Program: + + + Designated Payers: + {payees && payees.length > 0 ? ( + payees.map((payee, index) => ( + + + )) + ) : ( + No payees found. + )} + + + + + {/* Right column */} + + + Lead Artist(s): + + + {instructors && instructors.length > 0 ? ( + instructors.map((instructor, index) => ( + + + + + + )) + ) : ( + No instructors found. + )} + + + + + + ) +} + +const StatementComments = ({ comments = [], subtotal = 0.0 }) => { + return ( + + + + + + + + + + + + + + + + {comments.length > 0 ? ( + comments.map((comment, index) => { + return ( + + + + + + + + + + ); + }) + ) : ( + + + + )} + + + + + + +
+ Date + + Classroom + + Rental Hours + + Hourly Fee + + Total + + Adjustments + + Comments +
+ + + + + { + const startTime = comment.startTime.split('-')[0].substring(0, 5); + const endTime = comment.endTime.split('-')[0].substring(0, 5); + + const formatTime = (timeStr) => { + const [hours, minutes] = timeStr.split(':').map(Number); + const period = hours >= 12 ? 'pm' : 'am'; + const hour12 = hours % 12 || 12; + return `${hour12}:${minutes.toString().padStart(2, '0')} ${period}`; + }; + + return `${formatTime(startTime)} - ${formatTime(endTime)}`; + })() + : 'N/A' + } + size="sm" + mr={2} + /> + + + + + + {comment.adjustmentType} + + {comment.comment} +
No comments available.
+ +
+
+
+ ); +}; + + +const InvoiceSummary = ({ pastDue, subtotal }) => { + return( + + + + SUMMARY: + + if you have any questions about this invoice, please contact: classes@lapena.org + + + + + Past Due Balance: + + + + + Current Statement Subtotal: + + + + + Total Amount Due: + + + + + Payments are due at the end of each month: + + You can make your payment at: lapena.org/payment + + + + + + + ) + } + +export { StatementComments, EditInvoiceTitle, EditInvoiceDetails, InvoiceSummary } diff --git a/client/src/components/invoices/EmailHistory.jsx b/client/src/components/invoices/EmailHistory.jsx new file mode 100644 index 0000000..fdeb287 --- /dev/null +++ b/client/src/components/invoices/EmailHistory.jsx @@ -0,0 +1,114 @@ +import { format } from 'date-fns'; +import React, { useState } from 'react'; +import { FaAngleLeft, FaAngleRight } from "react-icons/fa6"; +import { + Button, + Flex, + Select, + Table, + Tbody, + Td, + Text, + Thead, + Th, + Tr, + Link + } from '@chakra-ui/react' + +const EmailHistory = ({ emails }) => { + const [emailsPerPage, setEmailsPerPage] = useState(3); + const [currentPageNumber, setCurrentPageNumber] = useState(1); + + const totalPages = Math.ceil((emails ?? []).length / emailsPerPage) || 1; + const currentPageEmails = (emails ?? []).slice( + (currentPageNumber - 1) * emailsPerPage, currentPageNumber * emailsPerPage + ); + + const handleEmailsPerPageChange = (event) => { + setEmailsPerPage(Number(event.target.value)); + setCurrentPageNumber(1); + }; + + const handlePrevPage = () => { + if (currentPageNumber > 1) { + setCurrentPageNumber(currentPageNumber - 1); + } + }; + + const handleNextPage = () => { + if (currentPageNumber < totalPages) { + setCurrentPageNumber(currentPageNumber + 1); + } + }; + + return ( + + + Email History + + + + + + + + + + + + + {emails && emails.length > 0 ? ( + currentPageEmails.map((email) => ( + + + + + )) + ) : ( + + + + )} + +
Date Change Log
+ {format(new Date(email.datetime), 'M/d/yy')} + + + View PDF + + + {email.comment} +
No emails available.
+
+ + Show: + + per page + + {currentPageNumber} of {totalPages < 1 ? 1 : totalPages} + + + + +
+ ); +} + +export { EmailHistory } diff --git a/client/src/components/invoices/InvoiceComponents.jsx b/client/src/components/invoices/InvoiceComponents.jsx index 6833e21..a6f4cae 100644 --- a/client/src/components/invoices/InvoiceComponents.jsx +++ b/client/src/components/invoices/InvoiceComponents.jsx @@ -19,7 +19,7 @@ import { Text, Thead, Th, - Tr + Tr, } from '@chakra-ui/react' import filterIcon from "../../assets/filter.svg"; import { CalendarIcon } from '@chakra-ui/icons'; @@ -150,7 +150,7 @@ const InvoicePayments = ({ comments }) => { return ( - Comments + Payments { > - Date - Comment - Amount + Date + Comment + Amount {comments && comments.length > 0 ? ( currentPageComments.map((comment) => ( - + {format(new Date(comment.datetime), 'M/d/yy')} - + {comment.comment} - + {comment.adjustmentValue ? `$${Number(comment.adjustmentValue).toFixed(2)}` : "N/A"} )) @@ -194,16 +194,16 @@ const InvoicePayments = ({ comments }) => { - Show: + Show: - per page + per page - {currentPageNumber} of {totalPages < 1 ? 1 : totalPages} + {currentPageNumber} of {totalPages < 1 ? 1 : totalPages} diff --git a/client/src/components/invoices/Invoice.jsx b/client/src/components/invoices/SingleInvoice.jsx similarity index 87% rename from client/src/components/invoices/Invoice.jsx rename to client/src/components/invoices/SingleInvoice.jsx index 5a92bba..d0404e0 100644 --- a/client/src/components/invoices/Invoice.jsx +++ b/client/src/components/invoices/SingleInvoice.jsx @@ -14,11 +14,13 @@ import { } from "@chakra-ui/react"; import { InvoicePayments, InvoiceStats, InvoiceTitle } from "./InvoiceComponents"; +import { EmailHistory } from "./EmailHistory"; + import Navbar from "../navbar/Navbar"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; -export const Invoice = () => { +export const SingleInvoice = () => { const { id } = useParams() const { backend } = useBackendContext(); const navigate = useNavigate(); @@ -27,6 +29,7 @@ export const Invoice = () => { const [remainingBalance, setRemainingBalance] = useState(0); const [billingPeriod, setBillingPeriod] = useState({}); const [comments, setComments] = useState([]); + const [emails, setEmails] = useState([]); const [payees, setPayees] = useState([]); const [event, setEvent] = useState(); @@ -42,6 +45,7 @@ export const Invoice = () => { setRemainingBalance(null); setBillingPeriod(null); setComments(null); + setEmails(null); setPayees(null); setEvent(null) return; @@ -78,6 +82,11 @@ export const Invoice = () => { const commentsResponse = await backend.get('/comments/paidInvoices/' + id); setComments(commentsResponse.data); + // get emails + const emailsResponse = await backend.get('/invoices/historicInvoices/' + id); + setEmails(emailsResponse.data); + console.log(emailsResponse) + // get payees const payeesResponse = await backend.get("/invoices/payees/" + id); setPayees(payeesResponse.data) @@ -118,7 +127,7 @@ export const Invoice = () => { {/* buttons */} - @@ -139,12 +148,18 @@ export const Invoice = () => { payees={payees} billingPeriod={billingPeriod} amountDue={total} - remainingBalance={remainingBalance} + remainingBalance={!remainingBalance} > - + + + + + diff --git a/server/routes/assignments.js b/server/routes/assignments.js index ef43697..0b43013 100644 --- a/server/routes/assignments.js +++ b/server/routes/assignments.js @@ -87,6 +87,23 @@ assignmentsRouter.get("/search", async (req, res) => { } }) +assignmentsRouter.get("/instructors/:id", async (req, res) => { + const { id } = req.params; + try { + // console.log(req.params) + const data = await db.query(` + SELECT * + FROM assignments + JOIN clients ON assignments.client_id = clients.id + WHERE assignments.event_id = $1 AND assignments.role = 'instructor'`, + [id]); + + 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; diff --git a/server/routes/comments.js b/server/routes/comments.js index e6ca375..f2b102e 100644 --- a/server/routes/comments.js +++ b/server/routes/comments.js @@ -44,6 +44,26 @@ commentsRouter.get("/paidInvoices/:id", async (req, res) => { } }); +// Get all comments details +commentsRouter.get("/details/:id", async (req, res) => { + try { + const { id } = req.params; + const data = await db.query( + ` + SELECT * FROM comments + JOIN bookings ON comments.booking_id = bookings.id + JOIN rooms ON rooms.id = bookings.room_id + WHERE invoice_id = $1 + `, + [id] + ); + + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}); + commentsRouter.get("/booking/:id", async (req, res) => { try { const { id } = req.params; diff --git a/server/routes/invoices.js b/server/routes/invoices.js index da66d0f..59645e2 100644 --- a/server/routes/invoices.js +++ b/server/routes/invoices.js @@ -10,6 +10,7 @@ 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( @@ -114,6 +115,20 @@ invoicesRouter.get("/:id", async (req, res) => { } }); +// Get historic invoice by id +invoicesRouter.get("/historicInvoices/:id", async (req, res) => { + try { + const { id } = req.params; + const data = await db.query( + "SELECT * FROM historic_invoices WHERE original_invoice = $1", + [id] + ); + res.status(200).json(keysToCamel(data)); + } catch (err) { + res.status(500).send(err.message); + } +}); + invoicesRouter.delete("/:id", async (req, res) => { try { const { id } = req.params;