Skip to content

Commit

Permalink
Add Work Order page to NYCHA LOC flow (#2466)
Browse files Browse the repository at this point in the history
* [sc-15698] add RAD/PACT to housing type options and run migrations

* [sc-15701] add routes

* testing

* add work order field

* add work order mutation

* update error state languate

* use SessionFormMutation instead of DjangoFormMutation to have work order ticket vars persist across session

* fix failing frontend test

* linter

* add backend tests

* add frontend test

* migrate minor change, fix a bug of 0 appearing in final letter
  • Loading branch information
kiwansim authored Jan 6, 2025
1 parent b07a0b9 commit 547ac93
Show file tree
Hide file tree
Showing 29 changed files with 893 additions and 46 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ workflows:
filters:
branches:
only:
- nycha-work-order
- master
only_deploy:
jobs:
Expand Down
2 changes: 1 addition & 1 deletion common-data/lease-choices.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
["RENT_CONTROLLED", "Rent Controlled"],
["OTHER_AFFORDABLE", "Affordable housing (other than rent-stabilized)"],
["MARKET_RATE", "Market Rate"],
["NYCHA", "NYCHA/Public Housing"],
["NYCHA", "NYCHA/Public Housing (includes RAD/PACT)"],
["NOT_SURE", "I'm not sure"],
["NO_LEASE", "I don't have a lease"],
[
Expand Down
2 changes: 1 addition & 1 deletion common-data/lease-choices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function getLeaseChoiceLabels(): LeaseChoiceLabels {
RENT_CONTROLLED: "Rent Controlled",
OTHER_AFFORDABLE: "Affordable housing (other than rent-stabilized)",
MARKET_RATE: "Market Rate",
NYCHA: "NYCHA/Public Housing",
NYCHA: "NYCHA/Public Housing (includes RAD/PACT)",
NOT_SURE: "I'm not sure",
NO_LEASE: "I don't have a lease",
RENT_STABILIZED_OR_CONTROLLED: "Either Rent Stabilized or Rent Controlled (legacy option)",
Expand Down
2 changes: 1 addition & 1 deletion frontend/lib/forms/form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class BaseFormContext<FormInput> {
*/
protected readonly fieldPropsRequested = new Set<string>();

constructor(protected readonly options: BaseFormContextOptions<FormInput>) {
constructor(readonly options: BaseFormContextOptions<FormInput>) {
this.isLoading = options.isLoading;
}

Expand Down
29 changes: 28 additions & 1 deletion frontend/lib/loc/letter-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { AllSessionInfo } from "../queries/AllSessionInfo";
import { issuesForArea, customIssuesForArea } from "../issues/issues";
import { formatPhoneNumber } from "../forms/phone-number-form-field";
import { TransformSession } from "../util/transform-session";
import { LeaseType } from "../queries/globalTypes";

const HEAT_ISSUE_CHOICES = new Set<IssueChoice>([
"HOME__NO_HEAT",
Expand All @@ -41,6 +42,7 @@ type LocContentProps = BaseLetterContentProps & {
issues: AreaIssues[];
accessDates: GraphQLDate[];
hasCalled311: boolean | null;
workOrderTickets?: string[] | null;
};

const LetterTitle: React.FC<LocContentProps> = (props) => (
Expand Down Expand Up @@ -99,6 +101,21 @@ const AccessDates: React.FC<LocContentProps> = (props) => (
</div>
);

const WorkOrderTickets: React.FC<LocContentProps> = (props) => (
<div className="jf-avoid-page-breaks-within">
<h2>Work Order Repair Tickets</h2>
<p>
I have documented these issues in the past by submitting work tickets to
management. I've included at least one work ticket(s) for your reference:
</p>
<ul>
{props.workOrderTickets?.map((ticket) => (
<li key={ticket}>{ticket}</li>
))}
</ul>
</div>
);

function hasHeatIssues(issues: AreaIssues[]): boolean {
return issues.some((areaIssues) =>
areaIssues.issues.some(
Expand Down Expand Up @@ -143,6 +160,7 @@ const LetterBody: React.FC<LocContentProps> = (props) => (
{props.accessDates.length > 0 && <AccessDates {...props} />}
<Requirements {...props} />
{props.hasCalled311 && <PreviousReliefAttempts />}
{!!props.workOrderTickets?.length && <WorkOrderTickets {...props} />}
</>
);

Expand Down Expand Up @@ -238,12 +256,21 @@ export function getLocContentPropsFromSession(
return null;
}

return {
const sessionProps = {
...baseProps,
issues: getIssuesFromSession(session),
accessDates: session.accessDates,
hasCalled311: onb.hasCalled311,
};

if (onb.leaseType === LeaseType.NYCHA) {
return {
...sessionProps,
workOrderTickets: session.workOrderTickets,
};
}

return sessionProps;
}

export const LocForUserPage: React.FC<{ isPdf: boolean }> = ({ isPdf }) => (
Expand Down
2 changes: 1 addition & 1 deletion frontend/lib/loc/letter-of-complaint-splash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const createLeaseLearnMoreModals = (): LeaseMoreInfo[] => [
landlord deregulated before 2019.",
},
{
title: "NYCHA/Public Housing",
title: "NYCHA/Public Housing (includes RAD/PACT)",
leaseType: "NYCHA",
leaseInfo:
"Federally-funded affordable housing developments owned by the government.",
Expand Down
1 change: 1 addition & 0 deletions frontend/lib/loc/route-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function createLetterOfComplaintRouteInfo(prefix: string) {
issues: createIssuesRouteInfo(`${prefix}/issues`),
accessDates: `${prefix}/access-dates`,
reliefAttempts: `${prefix}/relief-attempts`,
workOrders: `${prefix}/work-orders`,
yourLandlord: `${prefix}/your-landlord`,
preview: `${prefix}/preview`,
previewSendConfirmModal: `${prefix}/preview/send-confirm-modal`,
Expand Down
8 changes: 7 additions & 1 deletion frontend/lib/loc/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { LocSplash } from "./letter-of-complaint-splash";
import { GetStartedButton } from "../ui/get-started-button";
import { OnboardingInfoSignupIntent } from "../queries/globalTypes";
import ReliefAttemptsPage from "../onboarding/relief-attempts";
import { isUserNycha } from "../util/nycha";
import { isUserNonNycha, isUserNycha } from "../util/nycha";
import { createJustfixCrossSiteVisitorSteps } from "../justfix-cross-site-visitor-routes";
import { ProgressStepProps } from "../progress/progress-step-route";
import { assertNotNull } from "@justfixnyc/util";
import { Switch, Route } from "react-router-dom";
import { LocSamplePage, LocForUserPage } from "./letter-content";
import { createLetterStaticPageRoutes } from "../static-page/routes";
import { NycUsersOnly } from "../pages/nyc-users-only";
import WorkOrdersPage from "./work-orders";

export const Welcome: React.FC<ProgressStepProps> = (props) => {
const session = useContext(AppContext).session;
Expand Down Expand Up @@ -120,6 +121,11 @@ export const getLOCProgressRoutesProps = (): ProgressRoutesProps => ({
component: ReliefAttemptsPage,
shouldBeSkipped: isUserNycha,
},
{
path: JustfixRoutes.locale.loc.workOrders,
component: WorkOrdersPage,
shouldBeSkipped: isUserNonNycha,
},
{
path: JustfixRoutes.locale.loc.yourLandlord,
exact: true,
Expand Down
21 changes: 21 additions & 0 deletions frontend/lib/loc/tests/access-dates.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ describe("access dates page", () => {
});
});

describe("access dates page", () => {
it("redirects NYCHA users to Work Order step after successful submission", async () => {
const pal = new AppTesterPal(<LetterOfComplaintRoutes />, {
url: JustfixRoutes.locale.loc.accessDates,
session: newSb().withLoggedInNychaJustfixUser().value,
});

pal.fillFirstFormField([/Date/i, "2018-01-02"]);
pal.clickButtonOrLink("Next");
pal.withFormMutation(AccessDatesMutation).respondWith({
errors: [],
session: { accessDates: ["2018-01-02"] },
});

await pal.rt.waitFor(() => pal.rr.getByText(/Work order repairs ticket/i));
const { mock } = pal.appContext.updateSession;
expect(mock.calls).toHaveLength(1);
expect(mock.calls[0][0]).toEqual({ accessDates: ["2018-01-02"] });
});
});

test("getInitialState() works", () => {
const BLANK = { date1: "", date2: "", date3: "" };
const date1 = "2018-01-02";
Expand Down
79 changes: 79 additions & 0 deletions frontend/lib/loc/tests/work-orders.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";

import JustfixRoutes from "../../justfix-route-info";
import LetterOfComplaintRoutes from "../routes";
import { AppTesterPal } from "../../tests/app-tester-pal";
import { newSb } from "../../tests/session-builder";
import { WorkOrderTicketsMutation } from "../../queries/WorkOrderTicketsMutation";

describe("work order tickets page", () => {
it("redirects to next step after submitting valid work order tickets", async () => {
const pal = new AppTesterPal(<LetterOfComplaintRoutes />, {
url: JustfixRoutes.locale.loc.workOrders,
session: newSb().withLoggedInNychaJustfixUser().value,
});

pal.fillFirstFormField([/Work order ticket number/i, "ABCDE12345"]);
pal.clickButtonOrLink("Next");
pal.withFormMutation(WorkOrderTicketsMutation).respondWith({
errors: [],
session: { workOrderTickets: ["ABCDE12345"] },
});

await pal.rt.waitFor(() => pal.rr.getByText(/Landlord information/i));
const { mock } = pal.appContext.updateSession;
expect(mock.calls).toHaveLength(1);
expect(mock.calls[0][0]).toEqual({ workOrderTickets: ["ABCDE12345"] });
});
});

describe("work order tickets page", () => {
it("redirects to next step after selecting `I don't have a ticket number`", async () => {
const pal = new AppTesterPal(<LetterOfComplaintRoutes />, {
url: JustfixRoutes.locale.loc.workOrders,
session: newSb().withLoggedInNychaJustfixUser().value,
});
pal.clickRadioOrCheckbox("I don't have a ticket number");
pal.clickButtonOrLink("Next");

pal.withFormMutation(WorkOrderTicketsMutation).respondWith({
errors: [],
session: { workOrderTickets: [] },
});

await pal.rt.waitFor(() => pal.rr.getByText(/Landlord information/i));
const { mock } = pal.appContext.updateSession;
expect(mock.calls).toHaveLength(1);
expect(mock.calls[0][0]).toEqual({ workOrderTickets: [] });
});
});

describe("work order tickets page", () => {
it("displays error message if no ticket numbers are entered", async () => {
const pal = new AppTesterPal(<LetterOfComplaintRoutes />, {
url: JustfixRoutes.locale.loc.workOrders,
session: newSb().withLoggedInNychaJustfixUser().value,
});

pal.clickButtonOrLink("Next");
pal.withFormMutation(WorkOrderTicketsMutation).respondWith({
errors: [
{
field: "__all__",
extendedMessages: [
{
message:
"Enter at least 1 ticket number or select `I don't have a ticket number.`",
code: null,
},
],
},
],
session: null,
});

await pal.rt.waitFor(() =>
pal.rr.getByText(/Enter at least 1 ticket number/i)
);
});
});
89 changes: 89 additions & 0 deletions frontend/lib/loc/work-orders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from "react";

import Page from "../ui/page";
import { SessionUpdatingFormSubmitter } from "../forms/session-updating-form-submitter";
import { WorkOrderTicketsMutation } from "../queries/WorkOrderTicketsMutation";
import { TextualFormField, CheckboxFormField } from "../forms/form-fields";
import { ProgressButtons } from "../ui/buttons";
import { MiddleProgressStep } from "../progress/progress-step-route";
import { Formset } from "../forms/formset";
import { WorkOrderTicketsInput } from "../queries/globalTypes";

const MAX_TICKETS: number = 10;

function ticketNumberLabel(i: number): string {
let label: string = "Work order ticket number";
return i > 0 ? label + ` #${i + 1}` : label;
}

function getInitialState(
ticketNumbers: string[] | null
): WorkOrderTicketsInput {
if (!ticketNumbers) {
return {
ticketNumbers: [],
noTicket: false, // so that checkbox is not selected on load
};
}
return {
ticketNumbers: ticketNumbers.map((item) => ({ ticketNumber: item })),
noTicket: ticketNumbers.length == 0,
};
}

const WorkOrdersPage = MiddleProgressStep((props) => {
return (
<Page title="Work order repairs ticket">
<div>
<h1 className="title is-4 is-spaced">Work order repairs ticket</h1>
<p className="subtitle is-6">
Enter at least one work ticket number. We’ll include these in your
letter so management can see the issues you’ve already reported.{" "}
</p>
<SessionUpdatingFormSubmitter
mutation={WorkOrderTicketsMutation}
initialState={(session) => getInitialState(session.workOrderTickets)}
onSuccessRedirect={props.nextStep}
>
{(ctx) => (
<>
<Formset
{...ctx.formsetPropsFor("ticketNumbers")}
maxNum={MAX_TICKETS}
emptyForm={{ ticketNumber: "" }}
>
{(formsetCtx, i) => (
<TextualFormField
label={ticketNumberLabel(i)}
{...formsetCtx.fieldPropsFor("ticketNumber")}
isDisabled={ctx.options.currentState.noTicket}
/>
)}
</Formset>
{ctx.options.currentState.ticketNumbers.length == MAX_TICKETS && (
<p>
The maximum number of tickets you can enter is {MAX_TICKETS}.
</p>
)}
<CheckboxFormField
{...ctx.fieldPropsFor("noTicket")}
onChange={(value) => {
ctx.options.setField("noTicket", value);
ctx.options.setField("ticketNumbers", []);
}}
>
I don't have a ticket number
</CheckboxFormField>
<ProgressButtons
back={props.prevStep}
isLoading={ctx.isLoading}
/>
</>
)}
</SessionUpdatingFormSubmitter>
</div>
</Page>
);
});

export default WorkOrdersPage;
2 changes: 1 addition & 1 deletion frontend/lib/onboarding/onboarding-step-3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export const createLeaseLearnMoreModals = (
route: routes.step3LearnMoreModals.NYCHA,
leaseType: "NYCHA",
component: () => (
<LeaseLearnMoreModal title="What is NYCHA or Public Housing?">
<LeaseLearnMoreModal title="What is NYCHA, Public Housing, and RAD/PACT?">
<p>
Federally-funded affordable housing developments owned by the
government.
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/queries/autogen-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ sessionFields = ["issues", "customIssuesV2"]
[mutations.accessDates]
sessionFields = ["accessDates"]

[mutations.workOrderTickets]
sessionFields = ["workOrderTickets"]

[mutations.landlordDetailsV2]
sessionFields = ["landlordDetails"]

Expand Down
1 change: 1 addition & 0 deletions frontend/lib/queries/autogen/AllSessionInfo.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fragment AllSessionInfo on SessionInfo {
norentLettersSent,
norentUpcomingLetterRentPeriods,
accessDates,
workOrderTickets,
landlordDetails { ...LandlordDetailsType },
letterRequest {
mailChoice,
Expand Down
9 changes: 9 additions & 0 deletions frontend/lib/queries/autogen/WorkOrderTicketsMutation.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This file was auto-generated by querybuilder, please do not edit it.
mutation WorkOrderTicketsMutation($input: WorkOrderTicketsInput!) {
output: workOrderTickets(input: $input) {
errors { ...ExtendedFormFieldErrors },
session {
workOrderTickets
}
}
}
Loading

0 comments on commit 547ac93

Please sign in to comment.