Skip to content

Commit

Permalink
Add capital project detail panel and connect it to route for single p…
Browse files Browse the repository at this point in the history
…roject
  • Loading branch information
TylerMatteo committed Jul 8, 2024
1 parent 62a9766 commit cef1779
Show file tree
Hide file tree
Showing 7 changed files with 1,240 additions and 2,550 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { CapitalProjectDetailPanel } from "./CapitalProjectDetailPanel";
import {
CapitalProjectBudgeted,
createCapitalProjectBudgeted,
Agency,
} from "~/gen";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

describe("CapitalProjectDetailPanel", () => {
let capitalProject: CapitalProjectBudgeted;
let agencies: Agency[];
const onClose = vi.fn();
beforeAll(() => {
agencies = [
{ initials: "DDC", name: "Department of Design and Construction" },
{ initials: "DEP", name: "Department of Environmental Protection" },
];
capitalProject = {
...createCapitalProjectBudgeted(),
managingAgency: "DDC",
sponsoringAgencies: ["DEP"],
};
});

it("should render the detail panel with project description", () => {
render(
<CapitalProjectDetailPanel
capitalProject={capitalProject}
agencies={agencies}
onClose={onClose}
/>,
);
expect(screen.getByText(capitalProject.description)).toBeVisible();
});

it("should render the name of the managing agency", () => {
render(
<CapitalProjectDetailPanel
capitalProject={capitalProject}
agencies={agencies}
onClose={onClose}
/>,
);
expect(screen.getByText(agencies[0].name)).toBeVisible();
});

it("should render the name of the sponsoring agency", () => {
render(
<CapitalProjectDetailPanel
capitalProject={capitalProject}
agencies={agencies}
onClose={onClose}
/>,
);
expect(screen.getByText(agencies[1].name)).toBeVisible();
});

it("should call onClose when the back chevron is clicked", async () => {
render(
<CapitalProjectDetailPanel
capitalProject={capitalProject}
agencies={agencies}
onClose={onClose}
/>,
);

await userEvent.click(screen.getByLabelText("Close project detail panel"));
expect(onClose).toHaveBeenCalled();
});

it("should assign dates after July to the following fiscal year", () => {
capitalProject.minDate = "2018-08-03";
capitalProject.maxDate = "2018-08-03";
render(
<CapitalProjectDetailPanel
capitalProject={capitalProject}
agencies={agencies}
onClose={onClose}
/>,
);
expect(screen.getByText("FY2019")).toBeVisible();
});
});
181 changes: 181 additions & 0 deletions app/components/CapitalProjectDetailPanel/CapitalProjectDetailPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { useState } from "react";
import { getYear, getMonth, compareAsc } from "date-fns";
import numbro from "numbro";
import { ChevronLeftIcon } from "@chakra-ui/icons";
import {
Box,
Flex,
Heading,
HStack,
Text,
Wrap,
WrapItem,
IconButton,
Hide,
} from "@nycplanning/streetscape";
import { CapitalProjectBudgeted, Agency } from "../../gen";

export interface CapitalProjectDetailPanelProps {
capitalProject: CapitalProjectBudgeted;
agencies: Agency[];
onClose: () => void;
}

