generated from NYCPlanning/ae-remix-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
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…
…roject
- Loading branch information
1 parent
62a9766
commit cef1779
Showing
7 changed files
with
1,240 additions
and
2,550 deletions.
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
app/components/CapitalProjectDetailPanel/CapitalProjectDetailPanel.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
181
app/components/CapitalProjectDetailPanel/CapitalProjectDetailPanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { CapitalProjectDetailPanel } from './CapitalProjectDetailPanel'; |
40 changes: 39 additions & 1 deletion
40
app/routes/capital-projects.$managingCode.$capitalProjectId.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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("/"); | ||
}} | ||
/> | ||
); | ||
} |
Oops, something went wrong.