Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
TylerMatteo committed Jul 8, 2024
1 parent 62a9766 commit 8895640
Showing 7 changed files with 1,246 additions and 2,556 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 8895640

Please sign in to comment.