diff --git a/src/back-end/lib/db/evaluations/sprint-with-us/team-questions.ts b/src/back-end/lib/db/evaluations/sprint-with-us/team-questions.ts index 6a30833f9..8f7319b74 100644 --- a/src/back-end/lib/db/evaluations/sprint-with-us/team-questions.ts +++ b/src/back-end/lib/db/evaluations/sprint-with-us/team-questions.ts @@ -1,9 +1,15 @@ import { + RawSWUEvaluationPanelMember, readOneSWUEvaluationPanelMember, readOneSWUOpportunity } from "back-end/lib/db/opportunity/sprint-with-us"; import { getValidValue, isInvalid, valid } from "shared/lib/validation"; -import { Connection, readOneSWUProposalSlim, tryDb } from "back-end/lib/db"; +import { + Connection, + readOneSWUProposalSlim, + Transaction, + tryDb +} from "back-end/lib/db"; import { AuthenticatedSession, Session, @@ -380,6 +386,80 @@ export const updateSWUTeamQuestionResponseEvaluation = tryDb< ); }); +/** + * All evaluations have been submitted when every evaluator has evaluated every + * question for every proponent + */ +export async function allSWUTeamQuestionResponseEvaluatorEvaluationsSubmitted( + connection: Connection, + trx: Transaction, + opportunityId: string, + proposalsCount: number +) { + const [ + [{ count: submittedEvaluatorEvaluationsCount }], + [{ count: evaluatorsCount }], + [{ count: questionsCount }] + ] = await Promise.all([ + generateSWUTeamQuestionResponseEvaluationQuery(connection) + .transacting(trx) + .clearSelect() + .where({ + "statuses.status": SWUTeamQuestionResponseEvaluationStatus.Submitted + }) + .count("*"), + // Evaluators for the most recent version + connection( + "swuEvaluationPanelMembers as members" + ) + .transacting(trx) + .join( + connection.raw( + "(??) as versions", + connection("swuOpportunityVersions") + .select("opportunity", "id") + .rowNumber("rn", function () { + this.orderBy("createdAt", "desc").partitionBy("opportunity"); + }) + ), + function () { + this.on("members.opportunityVersion", "=", "versions.id"); + } + ) + .where({ + evaluator: true, + "versions.opportunity": opportunityId, + "versions.rn": 1 + }) + .count("*"), + // Questions for the most recent version + connection("swuTeamQuestions as questions") + .transacting(trx) + .join( + connection.raw( + "(??) as versions", + connection("swuOpportunityVersions") + .select("opportunity", "id") + .rowNumber("rn", function () { + this.orderBy("createdAt", "desc").partitionBy("opportunity"); + }) + ), + function () { + this.on("questions.opportunityVersion", "=", "versions.id"); + } + ) + .where({ + "versions.opportunity": opportunityId, + "versions.rn": 1 + }) + .count("*") + ]); + return ( + Number(submittedEvaluatorEvaluationsCount) === + Number(evaluatorsCount) * proposalsCount * Number(questionsCount) + ); +} + function generateSWUTeamQuestionResponseEvaluationQuery( connection: Connection, consensus = false @@ -391,24 +471,47 @@ function generateSWUTeamQuestionResponseEvaluationQuery( ? CHAIR_EVALUATION_STATUS_TABLE_NAME : EVALUATOR_EVALUATION_STATUS_TABLE_NAME; const query = connection(`${evaluationTableName} as evaluations`) - .join(`${evaluationStatusTableName} as statuses`, function () { - this.on( - "evaluations.evaluationPanelMember", - "=", - "statuses.evaluationPanelMember" - ) - .andOn("evaluations.proposal", "=", "statuses.proposal") - .andOnNotNull("statuses.status") - .andOn( - "statuses.createdAt", + .join( + connection.raw( + "(??) as statuses", + connection(`${evaluationStatusTableName}`) + .select("evaluationPanelMember", "proposal", "status", "createdAt") + .rowNumber("rn", function () { + this.orderBy("createdAt", "desc").partitionBy([ + "evaluationPanelMember", + "proposal" + ]); + }) + ), + function () { + this.on( + "evaluations.evaluationPanelMember", "=", - connection.raw( - `(select max("createdAt") from "${evaluationStatusTableName}" as statuses2 where - statuses2."evaluationPanelMember" = evaluations."evaluationPanelMember" and statuses2."proposal" = evaluations."proposal" - and statuses2.status is not null)` - ) - ); - }) + "statuses.evaluationPanelMember" + ) + .andOn("evaluations.proposal", "=", "statuses.proposal") + .andOn("statuses.rn", "=", connection.raw(1)); + } + ) + .join("swuProposals as proposals", "evaluations.proposal", "proposals.id") + .join( + "swuEvaluationPanelMembers as members", + "evaluations.evaluationPanelMember", + "members.user" + ) + .join( + connection.raw( + "(??) as versions", + connection("swuOpportunityVersions") + .select("opportunity", "id") + .rowNumber("rn", function () { + this.orderBy("createdAt", "desc").partitionBy("opportunity"); + }) + ), + function () { + this.on("proposals.opportunity", "=", "versions.opportunity"); + } + ) .select( "evaluations.proposal", "evaluations.evaluationPanelMember", @@ -420,7 +523,11 @@ function generateSWUTeamQuestionResponseEvaluationQuery( "evaluations.notes", "statuses.status", "statuses.createdAt" - ); + ) + .where({ + "members.opportunityVersion": connection.raw("versions.id"), + "versions.rn": 1 + }); return query; } diff --git a/src/back-end/lib/db/opportunity/sprint-with-us.ts b/src/back-end/lib/db/opportunity/sprint-with-us.ts index 32814c7b1..6c5f3f2c9 100644 --- a/src/back-end/lib/db/opportunity/sprint-with-us.ts +++ b/src/back-end/lib/db/opportunity/sprint-with-us.ts @@ -1,5 +1,6 @@ import { generateUuid } from "back-end/lib"; import { + allSWUTeamQuestionResponseEvaluatorEvaluationsSubmitted, CHAIR_EVALUATION_STATUS_TABLE_NAME, CHAIR_EVALUATION_TABLE_NAME, Connection, @@ -13,11 +14,8 @@ import { import { readOneFileById } from "back-end/lib/db/file"; import { readOneOrganizationContactEmail } from "back-end/lib/db/organization"; import { - allIndividualSWUTeamQuestionResponseEvaluationsComplete, - readManySWUProposals, readOneSWUAwardedProposal, - readSubmittedSWUProposalCount, - updateSWUProposalStatus + readSubmittedSWUProposalCount } from "back-end/lib/db/proposal/sprint-with-us"; import { RawSWUOpportunitySubscriber } from "back-end/lib/db/subscribers/sprint-with-us"; import { readOneUser, readOneUserSlim } from "back-end/lib/db/user"; @@ -1215,6 +1213,25 @@ export const updateSWUOpportunityVersion = tryDb< ...restOfOpportunity } = opportunity; const opportunityVersion = await connection.transaction(async (trx) => { + const prevPanel: RawSWUEvaluationPanelMember[] = + await connection( + "swuEvaluationPanelMembers as sepm" + ) + .select("sepm.*") + .join( + "swuOpportunityVersions as sov", + "sepm.opportunityVersion", + "=", + "sov.id" + ) + .where( + "sov.createdAt", + "=", + connection("swuOpportunityVersions as sov2") + .max("createdAt") + .where("sov2.opportunity", "=", restOfOpportunity.id) + ); + const [versionRecord] = await connection( "swuOpportunityVersions" ) @@ -1285,12 +1302,30 @@ export const updateSWUOpportunityVersion = tryDb< // Create evaluation panel for (const member of evaluationPanel) { - await connection("swuEvaluationPanelMembers") - .transacting(trx) - .insert({ - ...member, - opportunityVersion: versionRecord.id - }); + const prevMember = prevPanel.find(({ user }) => user === member.user); + if (prevMember) { + await connection( + "swuEvaluationPanelMembers" + ) + .transacting(trx) + .where({ + user: prevMember.user, + opportunityVersion: prevMember.opportunityVersion + }) + .update({ + ...member, + opportunityVersion: versionRecord.id + }); + } else { + await connection( + "swuEvaluationPanelMembers" + ) + .transacting(trx) + .insert({ + ...member, + opportunityVersion: versionRecord.id + }); + } } // Add an 'edit' change record @@ -1467,12 +1502,12 @@ export const closeSWUOpportunities = tryDb<[], number>(async (connection) => { )?.map((result) => result.id) || []; for (const [index, proposalId] of proposalIds.entries()) { - // Set the proposal to EVALUATION_QUESTIONS_INDIVIDUAL status + // Set the proposal to UNDER_REVIEW_QUESTIONS status await connection("swuProposalStatuses").transacting(trx).insert({ id: generateUuid(), createdAt: now, proposal: proposalId, - status: SWUProposalStatus.EvaluationTeamQuestionsIndividual, + status: SWUProposalStatus.UnderReviewTeamQuestions, note: "" }); @@ -1698,44 +1733,17 @@ export const submitIndividualQuestionEvaluations = tryDb< if (!statusRecord) { throw new Error("unable to update team question evaluation"); } - - if ( - await allIndividualSWUTeamQuestionResponseEvaluationsComplete( - connection, - trx, - proposalId, - id - ) - ) { - const result = await updateSWUProposalStatus( - connection, - proposalId, - SWUProposalStatus.EvaluationTeamQuestionsConsensus, - "", - session - ); - if (isInvalid(result)) { - throw new Error("unable to update proposal"); - } - } } ) ); - // Update opportunity status if all evaluations complete for proposal - const updatedSWUProposals = getValidValue( - await readManySWUProposals(connection, session, id), - undefined - ); - if (!updatedSWUProposals) { - throw new Error("unable to read proposals"); - } - + // Update opportunity status if all evaluations complete if ( - updatedSWUProposals.every( - (proposal) => - proposal.status !== - SWUProposalStatus.EvaluationTeamQuestionsIndividual + await allSWUTeamQuestionResponseEvaluatorEvaluationsSubmitted( + connection, + trx, + id, + evaluationParams.evaluations.map(({ proposal }) => proposal.id).length ) ) { const result = await updateSWUOpportunityStatus( @@ -1812,18 +1820,6 @@ export const submitConsensusQuestionEvaluations = tryDb< } ) ); - - const result = await updateSWUOpportunityStatus( - connection, - id, - SWUOpportunityStatus.EvaluationTeamQuestionsReview, - "", - session - ); - - if (!result) { - throw new Error("unable to update opportunity"); - } }); const dbResult = await readOneSWUOpportunity(connection, id, session); diff --git a/src/back-end/lib/db/proposal/sprint-with-us.ts b/src/back-end/lib/db/proposal/sprint-with-us.ts index ffd62d197..87a20d360 100644 --- a/src/back-end/lib/db/proposal/sprint-with-us.ts +++ b/src/back-end/lib/db/proposal/sprint-with-us.ts @@ -4,14 +4,11 @@ import { getOrgIdsForOwnerOrAdmin, Transaction, isUserOwnerOrAdminOfOrg, - tryDb, - EVALUATOR_EVALUATION_STATUS_TABLE_NAME, - SWUTeamQuestionResponseEvaluationStatusRecord + tryDb } from "back-end/lib/db"; import { readOneFileById } from "back-end/lib/db/file"; import { generateSWUOpportunityQuery, - RawSWUEvaluationPanelMember, RawSWUOpportunity, RawSWUOpportunitySlim, readManyTeamQuestions, @@ -62,7 +59,6 @@ import { UpdateEditRequestBody, UpdateTeamQuestionScoreBody } from "shared/lib/resources/proposal/sprint-with-us"; -import { SWUTeamQuestionResponseEvaluationStatus } from "shared/lib/resources/question-evaluation/sprint-with-us"; import { AuthenticatedSession, Session } from "shared/lib/resources/session"; import { User, userToUserSlim, UserType } from "shared/lib/resources/user"; import { adt, Id } from "shared/lib/types"; @@ -2119,54 +2115,3 @@ export const readOneSWUProposalAuthor = tryDb<[Id], User | null>( return authorId ? await readOneUser(connection, authorId) : valid(null); } ); - -export async function allIndividualSWUTeamQuestionResponseEvaluationsComplete( - connection: Connection, - trx: Transaction, - proposalId: string, - opportunityId: string -) { - const [ - [{ count: submittedIndividualEvaluationsCount }], - [{ count: evaluatorsCount }] - ] = await Promise.all([ - connection( - `${EVALUATOR_EVALUATION_STATUS_TABLE_NAME} as statuses` - ) - .transacting(trx) - .from( - connection( - `${EVALUATOR_EVALUATION_STATUS_TABLE_NAME} as statuses` - ) - .transacting(trx) - .max("createdAt") - .where({ - status: SWUTeamQuestionResponseEvaluationStatus.Submitted, - proposal: proposalId - }) - .groupBy("evaluationPanelMember", "proposal") - .as("submittedIndividualEvaluations") - ) - .count[]>("*"), - connection( - "swuEvaluationPanelMembers as members" - ) - .transacting(trx) - .join("swuOpportunityVersions as versions", function () { - this.on("members.opportunityVersion", "=", "versions.id").andOn( - "versions.createdAt", - "=", - connection.raw( - '(select max("createdAt") from "swuOpportunityVersions" as versions2 where \ - versions2.opportunity = ?)', - opportunityId - ) - ); - }) - .where({ - evaluator: true - }) - .count[]>("*") - ]); - return submittedIndividualEvaluationsCount === evaluatorsCount; -} diff --git a/src/back-end/lib/permissions.ts b/src/back-end/lib/permissions.ts index 0b67e1399..c95d0b78c 100644 --- a/src/back-end/lib/permissions.ts +++ b/src/back-end/lib/permissions.ts @@ -882,15 +882,11 @@ export async function readOneSWUTeamQuestionResponseEvaluation( (doesSWUOpportunityStatusAllowGovToViewTeamQuestionResponseEvaluations( evaluation.proposal.opportunity.status ) || - (evaluation.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsIndividual && - evaluation.proposal.opportunity.status === - SWUOpportunityStatus.EvaluationTeamQuestionsIndividual && + (evaluation.proposal.opportunity.status === + SWUOpportunityStatus.EvaluationTeamQuestionsIndividual && evaluation.evaluationPanelMember.user.id === session.user.id) || - (evaluation.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsConsensus && - evaluation.proposal.opportunity.status === - SWUOpportunityStatus.EvaluationTeamQuestionsConsensus && + (evaluation.proposal.opportunity.status === + SWUOpportunityStatus.EvaluationTeamQuestionsConsensus && (await isSWUOpportunityEvaluationPanelChair( connection, session, @@ -909,8 +905,8 @@ export async function readOneSWUTeamQuestionResponseConsensus( (doesSWUOpportunityStatusAllowGovToViewTeamQuestionResponseEvaluations( evaluation.proposal.opportunity.status ) || - (evaluation.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsConsensus && + (evaluation.proposal.opportunity.status === + SWUOpportunityStatus.EvaluationTeamQuestionsConsensus && evaluation.evaluationPanelMember.user.id === session.user.id)) ); } @@ -970,7 +966,8 @@ export async function readManySWUTeamQuestionResponseEvaluationsForConsensus( (doesSWUOpportunityStatusAllowGovToViewTeamQuestionResponseEvaluations( proposal.opportunity.status ) || - (proposal.status === SWUProposalStatus.EvaluationTeamQuestionsConsensus && + (proposal.opportunity.status === + SWUOpportunityStatus.EvaluationTeamQuestionsConsensus && ((await isSWUOpportunityEvaluationPanelEvaluator( connection, session, @@ -998,7 +995,8 @@ export async function createSWUTeamQuestionResponseConsensus( return ( !!session && (isAdmin(session) || isGovernment(session)) && - proposal.status === SWUProposalStatus.EvaluationTeamQuestionsConsensus && + proposal.opportunity.status === + SWUOpportunityStatus.EvaluationTeamQuestionsConsensus && (await isSWUOpportunityEvaluationPanelChair( connection, session, @@ -1014,7 +1012,8 @@ export async function createSWUTeamQuestionResponseEvaluation( return ( !!session && (isAdmin(session) || isGovernment(session)) && - proposal.status === SWUProposalStatus.EvaluationTeamQuestionsIndividual && + proposal.opportunity.status === + SWUOpportunityStatus.EvaluationTeamQuestionsIndividual && (await isSWUOpportunityEvaluationPanelEvaluator( connection, session, @@ -1031,8 +1030,6 @@ export function editSWUTeamQuestionResponseConsensus( !!session && (isAdmin(session) || isGovernment(session)) && evaluation.evaluationPanelMember.user.id === session.user.id && - evaluation.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsConsensus && evaluation.proposal.opportunity.status === SWUOpportunityStatus.EvaluationTeamQuestionsConsensus ); @@ -1046,8 +1043,6 @@ export function editSWUTeamQuestionResponseEvaluation( !!session && (isAdmin(session) || isGovernment(session)) && evaluation.evaluationPanelMember.user.id === session.user.id && - evaluation.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsIndividual && evaluation.proposal.opportunity.status === SWUOpportunityStatus.EvaluationTeamQuestionsIndividual ); @@ -1061,8 +1056,6 @@ export function submitSWUTeamQuestionResponseConsensus( !!session && (isAdmin(session) || isGovernment(session)) && evaluation.evaluationPanelMember.user.id === session.user.id && - evaluation.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsConsensus && evaluation.proposal.opportunity.status === SWUOpportunityStatus.EvaluationTeamQuestionsConsensus ); @@ -1076,13 +1069,28 @@ export function submitSWUTeamQuestionResponseEvaluation( !!session && (isAdmin(session) || isGovernment(session)) && evaluation.evaluationPanelMember.user.id === session.user.id && - evaluation.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsIndividual && evaluation.proposal.opportunity.status === SWUOpportunityStatus.EvaluationTeamQuestionsIndividual ); } +export async function editSWUEvaluationPanel( + connection: Connection, + session: Session, + opportunityId: Id +) { + return ( + !!session && + (isAdmin(session) || + (isGovernment(session) && + (await isSWUOpportunityAuthor( + connection, + session.user, + opportunityId + )))) + ); +} + // TWU Opportunities export function createTWUOpportunity( diff --git a/src/back-end/lib/resources/opportunity/sprint-with-us/index.ts b/src/back-end/lib/resources/opportunity/sprint-with-us/index.ts index 107c88522..c4f897e07 100644 --- a/src/back-end/lib/resources/opportunity/sprint-with-us/index.ts +++ b/src/back-end/lib/resources/opportunity/sprint-with-us/index.ts @@ -43,11 +43,13 @@ import { UpdateWithNoteRequestBody, CreateSWUEvaluationPanelMemberBody, CreateSWUEvaluationPanelMemberValidationErrors, - SubmitQuestionEvaluationsWithNoteRequestBody + SubmitQuestionEvaluationsWithNoteRequestBody, + canChangeEvaluationPanel } from "shared/lib/resources/opportunity/sprint-with-us"; import { CreateSWUTeamQuestionResponseEvaluationScoreValidationErrors, - isValidStatusChange as IsValidQuestionEvaluationStatusChange, + isValidEvaluationStatusChange, + isValidConsensusStatusChange, SWUTeamQuestionResponseEvaluation, SWUTeamQuestionResponseEvaluationStatus } from "shared/lib/resources/question-evaluation/sprint-with-us"; @@ -126,7 +128,8 @@ interface ValidatedUpdateRequestBody { | ADT< "submitConsensusQuestionEvaluations", ValidatedSubmitQuestionEvaluationsWithNoteRequestBody - >; + > + | ADT<"editEvaluationPanel", ValidatedUpdateEditRequestBody>; } type ValidatedUpdateEditRequestBody = Omit< @@ -822,6 +825,11 @@ const update: crud.Update< "submitConsensusQuestionEvaluations", value as SubmitQuestionEvaluationsWithNoteRequestBody ); + case "editEvaluationPanel": + return adt( + "editEvaluationPanel", + value as CreateSWUEvaluationPanelMemberBody[] + ); default: return null; } @@ -846,7 +854,8 @@ const update: crud.Update< if ( (![ "submitIndividualQuestionEvaluations", - "submitConsensusQuestionEvaluations" + "submitConsensusQuestionEvaluations", + "edutEvaluationPanel" ].includes(request.body.tag) && !(await permissions.editSWUOpportunity( connection, @@ -1713,7 +1722,7 @@ const update: crud.Update< } if ( - !IsValidQuestionEvaluationStatusChange( + !isValidEvaluationStatusChange( validatedSWUTeamQuestionResponseEvaluation.value.status, SWUTeamQuestionResponseEvaluationStatus.Submitted ) @@ -1836,7 +1845,7 @@ const update: crud.Update< } if ( - !IsValidQuestionEvaluationStatusChange( + !isValidConsensusStatusChange( validatedSWUTeamQuestionResponseEvaluation.value.status, SWUTeamQuestionResponseEvaluationStatus.Submitted ) @@ -1909,6 +1918,51 @@ const update: crud.Update< }) } as ValidatedUpdateRequestBody); } + case "editEvaluationPanel": { + if ( + !canChangeEvaluationPanel(swuOpportunity) || + !(await permissions.editSWUEvaluationPanel( + connection, + request.session, + swuOpportunity.id + )) + ) { + return invalid({ permissions: [permissions.ERROR_MESSAGE] }); + } + const validatedEvaluationPanel = + await validateSWUEvaluationPanelMembers( + connection, + request.body.value + ); + if ( + isInvalid( + validatedEvaluationPanel + ) + ) { + return invalid({ + opportunity: adt("edit" as const, { + evaluationPanel: validatedEvaluationPanel.value + }) + }); + } + return valid({ + session: request.session, + body: adt("editEvaluationPanel" as const, { + ...omit( + swuOpportunity, + "addenda", + "history", + "publishedAt", + "reporting", + "status", + "subscribed", + "updatedAt", + "updatedBy" + ), + evaluationPanel: validatedEvaluationPanel.value + }) + } as ValidatedUpdateRequestBody); + } default: return invalid({ opportunity: adt("parseFailure" as const) }); } @@ -2080,6 +2134,13 @@ const update: crud.Update< session ); break; + case "editEvaluationPanel": + dbResult = await db.updateSWUOpportunityVersion( + connection, + { ...body.value, id: request.params.id }, + session + ); + break; } if (isInvalid(dbResult)) { return basicResponse( diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/components/evaluation-panel.tsx b/src/front-end/typescript/lib/components/evaluation-panel.tsx similarity index 100% rename from src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/components/evaluation-panel.tsx rename to src/front-end/typescript/lib/components/evaluation-panel.tsx diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/consensus.tsx b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/consensus.tsx index 91a99b0ca..daca53ecb 100644 --- a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/consensus.tsx +++ b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/consensus.tsx @@ -32,13 +32,9 @@ import { compareSWUProposalAnonymousProponentNumber, getSWUProponentName, NUM_SCORE_DECIMALS, - SWUProposalSlim, - SWUProposalStatus + SWUProposalSlim } from "shared/lib/resources/proposal/sprint-with-us"; -import { - SWUTeamQuestionResponseEvaluation, - SWUTeamQuestionResponseEvaluationStatus -} from "shared/lib/resources/question-evaluation/sprint-with-us"; +import { SWUTeamQuestionResponseEvaluation } from "shared/lib/resources/question-evaluation/sprint-with-us"; import { ADT, adt } from "shared/lib/types"; import { isValid } from "shared/lib/validation"; import { validateSWUTeamQuestionResponseEvaluationScores } from "shared/lib/validation/question-evaluation/sprint-with-us"; @@ -117,15 +113,7 @@ const update: component_.page.Update = ({ .set( "canEvaluationsBeSubmitted", opportunity.status === - SWUOpportunityStatus.EvaluationTeamQuestionsConsensus && - evaluations.reduce( - (acc, e) => - acc || - (e.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsConsensus && - e.status === SWUTeamQuestionResponseEvaluationStatus.Draft), - false as boolean - ) + SWUOpportunityStatus.EvaluationTeamQuestionsConsensus ), [component_.cmd.dispatch(component_.page.readyMsg())] ]; diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/evaluation-panel.tsx b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/evaluation-panel.tsx new file mode 100644 index 000000000..eb1a9a539 --- /dev/null +++ b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/evaluation-panel.tsx @@ -0,0 +1,326 @@ +import { makeStartLoading, makeStopLoading } from "front-end/lib"; +import { Route } from "front-end/lib/app/types"; +import * as EvaluationPanel from "front-end/lib/components/evaluation-panel"; +import { + Immutable, + immutable, + component as component_ +} from "front-end/lib/framework"; +import * as api from "front-end/lib/http/api"; +import * as toasts from "front-end/lib/pages/opportunity/sprint-with-us/lib/toasts"; +import * as Tab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab"; +import EditTabHeader from "front-end/lib/pages/opportunity/sprint-with-us/lib/views/edit-tab-header"; +import { iconLinkSymbol, leftPlacement } from "front-end/lib/views/link"; +import React from "react"; +import { Col, Row } from "reactstrap"; +import { + canChangeEvaluationPanel, + SWUOpportunity, + UpdateEditValidationErrors, + UpdateValidationErrors +} from "shared/lib/resources/opportunity/sprint-with-us"; +import { adt, ADT } from "shared/lib/types"; + +export interface State extends Tab.Params { + opportunity: SWUOpportunity | null; + evaluationPanel: Immutable | null; + startEditingLoading: number; + startSaveLoading: number; + isEditing: boolean; +} + +export type InnerMsg = + | ADT<"onInitResponse", Tab.InitResponse> + | ADT<"evaluationPanel", EvaluationPanel.Msg> + | ADT<"startEditing"> + | ADT< + "onStartEditingResponse", + api.ResponseValidation + > + | ADT<"cancelEditing"> + | ADT<"save"> + | ADT< + "onSaveResponse", + api.ResponseValidation + >; + +export type Msg = component_.page.Msg; + +function resetEvaluationPanel( + state: Immutable, + opportunity: SWUOpportunity +): component_.page.UpdateReturnValue { + const [evaluationPanelState, evaluationPanelCommands] = EvaluationPanel.init({ + evaluationPanel: opportunity.evaluationPanel ?? [] + }); + return [ + state.merge({ + opportunity, + evaluationPanel: immutable(evaluationPanelState) + }), + component_.cmd.mapMany( + evaluationPanelCommands, + (msg) => adt("evaluationPanel", msg) as Msg + ) + ]; +} + +const init: component_.base.Init = (params) => { + return [ + { + ...params, + opportunity: null, + evaluationPanel: null, + startEditingLoading: 0, + startSaveLoading: 0, + isEditing: false + }, + [] + ]; +}; + +const startStartEditingLoading = makeStartLoading("startEditingLoading"); +const stopStartEditingLoading = makeStopLoading("startEditingLoading"); +const startSaveLoading = makeStartLoading("startSaveLoading"); +const stopSaveLoading = makeStopLoading("startSaveLoading"); + +const update: component_.page.Update = ({ + state, + msg +}) => { + switch (msg.tag) { + case "onInitResponse": { + const opportunity = msg.value[0]; + const existingEvaluationPanel = opportunity.evaluationPanel; + const [evaluationPanelState, evaluationPanelCmds] = EvaluationPanel.init({ + evaluationPanel: existingEvaluationPanel ?? [] + }); + return [ + state + .set("opportunity", opportunity) + .set("evaluationPanel", immutable(evaluationPanelState)), + [ + ...component_.cmd.mapMany( + evaluationPanelCmds, + (msg) => adt("evaluationPanel", msg) as Msg + ), + component_.cmd.dispatch(component_.page.readyMsg()) + ] + ]; + } + case "evaluationPanel": + return component_.base.updateChild({ + state, + childStatePath: ["evaluationPanel"], + childUpdate: EvaluationPanel.update, + childMsg: msg.value, + mapChildMsg: (value) => adt("evaluationPanel", value) + }); + case "startEditing": { + const opportunity = state.opportunity; + if (!opportunity) return [state, []]; + return [ + startStartEditingLoading(state), + [ + api.opportunities.swu.readOne()(opportunity.id, (response) => + adt("onStartEditingResponse", response) + ) + ] + ]; + } + case "onStartEditingResponse": { + const response = msg.value; + state = stopStartEditingLoading(state); + if (api.isValid(response)) { + state = state.set("isEditing", true); + return resetEvaluationPanel(state, response.value); + } else { + return [ + state, + [ + component_.cmd.dispatch( + component_.global.showToastMsg( + adt("error", toasts.startedEditing.error) + ) + ) + ] + ]; + } + } + case "cancelEditing": { + const opportunity = state.opportunity; + if (!opportunity) return [state, []]; + state = state.merge({ + isEditing: false + }); + return resetEvaluationPanel(state, opportunity); + } + case "save": { + const opportunity = state.opportunity; + const evaluationPaenl = state.evaluationPanel; + if (!opportunity || !evaluationPaenl) return [state, []]; + const values = EvaluationPanel.getValues(evaluationPaenl); + return [ + startSaveLoading(state), + [ + api.opportunities.swu.update()( + opportunity.id, + adt("editEvaluationPanel", values), + ( + response: api.ResponseValidation< + SWUOpportunity, + UpdateValidationErrors + > + ) => { + return adt( + "onSaveResponse", + api.mapInvalid(response, (errors) => { + if ( + errors.opportunity && + errors.opportunity.tag === "editEvaluationPanel" + ) { + return errors.opportunity.value; + } else { + return {}; + } + }) + ); + } + ) + ] + ]; + } + case "onSaveResponse": { + state = stopSaveLoading(state); + const response = msg.value; + switch (response.tag) { + case "valid": { + state = state.set("isEditing", false); + const [resetState, resetCmds] = resetEvaluationPanel( + state, + response.value + ); + return [ + resetState, + [ + ...resetCmds, + component_.cmd.dispatch( + component_.global.showToastMsg( + adt("success", toasts.changesPublished.success) + ) + ) + ] + ]; + } + case "invalid": + state = state.update( + "evaluationPanel", + (ep) => + ep && + EvaluationPanel.setErrors(ep, response.value.evaluationPanel) + ); + return [ + state, + [ + component_.cmd.dispatch( + component_.global.showToastMsg( + adt("error", toasts.changesPublished.error) + ) + ) + ] + ]; + case "unhandled": + default: + return [ + state, + [ + component_.cmd.dispatch( + component_.global.showToastMsg( + adt("error", toasts.changesPublished.error) + ) + ) + ] + ]; + } + } + default: + return [state, []]; + } +}; + +const view: component_.page.View = ({ + state, + dispatch +}) => { + if (!state.opportunity || !state.evaluationPanel) return null; + const disabled = !state.isEditing || state.startEditingLoading > 0; + return ( +
+ +
+

Evaluation Panel

+ + + + adt("evaluationPanel" as const, msg) + )} + state={state.evaluationPanel} + disabled={disabled} + /> + + +
+
+ ); +}; + +export const component: Tab.Component = { + init, + update, + view, + + onInitResponse(response) { + return adt("onInitResponse", response); + }, + + getActions({ state, dispatch }) { + if (!state.opportunity || !canChangeEvaluationPanel(state.opportunity)) + return component_.page.actions.none(); + const isEditingLoading = state.startEditingLoading > 0; + const isSaveLoading = state.startSaveLoading > 0; + const isLoading = isEditingLoading || isSaveLoading; + const isValid = + state.evaluationPanel && EvaluationPanel.isValid(state.evaluationPanel); + return state.isEditing + ? component_.page.actions.links([ + { + children: "Save Changes", + onClick: () => dispatch(adt("save") as Msg), + button: true, + loading: isLoading, + disabled: isLoading || !isValid, + symbol_: leftPlacement(iconLinkSymbol("bullhorn")), + color: "primary" + }, + { + children: "Cancel", + onClick: () => dispatch(adt("cancelEditing") as Msg) + } + ]) + : adt("links", [ + { + children: "Edit", + onClick: () => dispatch(adt("startEditing")), + button: true, + loading: isLoading, + disabled: isLoading, + symbol_: leftPlacement(iconLinkSymbol("user-edit")), + color: "primary" + } + ]); + } +}; diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/index.ts b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/index.ts index 40b5cecdb..57a2bc915 100644 --- a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/index.ts +++ b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/index.ts @@ -5,6 +5,7 @@ import * as AddendaTab from "front-end/lib/pages/opportunity/sprint-with-us/edit import * as CodeChallengeTab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab/code-challenge"; import * as HistoryTab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab/history"; import * as OpportunityTab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab/opportunity"; +import * as EvaluationPanelTab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab/evaluation-panel"; import * as OverviewTab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab/overview"; import * as ConsensusTab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab/consensus"; import * as ProposalsTab from "front-end/lib/pages/opportunity/sprint-with-us/edit/tab/proposals"; @@ -77,6 +78,12 @@ export interface Tabs { OpportunityTab.InnerMsg, InitResponse >; + evaluationPanel: TabbedPage.Tab< + Params, + EvaluationPanelTab.State, + EvaluationPanelTab.InnerMsg, + InitResponse + >; addenda: TabbedPage.Tab< Params, AddendaTab.State, @@ -152,6 +159,7 @@ export const parseTabId: TabbedPage.ParseTabId = (raw) => { case "instructions": case "overview": case "consensus": + case "evaluationPanel": return raw; default: return null; @@ -175,6 +183,7 @@ export function canGovUserViewTab( case "codeChallenge": case "teamScenario": case "proposals": + case "evaluationPanel": return isOpportunityOwnerOrAdmin; case "instructions": case "overview": @@ -194,6 +203,12 @@ export function idToDefinition( icon: "file-code", title: "Opportunity" } as TabbedPage.TabDefinition; + case "evaluationPanel": + return { + component: EvaluationPanelTab.component, + icon: "users", + title: "Evaluation Panel" + } as TabbedPage.TabDefinition; case "addenda": return { component: AddendaTab.component, @@ -290,6 +305,9 @@ export function makeSidebarState( makeSidebarLink("summary", opportunity.id, activeTab), adt("heading", "Opportunity Management"), makeSidebarLink("opportunity", opportunity.id, activeTab), + ...(canGovUserViewTabs("evaluationPanel") + ? [makeSidebarLink("evaluationPanel", opportunity.id, activeTab)] + : []), //Only show Addenda sidebar link if opportunity can have addenda. ...(canAddAddendumToSWUOpportunity(opportunity) ? [makeSidebarLink("addenda", opportunity.id, activeTab)] diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/opportunity.tsx b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/opportunity.tsx index 986ae8011..de07a951b 100644 --- a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/opportunity.tsx +++ b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/opportunity.tsx @@ -1194,9 +1194,6 @@ export const component: Tab.Component = { }); case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: case SWUOpportunityStatus.EvaluationTeamQuestionsConsensus: - case SWUOpportunityStatus.EvaluationTeamQuestionsReview: - case SWUOpportunityStatus.EvaluationTeamQuestionsChairSubmission: - case SWUOpportunityStatus.EvaluationTeamQuestionsAdminReview: case SWUOpportunityStatus.EvaluationTeamQuestions: case SWUOpportunityStatus.EvaluationCodeChallenge: case SWUOpportunityStatus.EvaluationTeamScenario: diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/overview.tsx b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/overview.tsx index c199d374e..24b767b0a 100644 --- a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/overview.tsx +++ b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/edit/tab/overview.tsx @@ -32,8 +32,7 @@ import { compareSWUProposalAnonymousProponentNumber, getSWUProponentName, NUM_SCORE_DECIMALS, - SWUProposalSlim, - SWUProposalStatus + SWUProposalSlim } from "shared/lib/resources/proposal/sprint-with-us"; import { SWUTeamQuestionResponseEvaluation, @@ -121,9 +120,7 @@ const update: component_.page.Update = ({ evaluations.reduce( (acc, e) => acc || - (e.proposal.status === - SWUProposalStatus.EvaluationTeamQuestionsIndividual && - e.status === SWUTeamQuestionResponseEvaluationStatus.Draft), + e.status === SWUTeamQuestionResponseEvaluationStatus.Draft, false as boolean ) ), diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/components/form.tsx b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/components/form.tsx index dbcd243df..f915ba9af 100644 --- a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/components/form.tsx +++ b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/components/form.tsx @@ -18,7 +18,7 @@ import { import * as api from "front-end/lib/http/api"; import * as Phases from "front-end/lib/pages/opportunity/sprint-with-us/lib/components/phases"; import * as TeamQuestions from "front-end/lib/pages/opportunity/sprint-with-us/lib/components/team-questions"; -import * as EvaluationPanel from "front-end/lib/pages/opportunity/sprint-with-us/lib/components/evaluation-panel"; +import * as EvaluationPanel from "front-end/lib/components/evaluation-panel"; import Icon from "front-end/lib/views/icon"; import Link, { routeDest } from "front-end/lib/views/link"; import { flatten } from "lodash"; @@ -725,6 +725,7 @@ export function isAttachmentsTabValid(state: Immutable): boolean { export function isValid(state: Immutable): boolean { return ( isOverviewTabValid(state) && + isEvaluationPanelTabValid(state) && isDescriptionTabValid(state) && isPhasesTabValid(state) && isTeamQuestionsTabValid(state) && diff --git a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/index.ts b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/index.ts index 1e33d7488..5b5c9e6ee 100644 --- a/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/index.ts +++ b/src/front-end/typescript/lib/pages/opportunity/sprint-with-us/lib/index.ts @@ -20,9 +20,6 @@ export function swuOpportunityStatusToColor( return "success"; case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: case SWUOpportunityStatus.EvaluationTeamQuestionsConsensus: - case SWUOpportunityStatus.EvaluationTeamQuestionsReview: - case SWUOpportunityStatus.EvaluationTeamQuestionsChairSubmission: - case SWUOpportunityStatus.EvaluationTeamQuestionsAdminReview: return "warning"; case SWUOpportunityStatus.EvaluationTeamQuestions: return "warning"; @@ -51,9 +48,6 @@ export function swuOpportunityStatusToTitleCase( return "Published"; case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: case SWUOpportunityStatus.EvaluationTeamQuestionsConsensus: - case SWUOpportunityStatus.EvaluationTeamQuestionsReview: - case SWUOpportunityStatus.EvaluationTeamQuestionsChairSubmission: - case SWUOpportunityStatus.EvaluationTeamQuestionsAdminReview: return "Evaluation"; case SWUOpportunityStatus.EvaluationTeamQuestions: return "Team Questions"; diff --git a/src/front-end/typescript/lib/pages/proposal/sprint-with-us/lib/index.ts b/src/front-end/typescript/lib/pages/proposal/sprint-with-us/lib/index.ts index 8babff5d0..b25976dd8 100644 --- a/src/front-end/typescript/lib/pages/proposal/sprint-with-us/lib/index.ts +++ b/src/front-end/typescript/lib/pages/proposal/sprint-with-us/lib/index.ts @@ -18,8 +18,6 @@ export function swuProposalStatusToColor( case SWUProposalStatus.UnderReviewTeamQuestions: case SWUProposalStatus.UnderReviewCodeChallenge: case SWUProposalStatus.UnderReviewTeamScenario: - case SWUProposalStatus.EvaluationTeamQuestionsIndividual: - case SWUProposalStatus.EvaluationTeamQuestionsConsensus: return "warning"; case SWUProposalStatus.EvaluatedTeamQuestions: case SWUProposalStatus.EvaluatedCodeChallenge: @@ -42,14 +40,6 @@ export function swuProposalStatusToTitleCase( return "Draft"; case SWUProposalStatus.Submitted: return "Submitted"; - case SWUProposalStatus.EvaluationTeamQuestionsIndividual: - return viewerUserType === UserType.Vendor - ? "Under Review" - : "Panel Evaluation (Individual)"; - case SWUProposalStatus.EvaluationTeamQuestionsConsensus: - return viewerUserType === UserType.Vendor - ? "Under Review" - : "Panel Evaluation (Consensus)"; case SWUProposalStatus.UnderReviewTeamQuestions: return viewerUserType === UserType.Vendor ? "Under Review" diff --git a/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/proposal.tsx b/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/proposal.tsx index e32394772..db4989099 100644 --- a/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/proposal.tsx +++ b/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/proposal.tsx @@ -518,8 +518,6 @@ export const component: Tab.Component = { onClick: () => dispatch(adt("showModal", "award" as const)) } ]); - case SWUProposalStatus.EvaluationTeamQuestionsIndividual: - case SWUProposalStatus.EvaluationTeamQuestionsConsensus: case SWUProposalStatus.Draft: case SWUProposalStatus.Submitted: case SWUProposalStatus.Withdrawn: diff --git a/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/team-questions.tsx b/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/team-questions.tsx index 6bbcd6d7e..f5ed590c6 100644 --- a/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/team-questions.tsx +++ b/src/front-end/typescript/lib/pages/proposal/sprint-with-us/view/tab/team-questions.tsx @@ -27,14 +27,13 @@ import { // hasSWUOpportunityPassedCodeChallenge, hasSWUOpportunityPassedTeamQuestions, hasSWUOpportunityPassedTeamQuestionsEvaluation, - SWUOpportunity + SWUOpportunity, + SWUOpportunityStatus } from "shared/lib/resources/opportunity/sprint-with-us"; import { getSWUProponentName, NUM_SCORE_DECIMALS, SWUProposal, - SWUProposalStatus, - // SWUProposalStatus, SWUProposalTeamQuestionResponse } from "shared/lib/resources/proposal/sprint-with-us"; import { @@ -1470,7 +1469,7 @@ export const component: Tab.Component = { getActions: ({ state, dispatch }) => { const proposal = state.proposal; - const propStatus = proposal.status; + const oppStatus = proposal.opportunity.status; const isSaveLoading = state.saveLoading > 0; const isStartEditingLoading = state.startEditingLoading > 0; const isLoading = isSaveLoading || isStartEditingLoading; @@ -1498,8 +1497,8 @@ export const component: Tab.Component = { } ]); } - switch (propStatus) { - case SWUProposalStatus.EvaluationTeamQuestionsIndividual: + switch (oppStatus) { + case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: return component_.page.actions.links( state.evaluating ? state.questionEvaluation @@ -1537,24 +1536,21 @@ export const component: Tab.Component = { ] : [] ); - case SWUProposalStatus.EvaluationTeamQuestionsConsensus: + case SWUOpportunityStatus.EvaluationTeamQuestionsConsensus: return component_.page.actions.links( state.evaluating ? state.questionEvaluation - ? state.questionEvaluation.status === - SWUTeamQuestionResponseEvaluationStatus.Draft - ? [ - { - children: "Edit", - onClick: () => dispatch(adt("startEditingConsensus")), - button: true, - loading: isStartEditingLoading, - disabled: isLoading, - symbol_: leftPlacement(iconLinkSymbol("edit")), - color: "primary" - } - ] - : [] + ? [ + { + children: "Edit", + onClick: () => dispatch(adt("startEditingConsensus")), + button: true, + loading: isStartEditingLoading, + disabled: isLoading, + symbol_: leftPlacement(iconLinkSymbol("edit")), + color: "primary" + } + ] : [ { children: "Save Draft", @@ -1575,45 +1571,6 @@ export const component: Tab.Component = { ] : [] ); - // case SWUProposalStatus.EvaluatedTeamQuestions: - // return component_.page.actions.links([ - // ...(canSWUOpportunityBeScreenedInToCodeChallenge(state.opportunity) - // ? [ - // { - // children: "Screen In", - // symbol_: leftPlacement(iconLinkSymbol("stars")), - // loading: isScreenToFromLoading, - // disabled: isScreenToFromLoading, - // button: true, - // color: "primary" as const, - // onClick: () => dispatch(adt("screenIn" as const)) - // } - // ] - // : []), - // { - // children: "Edit Score", - // symbol_: leftPlacement(iconLinkSymbol("star-full")), - // disabled: isScreenToFromLoading, - // button: true, - // color: "info", - // onClick: () => dispatch(adt("showModal", "enterScore" as const)) - // } - // ]) as component_.page.Actions; - // case SWUProposalStatus.UnderReviewCodeChallenge: - // if (hasSWUOpportunityPassedCodeChallenge(state.opportunity)) { - // return component_.page.actions.none(); - // } - // return component_.page.actions.links([ - // { - // children: "Screen Out", - // symbol_: leftPlacement(iconLinkSymbol("ban")), - // loading: isScreenToFromLoading, - // disabled: isScreenToFromLoading, - // button: true, - // color: "danger", - // onClick: () => dispatch(adt("screenOut" as const)) - // } - // ]); default: return component_.page.actions.none(); } diff --git a/src/migrations/tasks/20240718222006_swu-evaluation-tables.ts b/src/migrations/tasks/20240718222006_swu-evaluation-tables.ts index 819246739..0279160ce 100644 --- a/src/migrations/tasks/20240718222006_swu-evaluation-tables.ts +++ b/src/migrations/tasks/20240718222006_swu-evaluation-tables.ts @@ -10,9 +10,6 @@ enum SWUOpportunityStatus { Published = "PUBLISHED", EvaluationTeamQuestionsIndividual = "EVAL_QUESTIONS_INDIVIDUAL", EvaluationTeamQuestionsConsensus = "EVAL_QUESTIONS_CONSENSUS", - EvaluationTeamQuestionsReview = "EVAL_QUESTIONS_REVIEW", - EvaluationTeamQuestionsChairSubmission = "EVAL_QUESTIONS_CHAIR_SUBMISSION", - EvaluationTeamQuestionsAdminReview = "EVAL_QUESTIONS_ADMIN_REVIEW", EvaluationTeamQuestions = "EVAL_QUESTIONS", // TODO: Remove EvaluationCodeChallenge = "EVAL_CC", EvaluationTeamScenario = "EVAL_SCENARIO", @@ -36,10 +33,8 @@ enum PreviousSWUOpportunityStatus { enum SWUProposalStatus { Draft = "DRAFT", Submitted = "SUBMITTED", - EvaluationTeamQuestionsIndividual = "EVALUATION_QUESTIONS_INDIVIDUAL", - EvaluationTeamQuestionsConsensus = "EVALUATION_QUESTIONS_CONSENSUS", - UnderReviewTeamQuestions = "UNDER_REVIEW_QUESTIONS", // TODO: Remove - EvaluatedTeamQuestions = "EVALUATED_QUESTIONS", // TODO: Remove + UnderReviewTeamQuestions = "UNDER_REVIEW_QUESTIONS", + EvaluatedTeamQuestions = "EVALUATED_QUESTIONS", UnderReviewCodeChallenge = "UNDER_REVIEW_CODE_CHALLENGE", EvaluatedCodeChallenge = "EVALUATED_CODE_CHALLENGE", UnderReviewTeamScenario = "UNDER_REVIEW_TEAM_SCENARIO", diff --git a/src/shared/lib/resources/opportunity/sprint-with-us.ts b/src/shared/lib/resources/opportunity/sprint-with-us.ts index ee5b25f2a..c13894ed8 100644 --- a/src/shared/lib/resources/opportunity/sprint-with-us.ts +++ b/src/shared/lib/resources/opportunity/sprint-with-us.ts @@ -59,10 +59,7 @@ export enum SWUOpportunityStatus { Published = "PUBLISHED", EvaluationTeamQuestionsIndividual = "EVAL_QUESTIONS_INDIVIDUAL", EvaluationTeamQuestionsConsensus = "EVAL_QUESTIONS_CONSENSUS", - EvaluationTeamQuestionsReview = "EVAL_QUESTIONS_REVIEW", - EvaluationTeamQuestionsChairSubmission = "EVAL_QUESTIONS_CHAIR_SUBMISSION", - EvaluationTeamQuestionsAdminReview = "EVAL_QUESTIONS_ADMIN_REVIEW", - EvaluationTeamQuestions = "EVAL_QUESTIONS", // TODO: Remove + EvaluationTeamQuestions = "EVAL_QUESTIONS", EvaluationCodeChallenge = "EVAL_CC", EvaluationTeamScenario = "EVAL_SCENARIO", Awarded = "AWARDED", @@ -118,9 +115,6 @@ export function isSWUOpportunityStatusInEvaluation( switch (s) { case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: case SWUOpportunityStatus.EvaluationTeamQuestionsConsensus: - case SWUOpportunityStatus.EvaluationTeamQuestionsReview: - case SWUOpportunityStatus.EvaluationTeamQuestionsChairSubmission: - case SWUOpportunityStatus.EvaluationTeamQuestionsAdminReview: case SWUOpportunityStatus.EvaluationTeamQuestions: case SWUOpportunityStatus.EvaluationCodeChallenge: case SWUOpportunityStatus.EvaluationTeamScenario: @@ -134,9 +128,6 @@ export const publicOpportunityStatuses: readonly SWUOpportunityStatus[] = [ SWUOpportunityStatus.Published, SWUOpportunityStatus.EvaluationTeamQuestionsIndividual, SWUOpportunityStatus.EvaluationTeamQuestionsConsensus, - SWUOpportunityStatus.EvaluationTeamQuestionsReview, - SWUOpportunityStatus.EvaluationTeamQuestionsChairSubmission, - SWUOpportunityStatus.EvaluationTeamQuestionsAdminReview, SWUOpportunityStatus.EvaluationTeamQuestions, SWUOpportunityStatus.EvaluationCodeChallenge, SWUOpportunityStatus.EvaluationTeamScenario, @@ -424,7 +415,8 @@ export type UpdateRequestBody = | ADT< "submitConsensusQuestionEvaluations", SubmitQuestionEvaluationsWithNoteRequestBody - >; + > + | ADT<"editEvaluationPanel", CreateSWUEvaluationPanelMemberBody[]>; export type UpdateEditRequestBody = Omit; @@ -455,6 +447,7 @@ type UpdateADTErrors = | ADT<"addNote", UpdateWithNoteValidationErrors> | ADT<"submitIndividualQuestionEvaluations", string[]> | ADT<"submitConsensusQuestionEvaluations", string[]> + | ADT<"editEvaluationPanel", UpdateEditValidationErrors> | ADT<"parseFailure">; export interface UpdateValidationErrors extends BodyWithErrors { @@ -636,9 +629,6 @@ export function canAddAddendumToSWUOpportunity(o: SWUOpportunity): boolean { case SWUOpportunityStatus.Published: case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: case SWUOpportunityStatus.EvaluationTeamQuestionsConsensus: - case SWUOpportunityStatus.EvaluationTeamQuestionsReview: - case SWUOpportunityStatus.EvaluationTeamQuestionsChairSubmission: - case SWUOpportunityStatus.EvaluationTeamQuestionsAdminReview: case SWUOpportunityStatus.EvaluationTeamQuestions: case SWUOpportunityStatus.EvaluationCodeChallenge: case SWUOpportunityStatus.EvaluationTeamScenario: @@ -651,6 +641,18 @@ export function canAddAddendumToSWUOpportunity(o: SWUOpportunity): boolean { } } +export function canChangeEvaluationPanel(o: SWUOpportunity): boolean { + switch (o.status) { + case SWUOpportunityStatus.Draft: + case SWUOpportunityStatus.UnderReview: + case SWUOpportunityStatus.Published: + case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: + return true; + default: + return false; + } +} + export function isSWUOpportunityClosed(o: SWUOpportunity): boolean { return ( isDateInThePast(o.proposalDeadline) && @@ -674,9 +676,6 @@ export function hasSWUOpportunityPassedTeamQuestionsEvaluation( switch (h.type.value) { case SWUOpportunityStatus.EvaluationTeamQuestionsIndividual: case SWUOpportunityStatus.EvaluationTeamQuestionsConsensus: - case SWUOpportunityStatus.EvaluationTeamQuestionsReview: - case SWUOpportunityStatus.EvaluationTeamQuestionsChairSubmission: - case SWUOpportunityStatus.EvaluationTeamQuestionsAdminReview: case SWUOpportunityStatus.EvaluationTeamQuestions: case SWUOpportunityStatus.EvaluationCodeChallenge: case SWUOpportunityStatus.EvaluationTeamScenario: diff --git a/src/shared/lib/resources/proposal/sprint-with-us.ts b/src/shared/lib/resources/proposal/sprint-with-us.ts index 0817dd688..8046c1ea2 100644 --- a/src/shared/lib/resources/proposal/sprint-with-us.ts +++ b/src/shared/lib/resources/proposal/sprint-with-us.ts @@ -50,10 +50,8 @@ export function swuProposalPhaseTypeToTitleCase( export enum SWUProposalStatus { Draft = "DRAFT", Submitted = "SUBMITTED", - EvaluationTeamQuestionsIndividual = "EVALUATION_QUESTIONS_INDIVIDUAL", - EvaluationTeamQuestionsConsensus = "EVALUATION_QUESTIONS_CONSENSUS", - UnderReviewTeamQuestions = "UNDER_REVIEW_QUESTIONS", // TODO: Remove - EvaluatedTeamQuestions = "EVALUATED_QUESTIONS", // TODO: Remove + UnderReviewTeamQuestions = "UNDER_REVIEW_QUESTIONS", + EvaluatedTeamQuestions = "EVALUATED_QUESTIONS", UnderReviewCodeChallenge = "UNDER_REVIEW_CODE_CHALLENGE", EvaluatedCodeChallenge = "EVALUATED_CODE_CHALLENGE", UnderReviewTeamScenario = "UNDER_REVIEW_TEAM_SCENARIO", @@ -109,8 +107,6 @@ function quantifySWUProposalStatusForSort(a: SWUProposalStatus): number { return 0; case SWUProposalStatus.NotAwarded: return 1; - case SWUProposalStatus.EvaluationTeamQuestionsIndividual: - case SWUProposalStatus.EvaluationTeamQuestionsConsensus: case SWUProposalStatus.UnderReviewTeamQuestions: case SWUProposalStatus.EvaluatedTeamQuestions: case SWUProposalStatus.UnderReviewCodeChallenge: diff --git a/src/shared/lib/resources/question-evaluation/sprint-with-us.ts b/src/shared/lib/resources/question-evaluation/sprint-with-us.ts index 0f1fff429..8718aae9b 100644 --- a/src/shared/lib/resources/question-evaluation/sprint-with-us.ts +++ b/src/shared/lib/resources/question-evaluation/sprint-with-us.ts @@ -93,7 +93,7 @@ export interface UpdateValidationErrors extends BodyWithErrors { evaluation?: UpdateADTErrors; } -export function isValidStatusChange( +export function isValidEvaluationStatusChange( from: SWUTeamQuestionResponseEvaluationStatus, to: SWUTeamQuestionResponseEvaluationStatus ): boolean { @@ -104,3 +104,16 @@ export function isValidStatusChange( return false; } } + +export function isValidConsensusStatusChange( + from: SWUTeamQuestionResponseEvaluationStatus, + to: SWUTeamQuestionResponseEvaluationStatus +): boolean { + switch (from) { + case SWUTeamQuestionResponseEvaluationStatus.Draft: + case SWUTeamQuestionResponseEvaluationStatus.Submitted: + return SWUTeamQuestionResponseEvaluationStatus.Submitted === to; + default: + return false; + } +}