Skip to content

Commit

Permalink
feat: frontend expiring certification warnings and sign-in prevention
Browse files Browse the repository at this point in the history
  • Loading branch information
mathcolo committed Jan 2, 2025
1 parent fc721e1 commit 614450e
Show file tree
Hide file tree
Showing 15 changed files with 925 additions and 74 deletions.
130 changes: 70 additions & 60 deletions js/components/operatorSignIn/attestation.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,100 @@
import { ApiResult } from "../../api";
import { reload } from "../../browser";
import { useNow } from "../../dateTime";
import { lookupDisplayName } from "../../hooks/useEmployees";
import { anyOfExpired, Certification } from "../../models/certification";
import { Employee } from "../../models/employee";
import { className } from "../../util/dom";
import { removeLeadingZero } from "../../util/string";
import { Bypass, CertificateBoxes, Instructions } from "./expiry";
import { useSignInText } from "./text";
import { ReactElement, useEffect, useState } from "react";

export const Attestation = ({
badge,
employees,
certifications,
onComplete,
loading,
prefill,
}: {
badge: string;
employees: ApiResult<Employee[]>;
employees: Employee[];
certifications: Certification[];
onComplete: (radio: string) => void;
loading: boolean;
prefill: boolean;
}): ReactElement => {
const defaultValue = prefill ? badge : "";
const now = useNow("second");

const [enteredBadge, setEnteredBadge] = useState<string>(defaultValue);
const [enteredRadio, setEnteredRadio] = useState<string>("");
const valid = enteredBadge === badge && enteredRadio !== "";
const [bypass, setBypass] = useState<boolean>(false);

const name = lookupDisplayName(badge, employees);
const expired = anyOfExpired(certifications, now);

if (employees.status === "loading") {
return <div>Loading...</div>;
} else if (employees.status === "error") {
return (
<div className="text-center">
<div className="mb-4">Unable to download employee data</div>
<div>
<button
className="rounded bg-blue text-gray-200 w-1/4 max-w-20"
onClick={reload}
>
Reload
</button>
</div>
</div>
);
}
const name = lookupDisplayName(badge, employees.result);
return (
<div className="text-sm">
Step 2 of 2
<SignInText />
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<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>
<CertificateBoxes
certifications={certifications}
displayName={name}
ignoreExpired={bypass}
now={now}
/>
{!expired || bypass ?
<>
<SignInText />
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<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>
</>
: <>
<Instructions displayName={name} />
<Bypass
certifications={certifications}
displayName={name}
now={now}
onContinue={function (): void {
setBypass(true);
}}
/>
</>
}
</div>
);
};
Expand Down
163 changes: 163 additions & 0 deletions js/components/operatorSignIn/expiry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { daysBetween } from "../../dateTime";
import {
Certification,
humanReadableType,
isExpired,
} from "../../models/certification";
import { className } from "../../util/dom";
import { WarningParagraph } from "./warningParagraph";
import { DateTime } from "luxon";
import { ReactElement } from "react";

const WARN_WITHIN_D = 60;

const englishDaysBetween = (now: DateTime, date: DateTime) => {
const delta = daysBetween(now, date);
const wholeAbs = Math.abs(Math.ceil(delta));
if (delta <= -2) {
return `expired ${wholeAbs} days ago`;
} else if (delta <= -1) {
return "expired yesterday";
} else if (delta <= 0) {
return "expired today";
} else if (delta <= 1) {
return "expires tomorrow";
} else {
return `expires in ${wholeAbs} days`;
}
};

const CertificateBox = ({
now,
mode,
title,
operatorName,
certifications,
}: {
now: DateTime;
mode: "warning" | "error";
title: string;
operatorName: string;
certifications: Certification[];
}): ReactElement => {
const innerString =
`Our records show that ${operatorName}'s ` +
certifications
.map(
(c) =>
`${humanReadableType(c.type)} ${englishDaysBetween(
now,
c.expires,
)} on ${c.expires.toLocaleString(DateTime.DATE_SHORT)}`,
)
.join(" and ") +
".";
return (
<WarningParagraph
className={className([
"border-0 light:bg-opacity-40 dark:bg-opacity-30 dark:text-white",
mode === "warning" && "bg-[#FFDE9E]",
mode === "error" && "bg-[#FF919A]",
])}
>
<p className="font-bold uppercase">{title}</p>
<p className="mt-2">{innerString}</p>
{mode === "warning" && (
<p className="mt-2">Please have them call the Office.</p>
)}
</WarningParagraph>
);
};

export const CertificateBoxes = ({
now,
displayName,
ignoreExpired,
certifications,
}: {
now: DateTime;
displayName: string;
ignoreExpired: boolean;
certifications: Certification[];
}): ReactElement => {
const expired = certifications.filter((cert) => isExpired(cert, now));
const expiresSoon = certifications.filter((cert) => {
const delta = daysBetween(now, cert.expires);
return !isExpired(cert, now) && delta <= WARN_WITHIN_D;
});
return (
<>
{expiresSoon.length > 0 && (
<CertificateBox
now={now}
mode="warning"
title={
expiresSoon.length === 2 ? "Cards expire soon" : "Card expires soon"
}
operatorName={displayName}
certifications={expiresSoon}
/>
)}
{expired.length > 0 && !ignoreExpired && (
<CertificateBox
now={now}
mode="error"
title={expired.length === 2 ? "Expired cards" : "Expired card"}
operatorName={displayName}
certifications={expired}
/>
)}
</>
);
};

export const Instructions = ({
displayName,
}: {
displayName: string;
}): ReactElement => {
return (
<ol className="m-8 mr-0 list-decimal">
<li>Do not allow {displayName} to drive.</li>
<li>Call the Office.</li>
<li>Send {displayName} to the Supervisors&#39; Office.</li>
</ol>
);
};
export const Bypass = ({
displayName,
certifications,
now,
onContinue,
}: {
displayName: string;
certifications: Certification[];
now: DateTime;
onContinue: () => void;
}): ReactElement => {
const expireds = certifications.filter((c) => isExpired(c, now));

return (
<>
<hr className="h-[2px] bg-gray-300" />
<div className="m-2 text-sm text-gray-400">
<p>Is this warning incorrect?</p>
<span>
If {displayName} has a valid{" "}
{expireds.map((c) => humanReadableType(c.type)).join(" and ")}:
</span>
<ol className="mb-4 ml-10 mr-0 mt-2 list-decimal">
<li className="m-0">
Take a picture of{" "}
{expireds.length === 2 ? "both cards" : "the card"}.
</li>
<li className="m-0">Email pictures to supervisors.</li>
<li className="m-0">Continue to Fit for Duty Check.</li>
</ol>
<button className="underline" onClick={onContinue}>
Continue to Fit for Duty Check →
</button>
</div>
</>
);
};
Loading

0 comments on commit 614450e

Please sign in to comment.