Skip to content

Commit

Permalink
feat: Radio frontend (#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
mathcolo authored Oct 31, 2024
1 parent a7fcd70 commit 60bd233
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 51 deletions.
68 changes: 46 additions & 22 deletions js/components/operatorSignIn/attestation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ export const Attestation = ({
}: {
badge: string;
employees: ApiResult<Employee[]>;
onComplete: () => void;
onComplete: (radio: string) => void;
loading: boolean;
prefill: boolean;
}): ReactElement => {
const defaultValue = prefill ? badge : "";

const [entered, setEntered] = useState<string>(defaultValue);
const ready = entered === badge;
const [enteredBadge, setEnteredBadge] = useState<string>(defaultValue);
const [enteredRadio, setEnteredRadio] = useState<string>("");
const valid = enteredBadge === badge && enteredRadio !== "";

if (employees.status === "loading") {
return <div>Loading...</div>;
Expand All @@ -47,22 +48,43 @@ export const Attestation = ({
<div className="text-sm">
Step 2 of 2
<SignInText />
<SignaturePrompt defaultValue={defaultValue} onChange={setEntered} />
<SignatureHint badge={badge} signatureText={entered} />
<p className="my-3">
By pressing the button below I, <b className="fs-mask">{name}</b>,
confirm the above is true.
</p>
<button
className={className([
"block w-full md:max-w-64 mx-auto h-10 px-5 bg-gray-500 text-gray-200 rounded-md",
(!ready || loading) && "opacity-50",
])}
onClick={onComplete}
disabled={!ready}
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
Complete Fit for Duty Check
</button>
<InputBox
title={"Operator Badge Number"}
defaultValue={defaultValue}
onChange={(value) => {
setEnteredBadge(removeLeadingZero(value));
}}
/>
<SignatureHint badge={badge} signatureText={enteredBadge} />
<InputBox
title={"Radio Number"}
defaultValue={""}
onChange={(value) => {
setEnteredRadio(value);
}}
/>
<p className="my-3">
By pressing the button below I, <b className="fs-mask">{name}</b>,
confirm the above is true.
</p>
<button
className={className([
"block w-full md:max-w-64 mx-auto h-10 px-5 bg-gray-500 text-gray-200 rounded-md",
(!valid || loading) && "opacity-50",
])}
onClick={() => {
onComplete(enteredRadio);
}}
disabled={!valid}
>
Complete Fit for Duty Check
</button>
</form>
</div>
);
};
Expand Down Expand Up @@ -129,7 +151,7 @@ const SignatureHint = ({
return (
<p
className={className([
"fs-mask mt-2 h-6 overflow-y-hidden text-sm transition-[line-height] ease-out",
"fs-mask mt-2 h-6 overflow-y-hidden text-[12px] transition-[line-height] ease-out",
hintClass,
])}
title={title}
Expand All @@ -139,17 +161,19 @@ const SignatureHint = ({
);
};

export const SignaturePrompt = ({
const InputBox = ({
onChange,
defaultValue,
title,
}: {
onChange: (value: string) => void;
defaultValue: string;
title: string;
}): ReactElement => {
return (
<div>
<label className="text-sm">
<span className="text-xs">Operator Badge Number</span>
<span className="text-xs">{title}</span>
<span className="float-right text-xxs font-semibold uppercase tracking-wide-4">
Required
</span>
Expand All @@ -160,7 +184,7 @@ export const SignaturePrompt = ({
defaultValue={defaultValue}
// Do not set `value`- we are transforming below!
onChange={(evt) => {
onChange(removeLeadingZero(evt.target.value));
onChange(evt.target.value);
}}
required
/>
Expand Down
6 changes: 5 additions & 1 deletion js/components/operatorSignIn/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const List = ({ line }: { line: HeavyRailLine }): ReactElement => {
}

return (
<table className="break-words">
<table className="break-words text-[14px]">
<colgroup>
<col className="w-1/3" />
<col className="w-1/5" />
Expand All @@ -28,6 +28,7 @@ export const List = ({ line }: { line: HeavyRailLine }): ReactElement => {
<tr className="font-semibold">
<td className="border-y md:border-x p-1">Name</td>
<td className="border-y md:border-x p-1">Badge</td>
<td className="border-y md:border-x p-1">Radio</td>
<td className="border-y md:border-x p-1">Time</td>
<td className="border-y md:border-x p-1">Official</td>
</tr>
Expand All @@ -39,6 +40,9 @@ export const List = ({ line }: { line: HeavyRailLine }): ReactElement => {
<td className="fs-mask border-y md:border-x p-1 break-all">
{si.signed_in_employee}
</td>
<td className="border-y md:border-x p-1 break-all">
{si.radio_number}
</td>
<td className="border-y md:border-x p-1">
{si.signed_in_at
.toLocaleString(DateTime.TIME_SIMPLE)
Expand Down
10 changes: 7 additions & 3 deletions js/components/operatorSignIn/operatorSignInModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum CompleteState {

const submit = (
badgeEntry: BadgeEntry,
radio: string,
setComplete: React.Dispatch<React.SetStateAction<CompleteState | null>>,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
onComplete: () => void,
Expand All @@ -35,6 +36,7 @@ const submit = (
signed_in_employee_badge: badgeEntry.number,
signed_in_at: DateTime.now().toUnixInteger(),
line: "blue",
radio_number: radio,
method: badgeEntry.method,
})
.then((response) => {
Expand Down Expand Up @@ -94,6 +96,7 @@ const OperatorSignInModalContent = ({
close: () => void;
}): ReactElement => {
const [badge, setBadge] = useState<BadgeEntry | null>(null);
const [radio, setRadio] = useState<string>("");
const [complete, setComplete] = useState<CompleteState | null>(null);

const [loading, setLoading] = useState<boolean>(false);
Expand Down Expand Up @@ -123,7 +126,7 @@ const OperatorSignInModalContent = ({
name={name}
loading={loading}
onTryAgain={() => {
submit(badge, setComplete, setLoading, onComplete);
submit(badge, radio, setComplete, setLoading, onComplete);
}}
/>
: complete === CompleteState.BADGE_SERIAL_LOOKUP_ERROR ?
Expand All @@ -147,8 +150,9 @@ const OperatorSignInModalContent = ({
prefill={badge.method === "nfc"}
badge={badge.number}
loading={loading}
onComplete={() => {
submit(badge, setComplete, setLoading, onComplete);
onComplete={(radio: string) => {
setRadio(radio);
submit(badge, radio, setComplete, setLoading, onComplete);
}}
employees={employees}
/>
Expand Down
2 changes: 1 addition & 1 deletion js/components/operatorSignIn/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const useSignInText = (): {
return {
version: 1,
text: (
<ul className="my-8 mx-5 list-disc leading-tight">
<ul className="my-7 mx-5 list-disc leading-tight">
<li>I do not have an electronic device in my possession.</li>
<li>
I am fit for duty, and I do not possess nor am I under the influence
Expand Down
1 change: 1 addition & 0 deletions js/models/signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";

export const SignIn = z.object({
rail_line: z.enum(["blue", "orange", "red"]),
radio_number: z.string().nullable(),
signed_in_at: z.string(),
signed_in_by: z.string(),
signed_in_employee: z.string(),
Expand Down
70 changes: 59 additions & 11 deletions js/test/components/operatorSignIn/attestation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,59 @@ describe("Attestation", () => {
});

describe("signature text box", () => {
test("it's there", () => {
test("it pre-fills if requested", () => {
const view = render(
<Attestation
badge="123"
prefill={false}
prefill={true}
onComplete={jest.fn()}
loading={false}
employees={EMPLOYEES}
/>,
);
expect(view.getByRole("textbox")).toBeInTheDocument();
expect(view.getByRole("textbox")).toHaveValue("");
const input = view.getByLabelText(/Operator Badge Number/i, {
selector: "input",
});
expect(input).toHaveValue("123");
});
});

test("it pre-fills if requested", () => {
describe("radio text box", () => {
test("it's there", () => {
const view = render(
<Attestation
badge="123"
prefill={true}
prefill={false}
onComplete={jest.fn()}
loading={false}
employees={EMPLOYEES}
/>,
);
expect(view.getByRole("textbox")).toHaveValue("123");
const input = view.getByLabelText(/Radio Number/i, {
selector: "input",
});
expect(input).toBeInTheDocument();
expect(input).toHaveValue("");
});
test("cannot be blank", async () => {
const onComplete = jest.fn();
const view = render(
<Attestation
badge="123"
prefill={false}
onComplete={onComplete}
loading={false}
employees={EMPLOYEES}
/>,
);
const badgeInput = view.getByLabelText(/Operator Badge Number/i, {
selector: "input",
});
await userEvent.type(badgeInput, "123");
// Leave radio field blank
expect(
view.getByRole("button", { name: "Complete Fit for Duty Check" }),
).toBeDisabled();
});
});

Expand All @@ -97,6 +125,7 @@ describe("Attestation", () => {
});

test("valid attestation", async () => {
const radioNumber = "22";
const onComplete = jest.fn();
const view = render(
<Attestation
Expand All @@ -107,13 +136,20 @@ describe("Attestation", () => {
employees={EMPLOYEES}
/>,
);
await userEvent.type(view.getByRole("textbox"), "123");
const badgeInput = view.getByLabelText(/Operator Badge Number/i, {
selector: "input",
});
const radioInput = view.getByLabelText(/Radio Number/i, {
selector: "input",
});
await userEvent.type(badgeInput, "123");
await userEvent.type(radioInput, radioNumber);
expect(view.getByText("Looks good!")).toBeInTheDocument();

await userEvent.click(
view.getByRole("button", { name: "Complete Fit for Duty Check" }),
);
expect(onComplete).toHaveBeenCalledOnce();
expect(onComplete).toHaveBeenCalledExactlyOnceWith(radioNumber);
});

test("valid attestation with leading zero", async () => {
Expand All @@ -127,7 +163,14 @@ describe("Attestation", () => {
employees={EMPLOYEES}
/>,
);
await userEvent.type(view.getByRole("textbox"), "0123");
const badgeInput = view.getByLabelText(/Operator Badge Number/i, {
selector: "input",
});
const radioInput = view.getByLabelText(/Radio Number/i, {
selector: "input",
});
await userEvent.type(badgeInput, "0123");
await userEvent.type(radioInput, "22");
expect(view.getByText("Looks good!")).toBeInTheDocument();

await userEvent.click(
Expand All @@ -150,7 +193,12 @@ describe("Attestation", () => {
/>,
);

await user.type(view.getByRole("textbox"), "4123");
await user.type(
view.getByLabelText(/Operator Badge Number/i, {
selector: "input",
}),
"4123",
);
act(() => {
jest.runAllTimers();
});
Expand Down
2 changes: 2 additions & 0 deletions js/test/components/operatorSignIn/list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jest.mock("../../../hooks/useSignIns", () => ({
result: [
{
rail_line: "blue",
radio_number: 2102,
signed_in_at: DateTime.fromISO("2024-07-22T12:45:52.000-04:00", {
zone: "America/New_York",
}),
Expand All @@ -42,6 +43,7 @@ describe("List", () => {
expect(
view.getByText(`${EMPLOYEES[0].first_name} ${EMPLOYEES[0].last_name}`),
).toBeInTheDocument();
expect(view.getByText("2102")).toBeInTheDocument();
// NB: the email below contains a soft hyphen character
expect(view.getByText("user­@example.com")).toBeInTheDocument();
});
Expand Down
Loading

0 comments on commit 60bd233

Please sign in to comment.