Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
37 changes: 37 additions & 0 deletions APIClients/ReviewPageAPIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { fetchGraphql } from "@utils/makegqlrequest";
import { mutations } from "graphql/queries";
import BaseAPIClient from "./BaseAPIClient";

export type ReportReviewConflictResult = {
readonly applicantRecordId: string;
readonly reviewerId: number;
readonly status: string;
readonly score: number;
readonly reviewerHasConflict: boolean;
};

const reportReviewConflict = async (
applicantRecordId: string,
reviewerId: number,
): Promise<ReportReviewConflictResult> => {
BaseAPIClient.handleAuthRefresh();
if (!Number.isInteger(reviewerId)) {
throw new Error("Reviewer ID is invalid");
}

const result = await fetchGraphql(mutations.reportReviewConflict, {
applicantRecordId,
reviewerId,
});
const conflictResult = result?.data?.reportReviewConflict;

if (!conflictResult) {
throw new Error("Conflict report request returned no data");
}

return conflictResult;
};

export default {
reportReviewConflict,
};
86 changes: 86 additions & 0 deletions components/common/Dialogue.tsx
Comment thread
OpsEclipse marked this conversation as resolved.
Comment thread
OpsEclipse marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { ReactElement, ReactNode, Children } from "react";
import Dialog from "@mui/material/Dialog";
import { useTheme } from "@mui/material/styles";

type Props = {
readonly open: boolean;
readonly onClose: () => void;
readonly header: string;
readonly text: ReactNode;
readonly children?: ReactNode;
readonly textContainerClassName?: string;
};

const Dialogue = ({
open,
onClose,
header,
text,
children,
textContainerClassName,
}: Props): ReactElement => {
Comment thread
OpsEclipse marked this conversation as resolved.
Outdated
const textContainerClasses =
`flex w-full flex-col items-center gap-2 text-center ${
textContainerClassName ?? ""
}`.trim();
Comment thread
OpsEclipse marked this conversation as resolved.
Outdated

const theme = useTheme();

const actionChildren = Children.toArray(children).filter(Boolean);
const hasSingleAction = actionChildren.length === 1;
const actionsContainerClasses = hasSingleAction
? "flex w-full items-center justify-center"
: "flex w-full items-center justify-center gap-4";

const actionWrapperClass = hasSingleAction ? "w-full" : "flex-1";
Comment thread
OpsEclipse marked this conversation as resolved.
Outdated

return (
<Dialog
open={open}
onClose={onClose}
maxWidth={false}
PaperProps={{
sx: {
borderRadius: 2,
overflow: "hidden",
boxShadow: "none",
opacity: 1,
},
}}
>
<div
className="flex flex-col justify-center items-center gap-[36px] p-6"
style={{
width: "310px",
backgroundColor: theme.palette.background.paper,
}}
>
<div className={textContainerClasses}>
<h2
className="w-full font-poppins text-[20px] font-medium leading-[1.4]"
style={{ color: theme.palette.primary.main }}
>
{header}
</h2>
<div
className="w-full font-source text-[14px] font-normal leading-[1.4]"
style={{ color: theme.palette.text.primary }}
>
{text}
</div>
</div>
{actionChildren.length > 0 ? (
<div className={actionsContainerClasses}>
{actionChildren.map((child, idx) => (
<div className={actionWrapperClass} key={idx}>
Comment thread
OpsEclipse marked this conversation as resolved.
Outdated
{child}
</div>
))}
</div>
) : null}
</div>
</Dialog>
Comment thread
OpsEclipse marked this conversation as resolved.
);
};

export default Dialogue;
31 changes: 6 additions & 25 deletions components/common/SplitPageLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { LongLeftIcon } from "@components/icons/long-left.icon";
Comment thread
SaqAsh marked this conversation as resolved.
import { useTheme } from "@mui/material/styles";
import Link from "next/link";
import { ReactElement, ReactNode } from "react";

