diff --git a/apps/backend/src/datasources/ProposalDataSource.ts b/apps/backend/src/datasources/ProposalDataSource.ts index eb36715183..d1a046eef0 100644 --- a/apps/backend/src/datasources/ProposalDataSource.ts +++ b/apps/backend/src/datasources/ProposalDataSource.ts @@ -9,6 +9,7 @@ import { PaginationSortDirection } from '../utils/pagination'; import { ProposalsFilter } from './../resolvers/queries/ProposalsQuery'; export interface ProposalDataSource { + getRequestedTime(proposalPk: number, instrumentId: number): Promise; getProposalsFromView( filter?: ProposalsFilter, first?: number, diff --git a/apps/backend/src/datasources/mockups/ProposalDataSource.ts b/apps/backend/src/datasources/mockups/ProposalDataSource.ts index 3b4813af74..7c86693090 100644 --- a/apps/backend/src/datasources/mockups/ProposalDataSource.ts +++ b/apps/backend/src/datasources/mockups/ProposalDataSource.ts @@ -374,4 +374,11 @@ export class ProposalDataSourceMock implements ProposalDataSource { getInvitedProposal(inviteId: number): Promise { throw new Error('Method not implemented.'); } + + async getRequestedTime( + proposalPk: number, + instrumentId: number + ): Promise { + return 32; + } } diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index 5dc5585630..91fbf40d84 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -312,6 +312,32 @@ export default class PostgresProposalDataSource implements ProposalDataSource { }); } + async getRequestedTime( + proposalPk: number, + instrumentId: number + ): Promise { + //Non-nullable, time is either requested for the instrument or it is zero. + const result = await database('proposals as p') + .sum({ + total_time_requested: database.raw( + "(a.answer->'value'->>'timeRequested')::numeric" + ), + }) + .innerJoin('questionaries as q2', 'q2.questionary_id', 'p.questionary_id') + .innerJoin('answers as a', 'a.questionary_id', 'q2.questionary_id') + .innerJoin('questions as q', 'q.question_id', 'a.question_id') + .where('p.proposal_pk', proposalPk) + .where('q.data_type', 'INSTRUMENT_PICKER') + .whereRaw("a.answer->'value'->>'instrumentId' = ?", [ + instrumentId.toString(), + ]) + .first(); + + return result?.total_time_requested + ? Number(result.total_time_requested) + : 0; + } + async create( proposer_id: number, call_id: number, diff --git a/apps/backend/src/resolvers/queries/ProposalQuery.ts b/apps/backend/src/resolvers/queries/ProposalQuery.ts index 9fae7e40ba..cf0dbc4cc0 100644 --- a/apps/backend/src/resolvers/queries/ProposalQuery.ts +++ b/apps/backend/src/resolvers/queries/ProposalQuery.ts @@ -1,7 +1,11 @@ +import { container } from 'tsyringe'; import { Query, Ctx, Resolver, Arg, Int } from 'type-graphql'; +import { Tokens } from '../../config/Tokens'; import { ResolverContext } from '../../context'; +import { ProposalDataSource } from '../../datasources/ProposalDataSource'; import { Proposal } from '../types/Proposal'; + @Resolver() export class ProposalQuery { @Query(() => Proposal, { nullable: true }) @@ -19,4 +23,21 @@ export class ProposalQuery { ): Promise { return context.queries.proposal.get(context.user, proposalPk) !== null; } + + @Query(() => Number, { nullable: false }) + async proposalTimeRequested( + @Arg('proposalPk', () => Int) proposalPk: number, + @Arg('instrumentId', () => Int) instrumentId: number, + @Ctx() context: ResolverContext + ): Promise { + const proposalDataSource = container.resolve( + Tokens.ProposalDataSource + ); + const timeRequested = await proposalDataSource.getRequestedTime( + proposalPk, + instrumentId + ); + + return timeRequested || 0; + } } diff --git a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx index ae91db4096..27ee431986 100644 --- a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx +++ b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx @@ -42,6 +42,7 @@ type FapProposalWithAverageScoreAndAvailabilityZone = FapProposal & { proposalAverageScore: number | string; proposalDeviation: number | string; isInAvailabilityZone: boolean; + timeRequested: number; tableData?: { index: number; id: number }; }; @@ -152,7 +153,7 @@ const FapInstrumentProposalsTable = ({ }, }, { - title: 'Principal Investigator', + title: 'Principal investigator', render: (rowData: FapProposal) => { return getFullUserName(rowData.proposal.proposer); }, @@ -185,6 +186,10 @@ const FapInstrumentProposalsTable = ({ return rankOrder || '-'; }, }, + { + title: 'Time requested', + field: 'timeRequested', + }, { title: 'Time allocation', field: 'timeAllocation', @@ -223,13 +228,16 @@ const FapInstrumentProposalsTable = ({ ]; // NOTE: This is needed for adding the allocation time unit information on the column title without causing some console warning on re-rendering. - const columns = assignmentColumns.map((column) => ({ - ...column, - title: - column.field === 'timeAllocation' - ? `${column.title} (${selectedCall?.allocationTimeUnit}s)` - : column.title, - })); + const columns = assignmentColumns.map((column) => { + if (column.field === 'timeAllocation' || column.field === 'timeRequested') { + return { + ...column, + title: `${column.title} (${selectedCall?.allocationTimeUnit}s)`, + }; + } + + return column; + }); const DragState = { row: -1, @@ -272,11 +280,11 @@ const FapInstrumentProposalsTable = ({ useState([]); useEffect(() => { - const sortByRankOrAverageScore = (data: FapProposal[]) => { + const sortByRankOrAverageScore = async (data: FapProposal[]) => { let allocationTimeSum = 0; - return data - .map((proposalData) => { + const returnData = await Promise.all( + data.map(async (proposalData) => { const proposalAverageScore = average( getGradesFromReviews(proposalData.proposal.reviews ?? []) ); @@ -284,6 +292,13 @@ const FapInstrumentProposalsTable = ({ getGradesFromReviews(proposalData.proposal.reviews ?? []) ); + const result = await api().getProposalTimeRequested({ + proposalPk: proposalData.proposal.primaryKey, + instrumentId: fapInstrument.id, + }); + + const timeRequested = result.proposalTimeRequested; + return { ...proposalData, proposalAverageScore: isNaN(proposalAverageScore) @@ -292,8 +307,12 @@ const FapInstrumentProposalsTable = ({ proposalDeviation: isNaN(proposalDeviation) ? '-' : proposalDeviation, + timeRequested, }; }) + ); + + const sortedData = await returnData .sort((a, b) => { if ( typeof a.proposalDeviation === 'number' && @@ -343,10 +362,20 @@ const FapInstrumentProposalsTable = ({ }; } }); + + return sortedData; }; - const sortedProposals = sortByRankOrAverageScore(instrumentProposalsData); - setSortedProposalsWithAverageScore(sortedProposals); + const run = async () => { + const sortedProposals = await sortByRankOrAverageScore( + instrumentProposalsData + ); + setSortedProposalsWithAverageScore(sortedProposals); + }; + + if (instrumentProposalsData.length) { + run(); + } }, [instrumentProposalsData, fapInstrument.availabilityTime]); const ProposalTimeAllocationColumn = ( diff --git a/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql b/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql new file mode 100644 index 0000000000..37ed56e58b --- /dev/null +++ b/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql @@ -0,0 +1,6 @@ +query getProposalTimeRequested($proposalPk: Int!, $instrumentId: Int!) { + proposalTimeRequested( + proposalPk: $proposalPk, + instrumentId: $instrumentId + ) +}