Skip to content

Commit

Permalink
feat(interview): add feedback tag (#2276)
Browse files Browse the repository at this point in the history
* feat(interview): add feedback tag

* fix: add old feedback decision tags

---------

Co-authored-by: artsiom aliakseyenka <[email protected]>
  • Loading branch information
aaliakseyenka and artsiom aliakseyenka authored Aug 30, 2023
1 parent 54c9df4 commit bf3d231
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 30 deletions.
13 changes: 10 additions & 3 deletions client/src/components/Profile/PreScreeningIviewCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Typography, List, Button, Tag } from 'antd';
import { Typography, List, Button, Tag, Row } from 'antd';
import CommonCard from './CommonCard';
import { StageInterviewDetailedFeedback } from 'common/models/profile';
import { formatDate } from 'services/formatter';
Expand All @@ -9,7 +9,8 @@ import PreScreeningIviewModal from './PreScreeningIviewModal';
const { Text } = Typography;

import { QuestionCircleOutlined, FullscreenOutlined } from '@ant-design/icons';
import { getRating } from 'domain/interview';
import { DecisionTag, getRating } from 'domain/interview';
import { Decision } from 'data/interviews/technical-screening';

type Props = {
data: StageInterviewDetailedFeedback[];
Expand Down Expand Up @@ -53,12 +54,18 @@ class PreScreeningIviewsCard extends React.PureComponent<Props, State> {
<List
itemLayout="horizontal"
dataSource={interviews}
renderItem={({ courseName, interviewer, score, maxScore, date, isGoodCandidate, version }, idx) => (
renderItem={(
{ courseName, interviewer, score, maxScore, date, isGoodCandidate, version, decision },
idx,
) => (
<List.Item style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ flexGrow: 2 }}>
<p style={{ marginBottom: 0 }}>
<Text strong>{courseName}</Text>
</p>
<Row>
<DecisionTag decision={decision as Decision} />
</Row>
<Rating rating={getRating(score, maxScore, version)} />
<p style={{ fontSize: 12, marginBottom: 5 }}>Date: {formatDate(date)}</p>
{isGoodCandidate != null ? (
Expand Down
7 changes: 5 additions & 2 deletions client/src/components/Profile/PreScreeningIviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Rating } from 'components/Rating';
import { LegacyFeedback, StageInterviewDetailedFeedback } from 'common/models/profile';
import { LegacyScreeningFeedback } from './LegacyScreeningFeedback';
import { PrescreeningFeedback } from './PrescreeningFeedback';
import { getRating } from 'domain/interview';
import { DecisionTag, getRating } from 'domain/interview';
import { Decision } from 'data/interviews/technical-screening';

type Props = {
interviewResult: StageInterviewDetailedFeedback;
Expand All @@ -16,7 +17,8 @@ type Props = {
class PreScreeningIviewModal extends React.PureComponent<Props> {
render() {
const { interviewResult, isVisible, onHide } = this.props;
const { courseFullName, date, score, interviewer, isGoodCandidate, feedback, version, maxScore } = interviewResult;
const { courseFullName, date, score, interviewer, isGoodCandidate, feedback, version, maxScore, decision } =
interviewResult;
return (
<Modal
title={`${courseFullName} Pre-Screening Interview Feedback`}
Expand All @@ -25,6 +27,7 @@ class PreScreeningIviewModal extends React.PureComponent<Props> {
footer={null}
width={'80%'}
>
<DecisionTag decision={decision as Decision} />
<Rating rating={getRating(score, maxScore, version)} />
<p style={{ marginBottom: 5 }}>Date: {formatDate(date)}</p>
{isGoodCandidate != null ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ exports[`PreScreeningIviewCard Should render correctly 1`] = `
</strong>
</span>
</p>
<div
class="ant-row css-dev-only-do-not-override-w8mnev"
>
<span
class="ant-tag ant-tag-green css-dev-only-do-not-override-w8mnev"
>
Completed
</span>
</div>
<ul
class="ant-rate css-dev-only-do-not-override-w8mnev ant-rate-disabled"
role="radiogroup"
Expand Down Expand Up @@ -454,6 +463,15 @@ exports[`PreScreeningIviewCard Should render correctly 1`] = `
</strong>
</span>
</p>
<div
class="ant-row css-dev-only-do-not-override-w8mnev"
>
<span
class="ant-tag ant-tag-green css-dev-only-do-not-override-w8mnev"
>
Completed
</span>
</div>
<ul
class="ant-rate css-dev-only-do-not-override-w8mnev ant-rate-disabled"
role="radiogroup"
Expand Down
35 changes: 35 additions & 0 deletions client/src/domain/interview.ts → client/src/domain/interview.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Tag } from 'antd';
import { StageInterviewFeedbackVerdict, InterviewDetails as CommonInterviewDetails } from 'common/models';
import { Decision } from 'data/interviews/technical-screening';
import dayjs from 'dayjs';
Expand Down Expand Up @@ -100,3 +101,37 @@ export function getRating(score: number, maxScore: number, feedbackVersion: numb
const rating = (score / maxScore) * 5;
return rating;
}

export function DecisionTag({ decision, status }: { decision?: Decision; status?: InterviewStatus }) {
if (!decision) {
return (
<Tag color={status === InterviewStatus.Completed ? 'green' : undefined}>
{status === InterviewStatus.Completed ? 'Completed' : 'Uncompleted'}
</Tag>
);
}

switch (decision) {
case Decision.Yes:
case Decision.No:
return <Tag color="green">Completed</Tag>;
case Decision.Draft:
return <Tag color="orange">Unfilled form</Tag>;
case Decision.SeparateStudy:
return <Tag color="blue">Separate study</Tag>;
case Decision.MissedIgnoresMentor:
return <Tag color="red">Ignored mentor</Tag>;
case Decision.MissedWithReason:
return <Tag color="cyan">Missed with a reason</Tag>;
default: {
// fallback to the old feedback values
if (decision === 'noButGoodCandidate') {
return <Tag color="green">Completed</Tag>;
}
if (decision === 'didNotDecideYet') {
return <Tag color="orange">Unfilled form</Tag>;
}
return <Tag>Uncompleted</Tag>;
}
}
}
6 changes: 3 additions & 3 deletions client/src/modules/Interviews/pages/feedback/StepContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getDefaultStep,
getFeedbackFromTemplate,
getUpdatedFeedback,
isInterviewRejected,
isInterviewCanceled,
} from './feedbackTemplateHandler';

type ContextProps = {
Expand Down Expand Up @@ -45,7 +45,7 @@ export function StepContextProvider(props: PropsWithChildren<ContextProps>) {
const [activeStepIndex, setActiveIndex] = useState(() => getDefaultStep(feedback));
const activeStep = feedback.steps[activeStepIndex];

const [isFinished, setIsFinished] = useState(() => isInterviewRejected(activeStep.id, activeStep.values));
const [isFinished, setIsFinished] = useState(() => isInterviewCanceled(activeStep.id, activeStep.values));
const isFinalStep = activeStepIndex === feedback.steps.length - 1 || isFinished;

const saveFeedback = withLoading(async (values: InterviewFeedbackValues) => {
Expand Down Expand Up @@ -73,7 +73,7 @@ export function StepContextProvider(props: PropsWithChildren<ContextProps>) {

const onValuesChange = useCallback(
(_: InterviewFeedbackValues, values: InterviewFeedbackValues) => {
setIsFinished(isInterviewRejected(activeStep.id, values));
setIsFinished(isInterviewCanceled(activeStep.id, values));
},
[activeStep.id],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
getFeedbackFromTemplate,
getDefaultStep,
isInterviewRejected,
isInterviewCanceled,
getUpdatedFeedback,
} from './feedbackTemplateHandler';
import {
Expand Down Expand Up @@ -102,24 +102,24 @@ describe('getDefaultStep', () => {
});
});

describe('isInterviewRejected', () => {
describe('isInterviewCanceled', () => {
test('should return true if interview is rejected on the intro step', () => {
const stepValues = { interviewResult: 'missed' };
const isRejected = isInterviewRejected(FeedbackStepId.Introduction, stepValues);
const isRejected = isInterviewCanceled(FeedbackStepId.Introduction, stepValues);

expect(isRejected).toBe(true);
});

test('should return false if not rejected on intro step', () => {
const stepValues = { interviewResult: 'completed' };
const isRejected = isInterviewRejected(FeedbackStepId.Introduction, stepValues);
const isRejected = isInterviewCanceled(FeedbackStepId.Introduction, stepValues);

expect(isRejected).toBe(false);
});

test('should return false if not a intro step', () => {
const stepValues = {};
const isRejected = isInterviewRejected(FeedbackStepId.Theory, stepValues);
const isRejected = isInterviewCanceled(FeedbackStepId.Theory, stepValues);

expect(isRejected).toBe(false);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@ export function getDefaultStep(feedback: Feedback) {
for (let i = 0; i < feedback.steps.length; i++) {
const { isCompleted, id, values } = feedback.steps[i];

if (!isCompleted || isInterviewRejected(id, values)) {
if (!isCompleted || isInterviewCanceled(id, values) || i === feedback.steps.length - 1) {
return i;
}
}

return feedback.isCompleted ? feedback.steps.length - 1 : 0;
return 0;
}

/**
* checks whether the step contains rejection value
*/
export function isInterviewRejected(stepId: FeedbackStepId, stepValues: InterviewFeedbackValues = {}) {
export function isInterviewCanceled(stepId: FeedbackStepId, stepValues: InterviewFeedbackValues = {}) {
return stepId === FeedbackStepId.Introduction && stepValues.interviewResult === 'missed';
}

Expand Down Expand Up @@ -125,10 +125,10 @@ export function getUpdatedFeedback({
interviewMaxScore: number;
}) {
const { steps } = feedback;
const isRejected = isInterviewRejected(steps[activeStepIndex].id, newValues);
const isCanceled = isInterviewCanceled(steps[activeStepIndex].id, newValues);

const feedbackValues = {
steps: generateFeedbackValues(steps, activeStepIndex, newValues, isRejected),
steps: generateFeedbackValues(steps, activeStepIndex, newValues, isCanceled),
};
const newFeedback = mergeFeedbackValuesToTemplate(feedback, feedbackValues, interviewMaxScore);

Expand All @@ -144,7 +144,7 @@ function generateFeedbackValues(
steps: FeedbackStep[],
activeStepIndex: number,
newValues: InterviewFeedbackValues,
isRejected: boolean,
isCanceled: boolean,
): Record<FeedbackStepId, InterviewFeedbackStepData> {
return steps.reduce((stepMap, step, index) => {
if (index === activeStepIndex) {
Expand All @@ -155,10 +155,10 @@ function generateFeedbackValues(
return stepMap;
}

// if is rejected, all steps after the current one should be marked as not completed and values should be removed
// if is canceled, all steps after the current one should be marked as not completed and values should be removed
stepMap[step.id] = {
values: isRejected ? undefined : step.values,
isCompleted: isRejected ? false : step.isCompleted,
values: isCanceled ? undefined : step.values,
isCompleted: isCanceled ? false : step.isCompleted,
};
return stepMap;
}, {} as Record<FeedbackStepId, InterviewFeedbackStepData>);
Expand All @@ -171,7 +171,7 @@ function getInterviewSummary(feedback: Feedback) {
const { steps } = feedback;
const decision = steps.find(step => step.id === FeedbackStepId.Decision);
const introduction = steps.find(step => step.id === FeedbackStepId.Introduction);
const isInterviewConducted = !isInterviewRejected(FeedbackStepId.Introduction, introduction?.values);
const isInterviewConducted = !isInterviewCanceled(FeedbackStepId.Introduction, introduction?.values);

return {
score: isInterviewConducted ? (decision?.values?.finalScore as number) : 0,
Expand All @@ -180,6 +180,9 @@ function getInterviewSummary(feedback: Feedback) {
};

function getIsGoodCandidate() {
if (!isInterviewConducted) {
return false;
}
if (decision?.values?.isGoodCandidate == undefined) {
return;
}
Expand All @@ -203,7 +206,7 @@ function isInterviewCompleted(feedback: Feedback) {
const decision = feedback.steps.find(step => step.id === FeedbackStepId.Decision);

return (
(introduction && isInterviewRejected(introduction.id, introduction.values)) ||
(introduction && isInterviewCanceled(introduction.id, introduction.values)) ||
(steps.every(step => step.isCompleted) && decision?.values?.decision !== Decision.Draft)
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Col, Tag, Row, Button, Typography, Space } from 'antd';
import { Col, Row, Button, Typography, Space } from 'antd';
import { GithubAvatar } from 'components/GithubAvatar';
import GithubFilled from '@ant-design/icons/GithubFilled';
import { getInterviewFeedbackUrl } from 'domain/interview';
import { DecisionTag, getInterviewFeedbackUrl } from 'domain/interview';
import { MentorInterview } from 'services/course';
import css from 'styled-jsx/css';

Expand All @@ -12,9 +12,8 @@ export function StudentInterview(props: { interview: MentorInterview; template?:
<Col className={containerClassName}>
<Space size={21} direction="vertical" style={{ width: '100%' }}>
<Row justify="space-between" align="middle">
<Tag color={interview.completed ? 'green' : undefined}>
{interview.completed ? 'Completed' : 'Uncompleted'}
</Tag>
<DecisionTag decision={interview.decision} status={interview.status} />

<Button
type="primary"
ghost
Expand Down
5 changes: 4 additions & 1 deletion client/src/services/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
CrossCheckCriteriaDataDto,
} from 'api';
import { optionalQueryString } from 'utils/optionalQueryString';
import { Decision } from 'data/interviews/technical-screening';
import { InterviewStatus } from 'domain/interview';

export enum CrossCheckStatus {
Initial = 'initial',
Expand Down Expand Up @@ -697,7 +699,8 @@ export type MentorInterview = {
endDate: string;
completed: boolean;
interviewer: unknown;
status: number;
status: InterviewStatus;
student: UserBasic;
decision?: Decision;
id: number;
};
1 change: 1 addition & 0 deletions server/src/repositories/stageInterview.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export class StageInterviewRepository extends AbstractRepository<StageInterview>
endDate: it.courseTask.studentEndDate,
result: it.decision ?? null,
interviewer: { githubId: it.mentor.user.githubId, name: userService.createName(it.mentor.user) },
decision: it.decision,
student: {
id: it.student.id,
githubId: it.student.user.githubId,
Expand Down

0 comments on commit bf3d231

Please sign in to comment.