type SplitRatio = "equal";
Expand Down Expand Up @@ -60,10 +58,8 @@ interface PanelLayoutProps {
borderLeft?: boolean;
/** "xlarge" = left panel (28px, 600), "medium" = right panel (20px, 500) */
titleVariant?: "xlarge" | "medium";
/** Renders pill-shaped "Back to home" link with arrow icon */
backToHomeHref?: string;
/** Optional action (e.g. Report button) shown on the right of the Back to home row on the left panel */
headerRightAction?: ReactNode;
Comment thread
SaqAsh marked this conversation as resolved.
/** Optional top row above title (e.g. back link + actions) */
header?: ReactNode;
/** When false, hides subtitle (e.g. for INFO stage) */
showApplicationTitle?: boolean;
contentClassName?: string;
Expand All @@ -79,8 +75,7 @@ export const PanelLayout = ({
borderRight = false,
borderLeft = false,
titleVariant = "xlarge",
backToHomeHref,
headerRightAction,
header,
showApplicationTitle = true,
contentClassName,
children,
Expand Down Expand Up @@ -121,25 +116,11 @@ export const PanelLayout = ({
}}
>
<div className="flex flex-col h-full overflow-hidden px-9 py-8">
{(backToHomeHref || headerRightAction) && (
{header ? (
<div className="flex justify-between items-center w-full mb-8 shrink-0 gap-4">
{backToHomeHref ? (
<Link href={backToHomeHref} passHref>
<a className="font-source no-underline inline-flex justify-center items-center gap-2 w-fit cursor-pointer shrink-0 hover:opacity-90 rounded-full py-2 px-4 border-2 border-blue bg-white text-blue text-base font-normal leading-[1.4] hover:bg-sky-100 hover:border-blue hover:text-blue">
<LongLeftIcon />
Back to home
</a>
</Link>
) : (
<span />
)}
{headerRightAction != null ? (
<div className="shrink-0 flex items-center">
{headerRightAction}
</div>
) : null}
{header}
</div>
Comment thread
OpsEclipse marked this conversation as resolved.
)}
) : null}
{showApplicationTitle && subtitle && (
<p
className="font-poppins text-charcoal-500 mb-4 shrink-0 text-[15px]"
Expand Down
12 changes: 10 additions & 2 deletions components/interview/nav/InterviewNavPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ interface InterviewNavPanelProps {
candidateName: string;
}

export const InterviewNavPanel = ({ candidateName }: InterviewNavPanelProps) => {
export const InterviewNavPanel = ({
candidateName,
}: InterviewNavPanelProps) => {
const router = useRouter();
const theme = useTheme();

Expand Down Expand Up @@ -38,7 +40,13 @@ export const InterviewNavPanel = ({ candidateName }: InterviewNavPanelProps) =>
const active = isActive(item.path);
return (
<li key={item.step}>
<Link href={interviewedApplicantRecordId ? `${item.path}?interviewedApplicantRecordId=${interviewedApplicantRecordId}` : item.path}>
<Link
href={
interviewedApplicantRecordId
? `${item.path}?interviewedApplicantRecordId=${interviewedApplicantRecordId}`
: item.path
}
>
<a
className={`flex items-center justify-between self-stretch rounded-lg px-5 py-2.5 hover:bg-[#F1F1F1] ${
active ? "bg-[#F1F1F1] font-semibold" : "font-normal"
Expand Down
6 changes: 3 additions & 3 deletions components/interview/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NavItem, HeaderStepConfig } from "./types";

Comment thread
mxc-maggiechen marked this conversation as resolved.
export const InterviewStep = {
PROFILE : "PROFILE",
ASSESSMENT : "ASSESSMENT",
REPORT : "REPORT",
PROFILE: "PROFILE",
ASSESSMENT: "ASSESSMENT",
REPORT: "REPORT",
} as const;

export const InterviewHeaderStep = {
Expand Down
125 changes: 125 additions & 0 deletions components/review/shared/ConflictDialogue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Button from "@components/common/Button";
import Dialogue from "@components/common/Dialogue";
import ReviewPageAPIClient from "APIClients/ReviewPageAPIClient";
import { useTheme } from "@mui/material/styles";
import { useRouter } from "next/router";
import { ReactElement, useState } from "react";
import { BACK_TO_HOME_HREF } from "./constants";

type Props = {
readonly applicantRecordId: string | null;
readonly reviewerId: number | null;
readonly confirmOpen: boolean;
readonly onConfirmOpenChange: (open: boolean) => void;
};

const ConflictDialogue = ({
applicantRecordId,
reviewerId,
confirmOpen,
onConfirmOpenChange,
}: Props): ReactElement => {
const theme = useTheme();
const router = useRouter();

const [successDialogOpen, setSuccessDialogOpen] = useState(false);
const [isReporting, setIsReporting] = useState(false);
const [reportError, setReportError] = useState<string | null>(null);

const handleCloseConfirmDialog = () => {
if (isReporting) {
return;
}
setReportError(null);
onConfirmOpenChange(false);
};

const handleBackToHomepage = () => {
setSuccessDialogOpen(false);
router.push(`/${BACK_TO_HOME_HREF}`);
};

const handleReportConflict = async () => {
try {
setIsReporting(true);
setReportError(null);

if (applicantRecordId == null) {
throw new Error("Missing applicantRecordId in URL");
}

if (reviewerId == null || !Number.isInteger(reviewerId)) {
throw new Error("Missing authenticated reviewer ID");
}

await ReviewPageAPIClient.reportReviewConflict(
applicantRecordId,
reviewerId,
);

onConfirmOpenChange(false);
setSuccessDialogOpen(true);
} catch (error) {
setReportError("Couldn't report conflict. Please try again.");
} finally {
setIsReporting(false);
}
};

return (
Comment thread
OpsEclipse marked this conversation as resolved.
Outdated
<>
<Dialogue
open={confirmOpen}
onClose={handleCloseConfirmDialog}
header="Report as conflict of interest?"
text={
<div className="flex flex-col gap-2">
<span>Clicking yes will notify admins and cannot be undone.</span>
{reportError ? (
<span style={{ color: theme.palette.error.main }} role="alert">
{reportError}
</span>
) : null}
</div>
}
>
<Button
variant="secondary"
size="md"
onClick={handleCloseConfirmDialog}
className="whitespace-nowrap"
disabled={isReporting}
>
Cancel
</Button>
<Button
variant="primary"
size="md"
onClick={handleReportConflict}
className="whitespace-nowrap"
disabled={isReporting}
>
Yes, report
</Button>
</Dialogue>

<Dialogue
open={successDialogOpen}
onClose={() => setSuccessDialogOpen(false)}
header="Conflict reported!"
text="This applicant has been reported as a conflict of interest and will be re-assigned to another reviewer."
>
<Button
variant="primary"
size="md"
onClick={handleBackToHomepage}
className="whitespace-nowrap"
>
Back to homepage
</Button>
</Dialogue>
</>
);
};

export default ConflictDialogue;
27 changes: 27 additions & 0 deletions components/review/shared/ReviewStageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { LongLeftIcon } from "@components/icons/long-left.icon";
import Link from "next/link";
import { ReactNode } from "react";

interface ReviewStageHeaderProps {
backHref: string;
right?: ReactNode;
}

export const ReviewStageHeader = ({
backHref,
right,
}: ReviewStageHeaderProps) => {
return (
<>
<Link href={backHref} passHref>
<a className="font-source no-underline inline-flex justify-center items-center gap-2 w-fit cursor-pointer shrink-0 hover:opacity-90 rounded-full py-2 px-4 border-2 border-blue bg-white text-blue text-base font-normal leading-[1.4] hover:bg-sky-100 hover:border-blue hover:text-blue">
<LongLeftIcon />
Back to home
</a>
</Link>
{right != null ? (
<div className="shrink-0 flex items-center">{right}</div>
) : null}
</>
);
};
Loading