From 50f4e7670212f4487462ae9be9d555c037957042 Mon Sep 17 00:00:00 2001 From: Philip Zhang Date: Fri, 14 Jun 2024 01:48:46 -0700 Subject: [PATCH] Bulk Unit Export to Excel (#115) ## Tracking Info Resolves #68 ## Changes - implement bulk unit export - various improvements ## Testing - ensure exports only filtered units and associated data ## Confirmation of Change ![image](https://github.com/TritonSE/USHS-Housing-Portal/assets/24444266/3420f23d-7553-4780-86d9-7b93c94dd023) --------- Co-authored-by: Pranav Kumar Soma --- backend/bun.lockb | Bin 208860 -> 209221 bytes backend/package.json | 3 +- backend/src/controllers/units.ts | 10 ++ backend/src/models/referral.ts | 2 +- backend/src/routes/units.ts | 6 +- backend/src/services/units.ts | 66 +++++++++++- frontend/public/export-icon.svg | 3 + frontend/src/api/units.ts | 13 +++ frontend/src/components/ExportPopup.tsx | 88 ++++++++++++++++ frontend/src/components/UnitCard.tsx | 57 ++++++----- frontend/src/pages/Home.tsx | 54 ++++++++-- frontend/src/pages/RenterCandidatePage.tsx | 113 +++------------------ 12 files changed, 276 insertions(+), 139 deletions(-) create mode 100644 frontend/public/export-icon.svg diff --git a/backend/bun.lockb b/backend/bun.lockb index 4bed0761bde51a278961a0d5f41b513fffac0888..b8ce92e9295501083a3f1dd2aba6a71ab87ad770 100755 GIT binary patch delta 36550 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 Dl{O30 delta 37437 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 { + const workbookBuffer = await exportUnits(req.query as FilterParams); + + res.statusCode = 200; + res.setHeader("Content-Disposition", 'attachment; filename="ushs-data-export.xlsx"'); + res.setHeader("Content-Type", "application/vnd.ms-excel"); + res.end(workbookBuffer); +}); + /** * Handle a request to get a unit. */ diff --git a/backend/src/models/referral.ts b/backend/src/models/referral.ts index 6c2d4e0..2404de7 100644 --- a/backend/src/models/referral.ts +++ b/backend/src/models/referral.ts @@ -8,7 +8,7 @@ const referralSchema = new Schema( enum: ["Referred", "Viewing", "Pending", "Approved", "Denied", "Leased", "Canceled"], default: "Referred", }, - renterCandidate: { type: Schema.Types.ObjectId, ref: "Renter" }, + renterCandidate: { type: Schema.Types.ObjectId, ref: "Renter", required: true }, unit: { type: Schema.Types.ObjectId, ref: "Unit", diff --git a/backend/src/routes/units.ts b/backend/src/routes/units.ts index 12fbb90..8303a5b 100644 --- a/backend/src/routes/units.ts +++ b/backend/src/routes/units.ts @@ -13,6 +13,10 @@ import { createUnitValidators, updateUnitValidators } from "@/validators/units"; const router = express.Router(); +router.get("/", requireUser, UnitController.getUnitsHandler); + +router.get("/export", requireUser, UnitController.exportUnitsHandler); + router.get("/:id", requireUser, UnitController.getUnitHandler); router.post( @@ -22,8 +26,6 @@ router.post( UnitController.createUnitsHandler, ); -router.get("/", requireUser, UnitController.getUnitsHandler); - router.put( "/:id", requireHousingLocator, diff --git a/backend/src/services/units.ts b/backend/src/services/units.ts index 1b0cff4..972cbb5 100644 --- a/backend/src/services/units.ts +++ b/backend/src/services/units.ts @@ -1,6 +1,11 @@ -import { FilterQuery, UpdateQuery } from "mongoose"; +import { ObjectId } from "mongodb"; +import { Document, FilterQuery, UpdateQuery } from "mongoose"; +import * as XLSX from "xlsx"; +import { ReferralModel } from "@/models/referral"; +import { RenterModel } from "@/models/renter"; import { Unit, UnitModel } from "@/models/units"; +import { UserModel } from "@/models/user"; type UserReadOnlyFields = "approved" | "createdAt" | "updatedAt"; @@ -223,3 +228,62 @@ export const getUnits = async (filters: FilterParams) => { return filteredUnits; }; + +const sheetFromData = (data: Document[]) => { + const sanitizedData = data.map((doc) => { + // remove unneeded keys and convert all values to strings + const { _id, __v, ...rest } = doc.toJSON() as Record; + const sanitizedRest = Object.keys(rest).reduce>((acc, key) => { + const value = rest[key]; + if ((value as unknown) instanceof ObjectId) { + acc[key] = value.toString(); + } else if (Array.isArray(value)) { + acc[key] = JSON.stringify(value); + } else { + acc[key] = value; + } + return acc; + }, {}); + + return { + id: _id.toString(), + ...sanitizedRest, + }; + }); + return XLSX.utils.json_to_sheet(sanitizedData); +}; + +export const exportUnits = async (filters: FilterParams) => { + const unitsData = await getUnits(filters); + + const unitIds = unitsData.map((unit) => unit._id); + const referralsData = await ReferralModel.find().where("unit").in(unitIds).exec(); + + const renterCandidateIds = [ + ...new Set(referralsData.map((referral) => referral.renterCandidate)), + ]; + const renterCandidates = await RenterModel.find().where("_id").in(renterCandidateIds).exec(); + + const housingLocatorIds = [ + ...new Set(referralsData.map((referral) => referral.assignedHousingLocator)), + ]; + const referringStaffIds = [ + ...new Set(referralsData.map((referral) => referral.assignedReferringStaff)), + ]; + const staffIds = housingLocatorIds.concat(referringStaffIds); + const staffData = await UserModel.find().where("_id").in(staffIds).exec(); + + // Generate Excel workbook + const unitsSheet = sheetFromData(unitsData); + const referralsSheet = sheetFromData(referralsData); + const renterCandidatesSheet = sheetFromData(renterCandidates); + const staffSheet = sheetFromData(staffData); + + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, unitsSheet, "Units"); + XLSX.utils.book_append_sheet(workbook, referralsSheet, "Referrals"); + XLSX.utils.book_append_sheet(workbook, renterCandidatesSheet, "Renter Candidates"); + XLSX.utils.book_append_sheet(workbook, staffSheet, "Staff"); + + return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" }) as Buffer; +}; diff --git a/frontend/public/export-icon.svg b/frontend/public/export-icon.svg new file mode 100644 index 0000000..605c0a5 --- /dev/null +++ b/frontend/public/export-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/api/units.ts b/frontend/src/api/units.ts index 22099e7..55887b1 100644 --- a/frontend/src/api/units.ts +++ b/frontend/src/api/units.ts @@ -146,6 +146,19 @@ export async function getUnits(params: GetUnitsParams): Promise> { + try { + const queryParams = new URLSearchParams(params); + const url = `/units/export?${queryParams.toString()}`; + const response = await get(url); + + const data = await response.blob(); + return { success: true, data }; + } catch (error) { + return handleAPIError(error); + } +} + type HousingLocatorFields = | "leasedStatus" | "whereFound" diff --git a/frontend/src/components/ExportPopup.tsx b/frontend/src/components/ExportPopup.tsx index e69de29..4d94b00 100644 --- a/frontend/src/components/ExportPopup.tsx +++ b/frontend/src/components/ExportPopup.tsx @@ -0,0 +1,88 @@ +import styled from "styled-components"; + +import { Button } from "./Button"; + +const Overlay = styled.div` + width: 100vw; + height: 100vh; + top: 0; + left: 0; + right: 0; + bottom: 0; + position: fixed; + background: rgba(0, 0, 0, 0.25); + z-index: 2; +`; + +const Modal = styled.div` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 20px; + background: #fff; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 30px; + z-index: 2; + padding: 97px 186px; +`; + +const HeadingWrapper = styled.div` + font-family: "Neutraface Text"; + font-size: 32px; + font-style: normal; + font-weight: 700; + text-align: center; +`; + +const MessageWrapper = styled.div` + font-size: 16px; + margin-top: 10px; + text-align: center; +`; + +const ButtonsWrapper = styled.div` + padding-top: 25px; + display: flex; + flex-direction: row; + gap: 400px; +`; + +const Icon = styled.img` + width: 78px; + height: 78px; +`; + +type PopupProps = { + active: boolean; + onClose: () => void; +}; + +export const ExportPopup = ({ active, onClose }: PopupProps) => { + if (!active) return null; + + return ( + <> + + + +
+ Data Exporting... + + Generating an Excel sheet with the currently filtered Units and associated Referrals and + Renter Candidates. The download will start shortly... + +
+ + + +
+ + ); +}; diff --git a/frontend/src/components/UnitCard.tsx b/frontend/src/components/UnitCard.tsx index 14b7508..dac7644 100644 --- a/frontend/src/components/UnitCard.tsx +++ b/frontend/src/components/UnitCard.tsx @@ -12,21 +12,19 @@ import { FiltersContext } from "@/pages/Home"; const UnitCardContainer = styled.div<{ pending: boolean }>` display: flex; flex-direction: column; - justify-content: flex-start; + justify-content: space-between; align-content: flex-start; - gap: 8px; - padding-left: 20px; - padding-right: 20px; - padding-top: 20px; - width: 318px; + padding: 20px; height: 370px; + width: 330px; background-color: white; border-radius: 6.5px; border: 1.3px solid ${(props) => (props.pending ? "rgba(230, 159, 28, 0.50)" : "#cdcaca")}; box-shadow: 1.181px 1.181px 2.362px 0px rgba(188, 186, 183, 0.4); - - // position: absolute; + &:hover { + box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.15); + } `; const UnitCardText = styled.span` @@ -56,13 +54,18 @@ const BedBathRow = styled.div` gap: 4px; `; -const AddressRow = styled.div` +const Address = styled.div` display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; `; +const BottomRow = styled.div` + display: flex; + justify-content: space-between; +`; + const AvailabilityIcon = styled.img` width: 18px; height: 18px; @@ -112,10 +115,8 @@ const BedBathText = styled(NumberText)` const DeleteIcon = styled.img` width: 22px; height: 24px; - position: relative; - top: -32px; - left: 250px; cursor: pointer; + align-self: flex-end; `; const Overlay = styled.div` @@ -311,21 +312,23 @@ export const UnitCard = ({ unit, refreshUnits }: CardProps) => { {unit.sqft} sqft - - {unit.streetAddress} - {`${unit.city}, ${unit.state} ${unit.areaCode}`} - - {unit.approved && dataContext.currentUser?.isHousingLocator && ( - { - // Stop click from propagating to parent (opening the unit page) - e.preventDefault(); - e.stopPropagation(); - setPopup(true); - }} - /> - )} + +
+ {unit.streetAddress} + {`${unit.city}, ${unit.state} ${unit.areaCode}`} +
+ {unit.approved && dataContext.currentUser?.isHousingLocator && ( + { + // Stop click from propagating to parent (opening the unit page) + e.preventDefault(); + e.stopPropagation(); + setPopup(true); + }} + /> + )} +
{popup && ( diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index fcaf3bd..c7dd5f1 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -3,7 +3,8 @@ import { Helmet } from "react-helmet-async"; import { useLocation } from "react-router-dom"; import styled from "styled-components"; -import { FilterParams, GetUnitsParams, Unit, getUnits } from "@/api/units"; +import { FilterParams, GetUnitsParams, Unit, exportUnits, getUnits } from "@/api/units"; +import { ExportPopup } from "@/components/ExportPopup"; import { FilterDropdown } from "@/components/FilterDropdown"; import { FilterPanel } from "@/components/FilterPanel"; import { NavBar } from "@/components/NavBar"; @@ -15,6 +16,7 @@ import { DataContext } from "@/contexts/DataContext"; const ButtonsWrapper = styled.div` display: flex; justify-content: end; + align-items: center; `; const HeaderText = styled.span` @@ -25,8 +27,6 @@ const HeaderText = styled.span` const ToggleButtonWrapper = styled.div` display: flex; padding: 0; - width: 195px; - height: 140px; gap: 0px; border-radius: 100px 0px 0px 0px; opacity: 0px; @@ -54,10 +54,18 @@ const ListViewButton = styled(CardViewButton)` background: ${(props) => (props.selected ? "#ec85371a" : "#EEEEEE")}; `; +const ExportButton = styled.img` + cursor: pointer; + width: 43px; + height: 43px; + margin-left: 24px; +`; + const SearchStateWrapper = styled.div` display: flex; flex-direction: row; justify-content: space-between; + margin-bottom: 52px; `; export type FilterContextType = { @@ -117,9 +125,11 @@ const UnitContent = styled.div` overflow-y: scroll; padding: 70px 60px; `; + export function Home() { const dataContext = useContext(DataContext); const previousFilters = useLocation().state as FilterParams; + const [showExportPopup, setShowExportPopup] = useState(false); const [units, setUnits] = useState([]); const [filters, setFilters] = useState( previousFilters ?? { @@ -129,8 +139,8 @@ export function Home() { ); const [viewMode, setViewMode] = useState("card"); - const fetchUnits = (filterParams: FilterParams) => { - let query: GetUnitsParams = { + const filterQuery = (filterParams: FilterParams) => { + const query: GetUnitsParams = { sort: filterParams.sort, approved: filterParams.approved, search: filterParams.search, @@ -160,8 +170,11 @@ export function Home() { toDate: filterParams.toDate, }; - query = Object.fromEntries(Object.entries(query).filter(([_, value]) => value !== undefined)); + return Object.fromEntries(Object.entries(query).filter(([_, value]) => value !== undefined)); + }; + const fetchUnits = (filterParams: FilterParams) => { + const query = filterQuery(filterParams); getUnits(query) .then((response) => { if (response.success) { @@ -188,6 +201,24 @@ export function Home() { setViewMode("list"); }; + const handleExportUnitData = () => { + setShowExportPopup(true); + const query = filterQuery(filters); + exportUnits(query) + .then((response) => { + if (response.success) { + const url = window.URL.createObjectURL(response.data); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", "ushs-data-export.xlsx"); + document.body.appendChild(link); + link.click(); + link.remove(); + } + }) + .catch(console.error); + }; + return ( @@ -224,6 +255,11 @@ export function Home() { } > + @@ -265,6 +301,12 @@ export function Home() { + { + setShowExportPopup(false); + }} + /> ); } diff --git a/frontend/src/pages/RenterCandidatePage.tsx b/frontend/src/pages/RenterCandidatePage.tsx index a4deff6..6575f8e 100644 --- a/frontend/src/pages/RenterCandidatePage.tsx +++ b/frontend/src/pages/RenterCandidatePage.tsx @@ -8,10 +8,9 @@ import { Loading } from "./Loading"; import { UpdateReferralRequest, deleteReferral, updateReferral } from "@/api/referrals"; import { RenterCandidate, editRenterCandidate, getRenterCandidate } from "@/api/renter-candidates"; -import { Referral } from "@/api/units"; +import { REFERRAL_STATUSES, Referral } from "@/api/units"; import { Page } from "@/components"; import { Button } from "@/components/Button"; -import { CustomCheckboxRadio, OptionLabel } from "@/components/ListingForm/CommonStyles"; import { NavBar } from "@/components/NavBar"; import { ReferralTableDropDown } from "@/components/ReferralTableDropDown"; import { Table, TableCellContent } from "@/components/Table"; @@ -266,12 +265,6 @@ const XButton = styled.div` width: 10px; `; -const CustomAuthorityInput = styled.input` - font-size: 15px; - padding: 2px 5px; - margin-top: 2px; -`; - type ReferralQuery = Record>; export function RenterCandidatePage() { @@ -293,19 +286,6 @@ export function RenterCandidatePage() { const [currReferral, setCurrReferral] = useState(""); - const [currAuthority, setCurrAuthority] = useState(""); - const [customAuthority, setCustomAuthority] = useState(undefined); - - const REFERRAL_STATUSES = [ - "Referred", - "Viewing", - "Pending", - "Approved", - "Denied", - "Leased", - "Canceled", - ]; - const fetchRenterCandidate = () => { if (id !== undefined) { setLoading(true); @@ -314,10 +294,6 @@ export function RenterCandidatePage() { const { renter, referrals } = result.data; setRenterCandidate(renter); setRenterReferrals(referrals); - setCurrAuthority(renter.program); - if (renter.program !== "HACLA" && renter.program !== "LACDA") { - setCustomAuthority(renter.program); - } } else { // Go back to the referrals page if the renter is not found navigate("/referrals"); @@ -439,7 +415,6 @@ export function RenterCandidatePage() { if (isEditing) { handleUpdateRenter(); handleUpdateReferrals(); - setCustomAuthority(""); } setIsEditing(!isEditing); }} @@ -540,81 +515,17 @@ export function RenterCandidatePage() { - Housing Program: -
- - { - setEditRenterQuery({ - ...editRenterQuery, - program: "LACDA", - }); - setCurrAuthority("LACDA"); - }} - /> - LACDA - - - - { - setEditRenterQuery({ - ...editRenterQuery, - program: "HACLA", - }); - setCurrAuthority("HACLA"); - }} - /> - HACLA - - - - { - setEditRenterQuery({ - ...editRenterQuery, - program: customAuthority ?? "", - }); - setCurrAuthority(customAuthority ?? ""); - setCustomAuthority(customAuthority ?? ""); - }} - /> - Other: - { - const customInput = (e.target as HTMLTextAreaElement).value; - setEditRenterQuery({ - ...editRenterQuery, - program: customInput, - }); - setCurrAuthority(customInput); - setCustomAuthority(customInput); - }} - defaultValue={ - renterCandidate.program !== "HACLA" && - renterCandidate.program !== "LACDA" - ? renterCandidate.program - : "" - } - /> - -
+ Housing Program: + { + setEditRenterQuery({ + ...editRenterQuery, + program: (e.target as HTMLTextAreaElement).value, + }); + }} + />