Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create invoice page #64

Merged
merged 16 commits into from
Feb 9, 2025
9 changes: 9 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
import { BackendProvider } from "./contexts/BackendContext";
import { RoleProvider } from "./contexts/RoleContext";
import { ForgotPassword } from "./components/login/ForgotPassword";
import { Invoice } from "./components/invoices/Invoice";
import { EditProgram } from "./components/programs/EditProgram";
import { PDFViewer } from "@react-pdf/renderer";

Check warning on line 22 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / run-checks

'PDFViewer' is defined but never used. Allowed unused vars must match /^_/u
import PDFButton from "./components/PDFButton";

Check warning on line 23 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / run-checks

'PDFButton' is defined but never used. Allowed unused vars must match /^_/u
import { Program } from "./components/programs/Program";

const App = () => {
Expand All @@ -46,6 +47,14 @@
path="/dashboard"
element={<ProtectedRoute element={<Dashboard />} />}
/>
<Route
path="/invoices/:id"
element={
<ProtectedRoute
element={<Invoice />}
/>
}
/>
<Route
path="/admin"
element={
Expand Down
154 changes: 154 additions & 0 deletions client/src/components/invoices/Invoice.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { useState, useEffect } from "react";
import { FaAngleLeft } from "react-icons/fa6";
import { FiEdit, FiExternalLink } from "react-icons/fi";
import {
useNavigate,
useParams
} from "react-router-dom";

import {
Heading,
Flex,
Button,
IconButton,
} from "@chakra-ui/react";

import { InvoicePayments, InvoiceStats, InvoiceTitle } from "./InvoiceComponents";
import Navbar from "../navbar/Navbar";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";


export const Invoice = () => {
const { id } = useParams()
const { backend } = useBackendContext();
const navigate = useNavigate();

const [total, setTotal] = useState(0);
const [remainingBalance, setRemainingBalance] = useState(0);
const [billingPeriod, setBillingPeriod] = useState({});
const [comments, setComments] = useState([]);
const [payees, setPayees] = useState([]);
const [event, setEvent] = useState();

useEffect(() => {
const fetchData = async () => {
try {
// get current invoice
const currentInvoiceResponse = await backend.get("/invoices/" + id);

// If no invoice is found, set everything to null
if (!currentInvoiceResponse.data || currentInvoiceResponse.status === 404) {
setTotal(null);
setRemainingBalance(null);
setBillingPeriod(null);
setComments(null);
setPayees(null);
setEvent(null)
return;
}

// get invoice total
const invoiceTotalResponse = await backend.get("/invoices/total/" + id);
setTotal(invoiceTotalResponse.data.total)

// get the unpaid/remaining invoices
const unpaidInvoicesResponse = await backend.get("/events/remaining/" + currentInvoiceResponse.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;
setRemainingBalance(remainingBalance);

// set billing period
setBillingPeriod(
{
"startDate": currentInvoiceResponse.data[0]["startDate"],
"endDate": currentInvoiceResponse.data[0]["endDate"]
}
)

// get comments
const commentsResponse = await backend.get('/comments/paidInvoices/' + id);
setComments(commentsResponse.data);

// get payees
const payeesResponse = await backend.get("/invoices/payees/" + id);
setPayees(payeesResponse.data)

// get corresponding event
const eventResponse = await backend.get("/invoices/invoiceEvent/" + id);
setEvent(eventResponse.data)
} catch (error) {
// Invoice/field does not exist
console.error("Error fetching data:", error);
}
};
fetchData();
}, [backend, id]);

return (
<Navbar>
<Flex direction="row" height="100vh" width="100vw">
<Flex direction="column" height="100%" width="100%" padding="2.5vw" gap="1.25vw">
<Flex width="100%">
{/* back button */}
<IconButton
icon={<FaAngleLeft />}
onClick={() => {
navigate("/invoices");
}}
variant="link"
color="#474849"
fontSize="1.5em"
>
</IconButton>
</Flex>

<Flex direction="column" height="100%" width="100%" paddingLeft="2.5vw" paddingRight="2.5vw" gap="1.25vw">
{/* title*/}
<Flex direction="row" width="100%">
<Heading color="#4E4AE7">Invoice Details</Heading>

{/* buttons */}
<Flex direction="row" marginLeft="auto" gap={5}>
<Button height="100%" borderRadius={30} backgroundColor="#4E4AE7" color="#FFF" fontSize="clamp(.75rem, 1.25rem, 1.75rem)" gap={1}>
<FiEdit></FiEdit>
Edit
</Button>

<Button height="100%" borderRadius={30} backgroundColor="#4E4AE7" color="#FFF" fontSize="clamp(.75rem, 1.25rem, 1.75rem)" gap={1}>
<FiExternalLink></FiExternalLink>
Preview
</Button>

</Flex>
</Flex>

<InvoiceTitle
title={event ? event.name : "N/A"}
></InvoiceTitle>

<InvoiceStats
payees={payees}
billingPeriod={billingPeriod}
amountDue={total}
remainingBalance={remainingBalance}
></InvoiceStats>

<InvoicePayments
comments={comments}
></InvoicePayments>

</Flex>
</Flex>
</Flex>
</Navbar>
);
}
208 changes: 208 additions & 0 deletions client/src/components/invoices/InvoiceComponents.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { format } from 'date-fns';
import React, { useState } from 'react';
import { FaAngleLeft, FaAngleRight } from "react-icons/fa6";

import {
Button,
Flex,
Card,
Box,
Text,
Select,
Table,
Thead,
Tbody,
Tr,
Th,
Td
} from '@chakra-ui/react';

export const InvoiceTitle = ({ title }) => {
return (
<Flex direction="row" width="100%" alignItems="center">
<Text fontSize="clamp(1rem, 1.5rem, 2rem)" color="#474849" fontWeight="bold" marginRight="0.5rem">
Program:
</Text>
<Text fontSize="clamp(.75rem, 1.25rem, 1.75rem)" color="#474849">
{title}
</Text>
</Flex>
);
};

export const InvoiceStats = ({ payees, billingPeriod, amountDue, remainingBalance }) => {
const formatDate = (isoDate) => {
const date = new Date(isoDate);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric"
});
}

return (
<Flex direction="row" h="auto" w="100%" gap="2rem">
{/* Billed To Section */}
<Card
flex={1}
h="7em"
width="25%"
borderRadius={15}
borderWidth="1px"
boxShadow="none"
display="flex"
p={4}
color="#D2D2D2"
gap={2}
flexDirection="column"
overflowY="auto"
justifyContent="flex-start"
>
<Text fontSize="clamp(1rem, 1.5rem, 2rem)" fontWeight="bold" color="#474849"> Billed to: </Text>
{(payees && payees.length > 0) ? (
payees.map((payee) => (
<Box key={payee.id} mb="0.5rem">
<Text fontSize="clamp(.75rem, 1.25rem, 1.75rem)" color="#474849">{payee.name}</Text>
<Text fontSize="clamp(.75rem, 1.25rem, 1.75rem)" color="#474849">{payee.email}</Text>
</Box>
))
) : (
<Text fontSize="clamp(.75rem, 1.25rem, 1.75rem)" color="#474849">N/A</Text>
)}
</Card>

{/* Invoice Details Section */}
<Card
flex={3}
h="7em"
borderRadius={15}
borderWidth="1px"
color="#D2D2D2"
boxShadow="none"
width="75%"
padding="1.5em"
display="flex"
flexDirection="column"
justifyContent="center"
>
<Flex width="100%" justifyContent="space-between">
<Box>
<Text fontWeight="bold" fontSize="clamp(1rem, 1.5rem, 2rem)" color="#474849"> Billing Period </Text>

{(billingPeriod && billingPeriod["startDate"]) ? (
<Text color="#474849" fontSize="clamp(.75rem, 1.25rem, 1.75rem)">
{formatDate(billingPeriod["startDate"])} - {formatDate(billingPeriod["endDate"])}
</Text>
) : (
<Text color="#474849" fontSize="clamp(.75rem, 1.25rem, 1.75rem)">
N/A - N/A
</Text>
)}
</Box>
<Box>
<Text fontWeight="bold" fontSize="clamp(1rem, 1.5rem, 2rem)" color="#474849"> Amount Due </Text>
<Text color="#474849" fontSize="clamp(.75rem, 1.25rem, 1.75rem)"> {amountDue ? `$${amountDue.toFixed(2)}` : "N/A"} </Text>
</Box>
<Box>
<Text fontWeight="bold" fontSize="clamp(1rem, 1.5rem, 2rem)" color="#474849"> Remaining Balance </Text>
<Text color="#474849" fontSize="clamp(.75rem, 1.25rem, 1.75rem)"> {remainingBalance !== 0 ? `$${remainingBalance.toFixed(2)}` : "N/A"} </Text>
</Box>
</Flex>
</Card>
</Flex>
);
};

export const InvoicePayments = ({ comments }) => {
const [commentsPerPage, setCommentsPerPage] = useState(3);
const [currentPageNumber, setCurrentPageNumber] = useState(1);

const totalPages = Math.ceil((comments ?? []).length / commentsPerPage) || 1;
const currentPageComments = (comments ?? []).slice(
(currentPageNumber - 1) * commentsPerPage, currentPageNumber * commentsPerPage
);

const handleCommentsPerPageChange = (event) => {
setCommentsPerPage(Number(event.target.value));
setCurrentPageNumber(1);
};

const handlePrevPage = () => {
if (currentPageNumber > 1) {
setCurrentPageNumber(currentPageNumber - 1);
}
};

const handleNextPage = () => {
if (currentPageNumber < totalPages) {
setCurrentPageNumber(currentPageNumber + 1);
}
};

return (
<Flex direction="column" w="100%">
<Text fontWeight="bold" fontSize="clamp(.75rem, 1.25rem, 1.75rem)" color="#474849">
Comments
</Text>

<Flex
borderRadius={15}
borderWidth=".07em"
borderColor="#E2E8F0"
p={3}
mb={3}
>
<Table
variant="striped"
color="#EDF2F7"
>
<Thead>
<Tr>
<Th fontSize="clamp(.5rem, 1rem, 1.5rem)"> Date </Th>
<Th fontSize="clamp(.5rem, 1rem, 1.5rem)"> Comment </Th>
<Th fontSize="clamp(.5rem, 1rem, 1.5rem)"> Amount </Th>
</Tr>
</Thead>
<Tbody color="#2D3748">
{comments && comments.length > 0 ? (
currentPageComments.map((comment) => (
<Tr key={comment.id}>
<Td fontSize="clamp(.75rem, 1.25rem, 1.75rem)">
{format(new Date(comment.datetime), 'M/d/yy')}
</Td>
<Td fontSize="clamp(.75rem, 1.25rem, 1.75rem)">
{comment.comment}
</Td>
<Td fontSize="clamp(.75rem, 1.25rem, 1.75rem)" fontWeight="bold">
{comment.adjustmentValue ? `$${Number(comment.adjustmentValue).toFixed(2)}` : "N/A"}
</Td>
</Tr>))
) : (
<Tr>
<Td colSpan={3}>No comments available.</Td>
</Tr>
)}
</Tbody>
</Table>
</Flex>
<Flex direction="row" width="100%" alignItems="center" mb={5}>
<Text fontSize="clamp(.75rem, 1.25rem, 1.75rem)" marginRight="0.5rem"> Show: </Text>
<Select width="auto" marginRight="0.5rem" value={commentsPerPage} onChange={handleCommentsPerPageChange}>
<option value={3}>3</option>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
</Select>
<Text>per page</Text>
<Flex direction="row" marginLeft="auto" alignItems="center">
<Text fontSize="clamp(.75rem, 1.25rem, 1.75rem)" marginRight="1rem"> {currentPageNumber} of {totalPages < 1 ? 1 : totalPages} </Text>
<Button onClick={handlePrevPage} isDisabled={currentPageNumber === 1} borderLeftRadius={30}>
<FaAngleLeft></FaAngleLeft>
</Button>
<Button onClick={handleNextPage} isDisabled={currentPageNumber === totalPages || totalPages === 0} borderRightRadius={30}>
<FaAngleRight></FaAngleRight>
</Button>
</Flex>
</Flex>
</Flex>
);
}
Loading
Loading