diff --git a/apps/backend/src/config/Tokens.ts b/apps/backend/src/config/Tokens.ts index d5a07cacc8..2c48780fcd 100644 --- a/apps/backend/src/config/Tokens.ts +++ b/apps/backend/src/config/Tokens.ts @@ -50,6 +50,7 @@ export const Tokens = { PredefinedMessageDataSource: Symbol('PredefinedMessageDataSource'), InternalReviewDataSource: Symbol('InternalReviewDataSource'), FapDataColumns: Symbol('FapDataColumns'), + CallExtraFapDataColumns: Symbol('CallExtraFapDataColumns'), FapDataRow: Symbol('FapDataRow'), PopulateRow: Symbol('PopulateRow'), PopulateCallRow: Symbol('PopulateCallRow'), diff --git a/apps/backend/src/config/dependencyConfigDLS.ts b/apps/backend/src/config/dependencyConfigDLS.ts index 0e506f2ec2..7f357857e9 100644 --- a/apps/backend/src/config/dependencyConfigDLS.ts +++ b/apps/backend/src/config/dependencyConfigDLS.ts @@ -55,12 +55,15 @@ import { createPostToRabbitMQHandler, } from '../eventHandlers/messageBroker'; import { createApplicationEventBus } from '../events'; -import { FapDataColumns } from '../factory/xlsx/FapDataColumns'; import { - callFapPopulateRow, - getDataRow, - populateRow, -} from '../factory/xlsx/FapDataRow'; + DLSCallExtraFapDataColumns, + DLSFapDataColumns, +} from '../factory/xlsx/dls/DLSFapDataColumns'; +import { + callFapDLSPopulateRow, + getDLSDataRow, + populateDLSRow, +} from '../factory/xlsx/dls/DLSFapDataRow'; import BasicUserDetailsLoader from '../loaders/BasicUserDetailsLoader'; import { SkipAssetRegistrar } from '../services/assetRegistrar/skip/SkipAssetRegistrar'; @@ -127,10 +130,11 @@ mapClass(Tokens.AssetRegistrar, SkipAssetRegistrar); mapClass(Tokens.MailService, SMTPMailService); -mapValue(Tokens.FapDataColumns, FapDataColumns); -mapValue(Tokens.FapDataRow, getDataRow); -mapValue(Tokens.PopulateRow, populateRow); -mapValue(Tokens.PopulateCallRow, callFapPopulateRow); +mapValue(Tokens.FapDataColumns, DLSFapDataColumns); +mapValue(Tokens.CallExtraFapDataColumns, DLSCallExtraFapDataColumns); +mapValue(Tokens.FapDataRow, getDLSDataRow); +mapValue(Tokens.PopulateRow, populateDLSRow); +mapValue(Tokens.PopulateCallRow, callFapDLSPopulateRow); mapValue(Tokens.EmailEventHandler, DLSEmailHandler); mapClass(Tokens.EmailTemplateDataSource, PostgresEmailTemplateDataSource); diff --git a/apps/backend/src/factory/xlsx/FapDataRow.ts b/apps/backend/src/factory/xlsx/FapDataRow.ts index b42f2a9264..bdfc95ee91 100644 --- a/apps/backend/src/factory/xlsx/FapDataRow.ts +++ b/apps/backend/src/factory/xlsx/FapDataRow.ts @@ -1,19 +1,41 @@ import { CallRowObj } from './callFaps'; import { RowObj } from './fap'; +import { QuestionaryStep } from '../../models/Questionary'; +import { Review } from '../../models/Review'; -export function getDataRow( - proposalPk: number, - piName: string, - proposalAverageScore: number, - instrumentName: string, - instrumentAvailabilityTime: number, - fapTimeAllocation: number | null, - proposalTitle: string, - proposalId: number | null, - techReviewTimeAllocation: number | null, - technicalReviewComment: string | null, - propFapRankOrder: number | null -): RowObj { +export type FapDataRowInput = { + proposalPk: number; + piName: string; + proposalAverageScore: number; + instrumentName: string; + instrumentAvailabilityTime: number; + fapTimeAllocation: number | null; + proposalTitle: string; + proposalId: number | null; + techReviewTimeAllocation: number | null; + technicalReviewComment: string | null; + propFapRankOrder: number | null; + proposerId: number | null; + proposalAnswers: QuestionaryStep[] | null; + reviews: Review[] | null; + instrumentId: number; +}; + +export type FapDataRow = (input: FapDataRowInput) => RowObj | Promise; + +export function getDataRow({ + proposalPk, + piName, + proposalAverageScore, + instrumentName, + instrumentAvailabilityTime, + fapTimeAllocation, + proposalTitle, + proposalId, + techReviewTimeAllocation, + technicalReviewComment, + propFapRankOrder, +}: FapDataRowInput): RowObj { return { proposalPk: proposalPk, propShortCode: proposalId?.toString(), diff --git a/apps/backend/src/factory/xlsx/callFaps.ts b/apps/backend/src/factory/xlsx/callFaps.ts index fab85fc454..415d6889b7 100644 --- a/apps/backend/src/factory/xlsx/callFaps.ts +++ b/apps/backend/src/factory/xlsx/callFaps.ts @@ -108,7 +108,7 @@ export const collectCallFapXLSXData = async ( return { data: baseData, filename: filename.replace(/\s+/g, '_') }; }; -export const CallExtraFapDataColumns = [ +export const DefaultCallExtraFapDataColumns = [ 'Fap Time allocation', 'Fap Meeting Decision', 'Fap Meeting Comment for User', diff --git a/apps/backend/src/factory/xlsx/dls/DLSFapDataColumns.ts b/apps/backend/src/factory/xlsx/dls/DLSFapDataColumns.ts new file mode 100644 index 0000000000..e8e7e96ad3 --- /dev/null +++ b/apps/backend/src/factory/xlsx/dls/DLSFapDataColumns.ts @@ -0,0 +1,20 @@ +export const DLSFapDataColumns = [ + 'Proposal Reference Number', + 'Proposal Title', + 'Principal Investigator', + 'Institution', + 'Instrument', + 'Instrument available Time', + 'TA Recommended Time', + 'FAP Recommended Time', + 'Average Score', + 'Current Rank', + 'Is in availability zone', +]; + +export const DLSCallExtraFapDataColumns = [ + 'FAP Meeting Recommendation', + 'FAP Meeting Comment for User', + 'FAP Meeting Internal Comment', + 'Instrument Requested Time', +]; diff --git a/apps/backend/src/factory/xlsx/dls/DLSFapDataRow.ts b/apps/backend/src/factory/xlsx/dls/DLSFapDataRow.ts new file mode 100644 index 0000000000..99e70bb105 --- /dev/null +++ b/apps/backend/src/factory/xlsx/dls/DLSFapDataRow.ts @@ -0,0 +1,72 @@ +import { container } from 'tsyringe'; + +import { Tokens } from '../../../config/Tokens'; +import { ProposalDataSource } from '../../../datasources/ProposalDataSource'; +import { UserDataSource } from '../../../datasources/UserDataSource'; +import { CallRowObj } from '../callFaps'; +import { RowObj } from '../fap'; +import { FapDataRowInput, getDataRow } from '../FapDataRow'; + +type DLSFapRowObj = RowObj & { + instrumentRequestedTime: number | null | undefined; +}; + +function nullFieldHelper( + data: string | number | null | undefined +): string | number { + return data ?? ''; +} + +export async function getDLSDataRow( + input: FapDataRowInput +): Promise { + const { proposalPk, proposerId, instrumentId } = input; + const userDataSource = container.resolve( + Tokens.UserDataSource + ); + const pi = proposerId + ? await userDataSource.getBasicUserInfo(proposerId) + : null; + + const proposalDataSource = container.resolve( + Tokens.ProposalDataSource + ); + const instrumentRequestedTime = await proposalDataSource.getRequestedTime( + proposalPk, + instrumentId + ); + + return { + ...getDataRow(input), + piOrg: pi?.institution, + instrumentRequestedTime, + }; +} + +export function populateDLSRow(row: DLSFapRowObj): (string | number)[] { + return [ + nullFieldHelper(row.propShortCode), + nullFieldHelper(row.propTitle), + nullFieldHelper(row.principalInv), + nullFieldHelper(row.piOrg), + nullFieldHelper(row.instrName), + nullFieldHelper(row.instrAvailTime), + nullFieldHelper(row.techReviewTimeAllocation), + nullFieldHelper(row.fapTimeAllocation ?? row.techReviewTimeAllocation), + nullFieldHelper(row.propReviewAvgScore), + nullFieldHelper(row.propFapRankOrder), + nullFieldHelper(row.inAvailZone), + ]; +} + +export function callFapDLSPopulateRow( + row: CallRowObj & DLSFapRowObj +): (string | number)[] { + return [ + ...populateDLSRow(row), + nullFieldHelper(row.fapMeetingDecision), + nullFieldHelper(row.fapMeetingInComment), + nullFieldHelper(row.fapMeetingExComment), + nullFieldHelper(row.instrumentRequestedTime), + ].concat(row.reviews ? row.reviews.flatMap((review) => review) : []); +} diff --git a/apps/backend/src/factory/xlsx/fap.ts b/apps/backend/src/factory/xlsx/fap.ts index ca0c1ece01..358e1846e1 100644 --- a/apps/backend/src/factory/xlsx/fap.ts +++ b/apps/backend/src/factory/xlsx/fap.ts @@ -2,9 +2,8 @@ import { groupBy } from 'lodash'; import { container } from 'tsyringe'; import { collectCallFapXLSXData } from './callFaps'; -import { getDataRow } from './FapDataRow'; +import { FapDataRow, FapDataRowInput } from './FapDataRow'; import baseContext from '../../buildContext'; -import { getStfcDataRow } from './stfc/StfcFapDataRow'; import { Tokens } from '../../config/Tokens'; import { FapDataSource } from '../../datasources/FapDataSource'; import { UserWithRole } from '../../models/User'; @@ -36,9 +35,7 @@ export type RowObj = { accessRoute?: string | null; }; -const fapDataRow = container.resolve( - Tokens.FapDataRow -); +const fapDataRow = container.resolve(Tokens.FapDataRow); const populateRow = container.resolve<(row: RowObj) => (string | number)[]>( Tokens.PopulateRow @@ -118,22 +115,25 @@ export const collectFapXLSXRowData = async ( { proposalPk: proposal.proposal_pk, fapId: fapId } ); - return fapDataRow( - proposal.proposal_pk, - piFullName, - proposal.average_grade, - proposal.instrument_name, - proposal.availability_time, - proposal.fap_time_allocation, - proposal.title, - proposal.proposal_id, - proposal.time_allocation, - stripHtml(proposal.comment ?? ''), - proposal.rank_order, - proposal.proposer_id, + const rowInput: FapDataRowInput = { + proposalPk: proposal.proposal_pk, + piName: piFullName, + proposalAverageScore: proposal.average_grade, + instrumentName: proposal.instrument_name, + instrumentAvailabilityTime: proposal.availability_time, + fapTimeAllocation: proposal.fap_time_allocation, + proposalTitle: proposal.title, + proposalId: proposal.proposal_id, + techReviewTimeAllocation: proposal.time_allocation, + technicalReviewComment: stripHtml(proposal.comment ?? ''), + propFapRankOrder: proposal.rank_order, + proposerId: proposal.proposer_id, proposalAnswers, - reviews - ); + reviews, + instrumentId: proposal.instrument_id, + }; + + return fapDataRow(rowInput); }) ); diff --git a/apps/backend/src/factory/xlsx/stfc/StfcFapDataRow.ts b/apps/backend/src/factory/xlsx/stfc/StfcFapDataRow.ts index b5b489adf8..351599b25a 100644 --- a/apps/backend/src/factory/xlsx/stfc/StfcFapDataRow.ts +++ b/apps/backend/src/factory/xlsx/stfc/StfcFapDataRow.ts @@ -2,29 +2,13 @@ import { container } from 'tsyringe'; import { Tokens } from '../../../config/Tokens'; import { StfcUserDataSource } from '../../../datasources/stfc/StfcUserDataSource'; -import { QuestionaryStep } from '../../../models/Questionary'; -import { Review } from '../../../models/Review'; import { stripHtml } from '../../../utils/stringStripHtml'; import { CallRowObj } from '../callFaps'; import { RowObj } from '../fap'; -import { getDataRow } from '../FapDataRow'; - -export async function getStfcDataRow( - proposalPk: number, - piName: string, - proposalAverageScore: number, - instrument: string, - instrumentAvailabilityTime: number, - fapTimeAllocation: number | null, - proposalTitle: string, - proposalId: number | null, - technicalReviewTimeAllocation: number | null, - technicalReviewComment: string | null, - propFapRankOrder: number | null, - proposer_id: number | null, - proposalAnswers: QuestionaryStep[] | null, - reviews: Review[] | null -) { +import { FapDataRowInput, getDataRow } from '../FapDataRow'; + +export async function getStfcDataRow(input: FapDataRowInput) { + const { proposerId, proposalAnswers, reviews } = input; const stfcUserDataSource: StfcUserDataSource = container.resolve( Tokens.UserDataSource ) as StfcUserDataSource; @@ -65,31 +49,19 @@ export async function getStfcDataRow( )?.value; const piDetails = await stfcUserDataSource.getStfcBasicPeopleByUserNumbers([ - proposer_id?.toString() ?? '', + proposerId?.toString() ?? '', ]); const piCountry = piDetails.find( - (user) => user.userNumber === proposer_id?.toString() + (user) => user.userNumber === proposerId?.toString() )?.country; const piOrg = piDetails.find( - (user) => user.userNumber === proposer_id?.toString() + (user) => user.userNumber === proposerId?.toString() )?.orgName; return { - ...getDataRow( - proposalPk, - piName, - proposalAverageScore, - instrument, - instrumentAvailabilityTime, - fapTimeAllocation, - proposalTitle, - proposalId, - technicalReviewTimeAllocation, - technicalReviewComment, - propFapRankOrder - ), + ...getDataRow(input), accessRoute, timeRequested, reviews: individualReviews, diff --git a/apps/backend/src/middlewares/factory/xlsx.ts b/apps/backend/src/middlewares/factory/xlsx.ts index 64c9861f89..fefde3d0b7 100644 --- a/apps/backend/src/middlewares/factory/xlsx.ts +++ b/apps/backend/src/middlewares/factory/xlsx.ts @@ -10,7 +10,7 @@ import callFactoryService, { } from '../../factory/service'; import { getCurrentTimestamp } from '../../factory/util'; import { - CallExtraFapDataColumns, + DefaultCallExtraFapDataColumns, collectCallFapXLSXData, } from '../../factory/xlsx/callFaps'; import { collectFapXLSXData } from '../../factory/xlsx/fap'; @@ -21,6 +21,11 @@ import { } from '../../factory/xlsx/proposal'; const fapDataColumns = container.resolve(Tokens.FapDataColumns); +const callExtraFapDataColumns = container.isRegistered( + Tokens.CallExtraFapDataColumns +) + ? container.resolve(Tokens.CallExtraFapDataColumns) + : DefaultCallExtraFapDataColumns; const router = express.Router(); @@ -147,7 +152,7 @@ router.get(`/${XLSXType.CALL_FAP}/:call_id`, async (req, res, next) => { const meta: XLSXMetaBase = { singleFilename: filename, collectionFilename: filename, - columns: fapDataColumns.concat(CallExtraFapDataColumns), + columns: fapDataColumns.concat(callExtraFapDataColumns), }; const userRole = req.user.currentRole; diff --git a/apps/e2e/cypress/fixtures/exampleCallFapExportDLS.json b/apps/e2e/cypress/fixtures/exampleCallFapExportDLS.json new file mode 100644 index 0000000000..134b752901 --- /dev/null +++ b/apps/e2e/cypress/fixtures/exampleCallFapExportDLS.json @@ -0,0 +1,37 @@ +[ + { "Proposal Reference Number": "Central lavender" }, + { + "Proposal Reference Number": "567122", + "Proposal Title": "lumen proofread hertz", + "Principal Investigator": "Carl Carlsson", + "Institution": "Other", + "Instrument": "Central lavender", + "Instrument available Time": 20, + "TA Recommended Time": 25, + "FAP Recommended Time": 25, + "Average Score": 3, + "Current Rank": "", + "Is in availability zone": "no", + "FAP Time Allocation": "", + "FAP Meeting Recommendation": "Accepted", + "FAP Meeting Comment for User": "test", + "FAP Meeting Internal Comment": "test" + }, + { + "Proposal Reference Number": "701367", + "Proposal Title": "web Connecticut driver", + "Principal Investigator": "Carl Carlsson", + "Institution": "Other", + "Instrument": "Central lavender", + "Instrument available Time": 20, + "TA Recommended Time": 5, + "FAP Recommended Time": 5, + "Average Score": 3, + "Current Rank": "", + "Is in availability zone": "no", + "FAP Time Allocation": "", + "FAP Meeting Recommendation": "Accepted", + "FAP Meeting Comment for User": "test2", + "FAP Meeting Internal Comment": "test2" + } +]