export const CapitalProjectDetailPanel = ({
capitalProject,
agencies,
onClose,
}: CapitalProjectDetailPanelProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const getFiscalYearForDate = (date: Date): number => {
const year = getYear(date);
const month = getMonth(date);
return month <= 6 ? year : year + 1;
};

const formatFiscalYearRange = (minDate: Date, maxDate: Date) => {
if (compareAsc(minDate, maxDate) === 0) {
return `FY${getFiscalYearForDate(minDate)}`;
}
return `FY${getFiscalYearForDate(minDate)} - FY${getFiscalYearForDate(maxDate)}`;
};

return (
<Flex
borderRadius={"base"}
padding={{ base: 3, lg: 4 }}
background={"white"}
direction={"column"}
width={{ base: "full", lg: "21.25rem" }}
maxW={{ base: "21.25rem", lg: "unset" }}
boxShadow={"0px 8px 4px 0px rgba(0, 0, 0, 0.08)"}
gap={4}
>
<Hide above="lg">
<Box
height={"4px"}
width={20}
backgroundColor={"gray.300"}
borderRadius="2px"
alignSelf={"center"}
role="button"
aria-label={
isExpanded
? "Collapse project detail panel"
: "Expand project detail panel"
}
onClick={() => {
setIsExpanded(!isExpanded);
}}
/>
</Hide>
<HStack align={"start"}>
<IconButton
aria-label="Close project detail panel"
icon={<ChevronLeftIcon boxSize={10} />}
color={"black"}
backgroundColor={"white"}
_hover={{
border: "none",
backgroundColor: "blackAlpha.100",
}}
onClick={onClose}
/>
<Heading color="gray.600" fontWeight={"bold"} fontSize={"lg"}>
{capitalProject.description}
</Heading>
</HStack>
<Flex
height={{ base: isExpanded ? "436px" : "196px", lg: "auto" }}
overflowY={{ base: "scroll", lg: "auto" }}
direction={"column"}
transition={"height 0.5s ease-in-out"}
gap={4}
>
<Box
backgroundColor="gray.50"
paddingY={3}
paddingX={2}
borderRadius={"base"}
>
<Heading color="gray.600" fontWeight={"medium"}>
Capital Commitments
</Heading>
<Text mb={3}>
{formatFiscalYearRange(
new Date(capitalProject.minDate),
new Date(capitalProject.maxDate),
)}
</Text>
<Heading color="gray.600" fontWeight={"medium"}>
Total Future Commitments
</Heading>
<Text>
{numbro(capitalProject.commitmentsTotal)
.format({
average: true,
mantissa: 2,
output: "currency",
spaceSeparated: true,
})
.toUpperCase()}
</Text>
</Box>
<Text>
Project ID: {capitalProject.managingCode}
{capitalProject.id}
</Text>
<Box>
<Heading color="gray.600" fontWeight={"medium"}>
Managing Agency
</Heading>
<Text>
{
agencies.find(
(agency) => agency.initials === capitalProject.managingAgency,
)?.name
}
</Text>
</Box>
<Box>
<Heading color="gray.600" fontWeight={"medium"}>
Sponsoring Agency
</Heading>
<Text>
{capitalProject.sponsoringAgencies
.map(
(initials) =>
agencies.find((agency) => agency.initials === initials)?.name,
)
.join(" ")}
</Text>
</Box>
<Box>
<Heading color="gray.600" fontWeight={"medium"} mb={2}>
Project Type
</Heading>
<Wrap spacing={1}>
{capitalProject.budgetTypes.map((budgetType) => (
<WrapItem key={budgetType}>
<Text
as={"span"}
display={"inline-block"}
width="auto"
paddingX={2}
paddingY={1}
borderRadius={"0.5rem"}
borderColor="gray.400"
borderStyle={"solid"}
borderWidth={"1.5px"}
backgroundColor={"gray.100"}
>
{budgetType}
</Text>
</WrapItem>
))}
</Wrap>
</Box>
</Flex>
</Flex>
);
};
1 change: 1 addition & 0 deletions app/components/CapitalProjectDetailPanel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CapitalProjectDetailPanel } from './CapitalProjectDetailPanel';
40 changes: 39 additions & 1 deletion app/routes/capital-projects.$managingCode.$capitalProjectId.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData, useNavigate } from "@remix-run/react";
import {
findCapitalProjectByManagingCodeCapitalProjectId,
findAgencies,
} from "../gen";
import { CapitalProjectDetailPanel } from "../components/CapitalProjectDetailPanel";

export async function loader({ params }: LoaderFunctionArgs) {
const agenciesResponse = await findAgencies({
baseURL: `${import.meta.env.VITE_ZONING_API_URL}/api`,
});
if (
typeof params.managingCode === "undefined" ||
typeof params.capitalProjectId === "undefined"
) {
throw json("Bad Request", { status: 400 });
}
const capitalProject = await findCapitalProjectByManagingCodeCapitalProjectId(
params.managingCode,
params.capitalProjectId,
{
baseURL: `${import.meta.env.VITE_ZONING_API_URL}/api`,
},
);
return json({ capitalProject, agencies: agenciesResponse.agencies });
}

export default function CapitalProject() {
return <></>;
const navigate = useNavigate();
const { capitalProject, agencies } = useLoaderData<typeof loader>();
return (
<CapitalProjectDetailPanel
capitalProject={capitalProject}
agencies={agencies}
onClose={() => {
navigate("/");
}}
/>
);
}
Loading

0 comments on commit cef1779

Please sign in to comment.