Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Radio frontend #164

Merged
merged 15 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
mathcolo marked this conversation as resolved.
Show resolved Hide resolved
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
mathcolo marked this conversation as resolved.
Show resolved Hide resolved
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional refactoring idea: I remember there was a thing about using form onSubmit instead of the button onClick for something else recently. Would it be better to make this button a submit button and do this call in the form's onSubmit handler instead too?

}}
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>("");
mathcolo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading