From c24055c557e795608f24a16b788f3dae0f461389 Mon Sep 17 00:00:00 2001 From: Victor Zheng <36215359+victorzheng02@users.noreply.github.com> Date: Sun, 24 Dec 2023 16:40:25 -0500 Subject: [PATCH] SF Claim Summary Improvements (#165) Relates to #169, partially resolved by adding hyperlink elements in 3 places: - WATonomous Finance System "logo" at top left - "View Claim" button in SFAdminContentTable - Ticket Tree on claim summary pages --------- Co-authored-by: Anson He --- backend/controller/files.controller.js | 10 + backend/routes/files.routes.js | 5 + backend/service/files.service.js | 31 ++ frontend/src/App.js | 11 +- frontend/src/components/CommentSection.js | 15 +- frontend/src/components/Navbar.js | 25 +- .../TicketContent/SFAdminContentTable.js | 20 +- .../TicketContent/SFContentTable.js | 1 - frontend/src/components/TreeView.js | 2 +- frontend/src/components/TreeViewWithLinks.js | 123 +++++++ frontend/src/hooks/hooks.js | 10 + frontend/src/pages/ClaimSummary.js | 302 +++++++++++------- frontend/src/pages/Dashboard.js | 31 +- frontend/src/utils/utils.js | 2 +- 14 files changed, 433 insertions(+), 155 deletions(-) create mode 100644 frontend/src/components/TreeViewWithLinks.js diff --git a/backend/controller/files.controller.js b/backend/controller/files.controller.js index 0bc6630..23fcffc 100644 --- a/backend/controller/files.controller.js +++ b/backend/controller/files.controller.js @@ -1,6 +1,7 @@ const { getFile, getAllFilesByReference, + getAllFilesBySF, bulkCreateFiles, deleteFile, } = require('../service/files.service') @@ -29,6 +30,14 @@ const getAllFilesByReferenceController = (req, res) => { .catch((err) => res.status(500).json('Error: ' + err)) } +const getAllFilesBySFId = (req, res) => { + getAllFilesBySF(Number(req.params.sf_id)) + .then((files) => { + res.status(200).json(files) + }) + .catch((err) => res.status(500).json('Error: ' + err)) +} + const bulkCreateFileController = (req, res) => { bulkCreateFiles( req.files, @@ -42,6 +51,7 @@ const bulkCreateFileController = (req, res) => { module.exports = { getFileController, getAllFilesByReferenceController, + getAllFilesBySFId, bulkCreateFileController, deleteFileController, } diff --git a/backend/routes/files.routes.js b/backend/routes/files.routes.js index 48bd90d..2436919 100644 --- a/backend/routes/files.routes.js +++ b/backend/routes/files.routes.js @@ -32,6 +32,11 @@ router.get( authenticateUser, FilesController.getAllFilesByReferenceController ) +router.get( + '/getallbysf/:sf_id', + authenticateUser, + FilesController.getAllFilesBySFId +) router.post( '/bulk/:reference_code', authenticateUser, diff --git a/backend/service/files.service.js b/backend/service/files.service.js index 7767c0c..10284dc 100644 --- a/backend/service/files.service.js +++ b/backend/service/files.service.js @@ -4,6 +4,7 @@ const { deleteS3File, generatePresignedUrl, } = require('../aws/s3') +const { getAllChildren } = require('./sponsorshipfunds.service') const getFile = async (id) => { return File.findById(id) @@ -26,6 +27,35 @@ const getAllFilesByReference = async (reference) => { ) } +// returns list of ticket codes from output of getAllChildren +const extractAllTicketCodes = (sf) => { + const output = [sf.code] + for (const fi of sf.fundingItems) { + output.push(fi.code) + for (const ppr of fi.personalPurchases) { + output.push(ppr.code) + } + for (const upr of fi.uwFinancePurchases) { + output.push(upr.code) + } + } + return output +} + +// returns map of ticket code to each ticket's list of files for all tickets associated with sf_id +const getAllFilesBySF = async (sf_id) => { + const sf = await getAllChildren(sf_id) + const ticketCodes = extractAllTicketCodes(sf) + const attachmentsByKey = {} + await Promise.all( + ticketCodes.map(async (code) => { + const attachments = await getAllFilesByReference(code) + attachmentsByKey[code] = attachments + }) + ) + return attachmentsByKey +} + const createFile = async (file, referenceCode, isSupportingDocument) => { const newFile = new File({ reference_code: referenceCode, @@ -60,6 +90,7 @@ const deleteFile = async (referenceCode, fileName) => { module.exports = { getFile, getAllFilesByReference, + getAllFilesBySF, createFile, bulkCreateFiles, deleteFile, diff --git a/frontend/src/App.js b/frontend/src/App.js index a1ebd3f..1eb1d7e 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -9,16 +9,11 @@ import { import { ChakraProvider } from '@chakra-ui/react' import { AuthLayout } from './contexts/AuthContext' -import { - PrivateRoute, - LoggedInRedirect, - PublicRoute, -} from './contexts/CustomRoutes' +import { PrivateRoute, LoggedInRedirect } from './contexts/CustomRoutes' import { RecoilRoot } from 'recoil' import Login from './pages/Login' import Dashboard from './pages/Dashboard' -import ClaimSummary from './pages/ClaimSummary' import NotFound from './pages/NotFound' const router = createBrowserRouter( @@ -27,15 +22,13 @@ const router = createBrowserRouter( }> } /> - }> - } /> - }> } /> } /> } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/components/CommentSection.js b/frontend/src/components/CommentSection.js index 4bca30b..4ad4600 100644 --- a/frontend/src/components/CommentSection.js +++ b/frontend/src/components/CommentSection.js @@ -1,27 +1,16 @@ // modified version of https://github.com/ianstormtaylor/slate/blob/main/site/examples/richtext.tsx import React, { useCallback, useMemo } from 'react' import isHotkey from 'is-hotkey' -import { Editable, withReact, useSlate, Slate } from 'slate-react' -import { - Editor, - Transforms, - createEditor, - Element as SlateElement, -} from 'slate' +import { Editable, withReact, Slate } from 'slate-react' +import { createEditor } from 'slate' import { withHistory } from 'slate-history' import { BlockButton, - Button, Element, - Icon, Leaf, MarkButton, - TEXT_ALIGN_TYPES, Toolbar, - isBlockActive, - isMarkActive, - toggleBlock, toggleMark, } from './SlateComponents' import { Box, Heading } from '@chakra-ui/react' diff --git a/frontend/src/components/Navbar.js b/frontend/src/components/Navbar.js index 16e695d..d4e3dbb 100644 --- a/frontend/src/components/Navbar.js +++ b/frontend/src/components/Navbar.js @@ -1,6 +1,13 @@ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Button, Flex, Heading, Spacer, useDisclosure } from '@chakra-ui/react' +import { + Button, + Flex, + Heading, + Spacer, + useDisclosure, + Link, +} from '@chakra-ui/react' import { useAuth } from '../contexts/AuthContext' import { CreateTicketModal } from './CreateTicketModal' const Navbar = () => { @@ -44,14 +51,14 @@ const Navbar = () => { w="100%" h="80px" > - navigate('/')} - cursor="pointer" - > - WATonomous Finance System - + + + WATonomous Finance System + + {currentUser && ( + {/* can remove getPreserveParamsHref if it does not make sense to preserve params */} + + + } ) } -export default UPRAdminContentTable +export default SFAdminContentTable diff --git a/frontend/src/components/TicketContent/SFContentTable.js b/frontend/src/components/TicketContent/SFContentTable.js index 20e784a..db6c494 100644 --- a/frontend/src/components/TicketContent/SFContentTable.js +++ b/frontend/src/components/TicketContent/SFContentTable.js @@ -7,7 +7,6 @@ import { currentTicketState } from '../../state/atoms' const SFContentTable = () => { const currentTicket = useRecoilValue(currentTicketState) - return ( diff --git a/frontend/src/components/TreeView.js b/frontend/src/components/TreeView.js index aa99cea..8a70a8c 100644 --- a/frontend/src/components/TreeView.js +++ b/frontend/src/components/TreeView.js @@ -10,7 +10,7 @@ const TreeView = () => { const preserveParamsNavigate = usePreserveParamsNavigate() const sortTickets = (ticketList) => { - return [...ticketList].sort((a, b) => (a._id > b._id ? 1 : -1)) + return ticketList.toSorted((a, b) => a._id - b._id) } const getFundingItemTree = (fi) => ( diff --git a/frontend/src/components/TreeViewWithLinks.js b/frontend/src/components/TreeViewWithLinks.js new file mode 100644 index 0000000..9d65f6f --- /dev/null +++ b/frontend/src/components/TreeViewWithLinks.js @@ -0,0 +1,123 @@ +import React from 'react' +import { Text, Box, Stack, Link } from '@chakra-ui/react' +import { useGetPreserveParamsHref } from '../hooks/hooks' +import { useRecoilValue } from 'recoil' +import { currentTicketState, currentTreeState } from '../state/atoms' + +const TreeViewWithLinks = () => { + const currentTicket = useRecoilValue(currentTicketState) + const currentTree = useRecoilValue(currentTreeState) + const getPreserveParamsHref = useGetPreserveParamsHref() + + const sortTickets = (ticketList) => { + return ticketList.toSorted((a, b) => a._id - b._id) + } + + const getFundingItemTree = (fi) => ( + + + + + {fi.codename} + + + + {sortTickets(fi.personalPurchases).map((ppr) => { + return ( + + + + {ppr.codename} + + + + ) + })} + {sortTickets(fi.uwFinancePurchases).map((upr) => { + return ( + + + + {upr.codename} + + + + ) + })} + + ) + + if ( + !currentTicket.type || + !currentTree || + Object.keys(currentTree).length === 0 + ) + return No tree to display + + // Special Case: WATO Cash + // OR UPR/PPR with fi_link to WATO Cash + if (currentTree.sf_link === -1) { + return {getFundingItemTree(currentTree)} + } + + return ( + + + + + {currentTree.codename} + + + + {sortTickets(currentTree.fundingItems).map(getFundingItemTree)} + + ) +} + +export default TreeViewWithLinks diff --git a/frontend/src/hooks/hooks.js b/frontend/src/hooks/hooks.js index 9db4a9b..b93adfc 100644 --- a/frontend/src/hooks/hooks.js +++ b/frontend/src/hooks/hooks.js @@ -9,3 +9,13 @@ export const usePreserveParamsNavigate = () => { navigate(`${path}?${oldSearchParams.toString()}`) } } + +export const useGetPreserveParamsHref = () => { + const [searchParams] = useSearchParams() + const oldSearchParams = new URLSearchParams(searchParams) + const newSearchParams = + oldSearchParams.toString().trim() === '' + ? '' + : `?${oldSearchParams.toString()}` + return (path) => `${path}${newSearchParams}` +} diff --git a/frontend/src/pages/ClaimSummary.js b/frontend/src/pages/ClaimSummary.js index c42b68b..0d1916e 100644 --- a/frontend/src/pages/ClaimSummary.js +++ b/frontend/src/pages/ClaimSummary.js @@ -1,8 +1,11 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { Box, + Card, + CardBody, + Flex, Heading, - Center, + StackDivider, Th, Tr, Td, @@ -10,130 +13,211 @@ import { Table, Thead, Tbody, - Button, Stack, + Center, } from '@chakra-ui/react' import { getStandardizedDate } from '../utils/utils' -import { useParams } from 'react-router-dom' import LoadingSpinner from '../components/LoadingSpinner' +import TreeViewWithLinks from '../components/TreeViewWithLinks' +import { getFormattedCurrency } from '../utils/utils' const FundingItemView = ({ fundingItem }) => { return ( - - {fundingItem.name} - - {`Allocation: ${fundingItem.funding_allocation}`} - - - {`Amount Reimbursed: ${fundingItem.amount_reimbursed}`} - - - Personal Purchases - -
- - - - - - - - - - - {fundingItem.personalPurchases.map((pp) => { - return ( - - - - - - - - ) - })} - -
Item NameItem SpendRequisition NumberPurchase Order NumberAction
{pp.name}{pp.cost}{pp.requisition_number}{pp.po_number} - -
- - - - UW Finance Purchases - - - - - - - - - - - - - {fundingItem.uwFinancePurchases.map((uwfp) => { - return ( - - - - - - - - ) - })} - -
Item NameItem SpendRequisition NumberPurchase Order NumberAction
{uwfp.name}{uwfp.cost}{uwfp.requisition_number}{uwfp.po_number} - -
-
-
- + + + } spacing="4"> + + + {fundingItem.codename} + + + {`Funding Allocation: ${getFormattedCurrency( + fundingItem.funding_allocation + )}`} + + + {`Funding Spent: ${getFormattedCurrency( + fundingItem.funding_spent + )}`} + + + {fundingItem.personalPurchases.length > 0 && ( + + Personal Purchases + + + + + + + + + + + {fundingItem.personalPurchases.map( + (ppr) => { + return ( + + + + + + ) + } + )} + +
Item NameCostStatus
+ {ppr.codename} + + {getFormattedCurrency( + ppr.cost + )} + + {ppr.status} +
+
+
+ )} + {fundingItem.uwFinancePurchases.length > 0 && ( + + UW Finance Purchases + + + + + + + + + + + + + {fundingItem.uwFinancePurchases.map( + (upr) => { + return ( + + + + + + + + ) + } + )} + +
Item NameCostStatusReq #PO #
+ {upr.codename} + + {getFormattedCurrency( + upr.cost + )} + + {upr.status} + + { + upr.requisition_number + } + + {upr.po_number} +
+
+
+ )} +
+
+
) } -const ClaimSummary = () => { - const { id } = useParams() - const [claimData, setClaimData] = useState() - useEffect(() => { - const fetchClaimData = async () => { - const res = await fetch( - `${process.env.REACT_APP_BACKEND_URL}/sponsorshipfunds/getallchildren/${id}` - ) - const data = await res.json() - setClaimData(data) - } - fetchClaimData() - }, [id]) +const ClaimSummary = ({ claimData }) => { + // 24px top padding + 43.2 for main heading + 6 * (24px subheading + 8px gap) + 24px bottom padding + 2px bottom border + const claimSummaryInfoHeight = 285.2 + const claimSummaryInfoHeightText = `${claimSummaryInfoHeight}px` + const ticketTreeHeightText = `calc(100vh - ${ + claimSummaryInfoHeight + 80 + }px)` - if (!claimData) + if (Object.keys(claimData).length === 0) return ( - + ) return ( - - Claim Summary - {`Sponsorship Fund ID: ${id}`} - {`Name: ${claimData.name}`} - - {`Deadline: ${getStandardizedDate(claimData.claim_deadline)}`} - -
- {claimData.fundingItems.map((fi) => { - return - })} -
-
+ <> + + + Claim Summary + {claimData.codename} + + {`Proposal ID: ${claimData.proposal_id}`} + + + {`Deadline: ${getStandardizedDate( + claimData.claim_deadline + )}`} + + {`Status: ${claimData.status}`} + + {`Funding Allocation: ${getFormattedCurrency( + claimData.funding_allocation + )}`} + + + {`Funding Spent: ${getFormattedCurrency( + claimData.funding_spent + )}`} + + + + + + + +
+ {claimData.fundingItems.map((fi) => { + return + })} +
+
+ ) } diff --git a/frontend/src/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index 5db4382..1064b3a 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -30,7 +30,7 @@ import { TICKET_TYPES } from '../constants' import buildTicketTree from '../utils/buildTicketTree' import DeleteTicketAlertDialog from '../components/DeleteTicketAlertDialog' import { axiosPreset } from '../axiosConfig' -import { useRecoilState, useSetRecoilState } from 'recoil' +import { useRecoilState } from 'recoil' import { allTicketsState, currentFiles, @@ -47,6 +47,7 @@ import FIAdminContentTable from '../components/TicketContent/FIAdminContentTable import { getAllTickets } from '../utils/globalSetters' import FileViewer from '../components/FileViewer' import PPRReporterTable from '../components/TicketContent/PPRReporterTable' +import ClaimSummary from './ClaimSummary' import { createErrorMessage } from '../utils/errorToasts' const Dashboard = () => { @@ -78,10 +79,11 @@ const Dashboard = () => { } = useDisclosure() const auth = useAuth() + const [displayClaimSummary, setDisplayClaimSummary] = useState(false) const [allUsers, setAllUsers] = useState({ users: [] }) const [isCurrentTicketReporter, setIsCurrentTicketReporter] = useState(false) - const setCurrentTree = useSetRecoilState(currentTreeState) + const [currentTree, setCurrentTree] = useRecoilState(currentTreeState) const [currentTicket, setCurrentTicket] = useRecoilState(currentTicketState) const [allTickets, setAllTickets] = useRecoilState(allTicketsState) const [uploadedFiles, setUploadedFiles] = useRecoilState(currentFiles) @@ -135,16 +137,23 @@ const Dashboard = () => { useEffect(() => { if (location.pathname === '/') return const splitPath = location.pathname.split('/') + let currentTicketType = splitPath[1] + const currentTicketId = parseInt(splitPath[2]) + if (currentTicketType === 'claim') { + setDisplayClaimSummary(true) + currentTicketType = TICKET_TYPES.SF + } else { + setDisplayClaimSummary(false) + } + if ( splitPath.length !== 3 || - !Object.values(TICKET_TYPES).includes(splitPath[1]) + !Object.values(TICKET_TYPES).includes(currentTicketType) ) { navigate('/notfound') return } - const currentTicketType = splitPath[1] - const currentTicketId = parseInt(splitPath[2]) const currentTicketData = allTickets[ TICKET_TYPES[currentTicketType] ].find((ticket) => ticket._id === currentTicketId) @@ -172,6 +181,7 @@ const Dashboard = () => { setCurrentTree, auth.currentUser.uid, getUploadedFiles, + displayClaimSummary, ]) useEffect(() => { @@ -368,8 +378,14 @@ const Dashboard = () => { - - {getMainContent()} + {displayClaimSummary ? ( + + ) : ( + <> + + {getMainContent()} + + )} {isUploadModalOpen && ( { isSupportingDocument={true} /> )} - {isDeleteTicketOpen && ( { style: 'currency', currency: 'CAD', }) - return `CAD ${currencyFormatter.format(currencyStr)}` + return currencyFormatter.format(currencyStr) }