From 6d9fff10917dabc6b82733afef9412f3f7def5df Mon Sep 17 00:00:00 2001 From: zachary Date: Mon, 15 Jun 2026 13:24:30 +0100 Subject: [PATCH 1/8] add backend support for hours requested --- .../src/datasources/ProposalDataSource.ts | 4 +++ .../datasources/mockups/ProposalDataSource.ts | 7 ++++++ .../postgres/ProposalDataSource.ts | 25 +++++++++++++++++++ .../src/resolvers/queries/ProposalQuery.ts | 21 ++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/apps/backend/src/datasources/ProposalDataSource.ts b/apps/backend/src/datasources/ProposalDataSource.ts index eb36715183..660c466649 100644 --- a/apps/backend/src/datasources/ProposalDataSource.ts +++ b/apps/backend/src/datasources/ProposalDataSource.ts @@ -9,6 +9,10 @@ import { PaginationSortDirection } from '../utils/pagination'; import { ProposalsFilter } from './../resolvers/queries/ProposalsQuery'; export interface ProposalDataSource { + getRequestedTime( + proposalId: 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..e925d5141a 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( + proposalId: 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..76e47fb4bb 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -312,6 +312,31 @@ export default class PostgresProposalDataSource implements ProposalDataSource { }); } + async getRequestedTime( + proposalId: number, + instrumentId: number + ): Promise { + 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('templates as t', 't.template_id', 'q2.template_id') + .innerJoin('answers as a', 'a.questionary_id', 'q2.questionary_id') + .where('p.proposal_pk', proposalId) + .where('a.question_id', 'instrument_picker_question') + .whereRaw("a.answer->'value'->>'instrumentId' = ?", [ + instrumentId.toString(), + ]) + .first(); + + return result && result.total_time_requested !== null + ? Number(result.total_time_requested) + : null; + } + 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..bae9cd73b9 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: true }) + async getProposalTimeRequested( + @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; + } } From 2ed5685a41b70e2b1039cdc137bdb5ef02054361 Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 16 Jun 2026 08:34:26 +0100 Subject: [PATCH 2/8] add graphql --- .../src/graphql/proposal/getProposalTimeRequested.graphql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql diff --git a/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql b/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql new file mode 100644 index 0000000000..d9fe535a8c --- /dev/null +++ b/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql @@ -0,0 +1,6 @@ +query GetProposalTimeRequested($proposalPk: Int!, $instrumentId: Int!) { + getProposalTimeRequested( + proposalPk: $proposalPk, + instrumentId: $instrumentId + ) +} From 5fdbda227468e7002407445de5a58fca0f7cbe1c Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 16 Jun 2026 09:34:39 +0100 Subject: [PATCH 3/8] add to ui --- .../FapInstrumentProposalsTable.tsx | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx index ae91db4096..ed7147b2c3 100644 --- a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx +++ b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx @@ -26,6 +26,7 @@ import { ProposalPkWithRankOrder, } from 'generated/sdk'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; +import { useDataApi } from 'hooks/common/useDataApi'; import { useFapProposalsByInstrument } from 'hooks/fap/useFapProposalsByInstrument'; import { tableIcons } from 'utils/materialIcons'; import { @@ -42,6 +43,7 @@ type FapProposalWithAverageScoreAndAvailabilityZone = FapProposal & { proposalAverageScore: number | string; proposalDeviation: number | string; isInAvailabilityZone: boolean; + timeRequested?: number | string; tableData?: { index: number; id: number }; }; @@ -118,6 +120,7 @@ const FapInstrumentProposalsTable = ({ const { api } = useDataApiWithFeedback(); const [openProposal, setOpenProposal] = useState(null); const { t } = useTranslation(); + const dataapi = useDataApi(); const getInstrumentTechnicalReview = ( technicalReviews: TechnicalReview[] | null @@ -127,6 +130,11 @@ const FapInstrumentProposalsTable = ({ ); const assignmentColumns = [ + { + title: 'Time Requested', + field: 'timeRequested', + emptyValue: '-', + }, { title: 'Actions', cellStyle: { padding: 0, minWidth: 100 }, @@ -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 dataapi().GetProposalTimeRequested({ + proposalPk: proposalData.proposal.primaryKey, + instrumentId: fapInstrument.id, + }); + + const timeRequested = result.getProposalTimeRequested || 0; + 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 = ( From 739e806a71cf52b19389fc96aad60fa68882df58 Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 16 Jun 2026 11:10:52 +0100 Subject: [PATCH 4/8] make non-nullable --- .../backend/src/datasources/ProposalDataSource.ts | 5 +---- .../src/datasources/mockups/ProposalDataSource.ts | 2 +- .../datasources/postgres/ProposalDataSource.ts | 8 ++++---- .../src/resolvers/queries/ProposalQuery.ts | 2 +- .../FapInstrumentProposalsTable.tsx | 15 +++++++-------- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/datasources/ProposalDataSource.ts b/apps/backend/src/datasources/ProposalDataSource.ts index 660c466649..fce2acd849 100644 --- a/apps/backend/src/datasources/ProposalDataSource.ts +++ b/apps/backend/src/datasources/ProposalDataSource.ts @@ -9,10 +9,7 @@ import { PaginationSortDirection } from '../utils/pagination'; import { ProposalsFilter } from './../resolvers/queries/ProposalsQuery'; export interface ProposalDataSource { - getRequestedTime( - proposalId: number, - instrumentId: number - ): Promise; + getRequestedTime(proposalId: 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 e925d5141a..51a89acf13 100644 --- a/apps/backend/src/datasources/mockups/ProposalDataSource.ts +++ b/apps/backend/src/datasources/mockups/ProposalDataSource.ts @@ -378,7 +378,7 @@ export class ProposalDataSourceMock implements ProposalDataSource { async getRequestedTime( proposalId: number, instrumentId: number - ): Promise { + ): Promise { return 32; } } diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index 76e47fb4bb..843c1a3b96 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -315,7 +315,8 @@ export default class PostgresProposalDataSource implements ProposalDataSource { async getRequestedTime( proposalId: number, instrumentId: number - ): Promise { + ): 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( @@ -323,7 +324,6 @@ export default class PostgresProposalDataSource implements ProposalDataSource { ), }) .innerJoin('questionaries as q2', 'q2.questionary_id', 'p.questionary_id') - .innerJoin('templates as t', 't.template_id', 'q2.template_id') .innerJoin('answers as a', 'a.questionary_id', 'q2.questionary_id') .where('p.proposal_pk', proposalId) .where('a.question_id', 'instrument_picker_question') @@ -332,9 +332,9 @@ export default class PostgresProposalDataSource implements ProposalDataSource { ]) .first(); - return result && result.total_time_requested !== null + return result?.total_time_requested ? Number(result.total_time_requested) - : null; + : 0; } async create( diff --git a/apps/backend/src/resolvers/queries/ProposalQuery.ts b/apps/backend/src/resolvers/queries/ProposalQuery.ts index bae9cd73b9..2d8796e904 100644 --- a/apps/backend/src/resolvers/queries/ProposalQuery.ts +++ b/apps/backend/src/resolvers/queries/ProposalQuery.ts @@ -24,7 +24,7 @@ export class ProposalQuery { return context.queries.proposal.get(context.user, proposalPk) !== null; } - @Query(() => Number, { nullable: true }) + @Query(() => Number, { nullable: false }) async getProposalTimeRequested( @Arg('proposalPk', () => Int) proposalPk: number, @Arg('instrumentId', () => Int) instrumentId: number, diff --git a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx index ed7147b2c3..f5e3682858 100644 --- a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx +++ b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx @@ -43,7 +43,7 @@ type FapProposalWithAverageScoreAndAvailabilityZone = FapProposal & { proposalAverageScore: number | string; proposalDeviation: number | string; isInAvailabilityZone: boolean; - timeRequested?: number | string; + timeRequested: number; tableData?: { index: number; id: number }; }; @@ -130,11 +130,6 @@ const FapInstrumentProposalsTable = ({ ); const assignmentColumns = [ - { - title: 'Time Requested', - field: 'timeRequested', - emptyValue: '-', - }, { title: 'Actions', cellStyle: { padding: 0, minWidth: 100 }, @@ -160,7 +155,7 @@ const FapInstrumentProposalsTable = ({ }, }, { - title: 'Principal Investigator', + title: 'Principal investigator', render: (rowData: FapProposal) => { return getFullUserName(rowData.proposal.proposer); }, @@ -193,6 +188,10 @@ const FapInstrumentProposalsTable = ({ return rankOrder || '-'; }, }, + { + title: 'Time requested', + field: 'timeRequested', + }, { title: 'Time allocation', field: 'timeAllocation', @@ -297,7 +296,7 @@ const FapInstrumentProposalsTable = ({ instrumentId: fapInstrument.id, }); - const timeRequested = result.getProposalTimeRequested || 0; + const timeRequested = result.getProposalTimeRequested; return { ...proposalData, From be1308c75384561c3aec41f8ab1214e123119130 Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 16 Jun 2026 11:32:30 +0100 Subject: [PATCH 5/8] add alloc to title --- .../FapInstrumentProposalsTable.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx index f5e3682858..ed8061bd6a 100644 --- a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx +++ b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx @@ -230,13 +230,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, From 1714f43a5df79a425506a334a6cc150cd550242a Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 16 Jun 2026 13:50:07 +0100 Subject: [PATCH 6/8] fix var names --- apps/backend/src/resolvers/queries/ProposalQuery.ts | 2 +- .../fap/MeetingComponents/FapInstrumentProposalsTable.tsx | 6 ++---- .../src/graphql/proposal/getProposalTimeRequested.graphql | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/resolvers/queries/ProposalQuery.ts b/apps/backend/src/resolvers/queries/ProposalQuery.ts index 2d8796e904..cf0dbc4cc0 100644 --- a/apps/backend/src/resolvers/queries/ProposalQuery.ts +++ b/apps/backend/src/resolvers/queries/ProposalQuery.ts @@ -25,7 +25,7 @@ export class ProposalQuery { } @Query(() => Number, { nullable: false }) - async getProposalTimeRequested( + async proposalTimeRequested( @Arg('proposalPk', () => Int) proposalPk: number, @Arg('instrumentId', () => Int) instrumentId: number, @Ctx() context: ResolverContext diff --git a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx index ed8061bd6a..27ee431986 100644 --- a/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx +++ b/apps/frontend/src/components/fap/MeetingComponents/FapInstrumentProposalsTable.tsx @@ -26,7 +26,6 @@ import { ProposalPkWithRankOrder, } from 'generated/sdk'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; -import { useDataApi } from 'hooks/common/useDataApi'; import { useFapProposalsByInstrument } from 'hooks/fap/useFapProposalsByInstrument'; import { tableIcons } from 'utils/materialIcons'; import { @@ -120,7 +119,6 @@ const FapInstrumentProposalsTable = ({ const { api } = useDataApiWithFeedback(); const [openProposal, setOpenProposal] = useState(null); const { t } = useTranslation(); - const dataapi = useDataApi(); const getInstrumentTechnicalReview = ( technicalReviews: TechnicalReview[] | null @@ -294,12 +292,12 @@ const FapInstrumentProposalsTable = ({ getGradesFromReviews(proposalData.proposal.reviews ?? []) ); - const result = await dataapi().GetProposalTimeRequested({ + const result = await api().getProposalTimeRequested({ proposalPk: proposalData.proposal.primaryKey, instrumentId: fapInstrument.id, }); - const timeRequested = result.getProposalTimeRequested; + const timeRequested = result.proposalTimeRequested; return { ...proposalData, diff --git a/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql b/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql index d9fe535a8c..37ed56e58b 100644 --- a/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql +++ b/apps/frontend/src/graphql/proposal/getProposalTimeRequested.graphql @@ -1,5 +1,5 @@ -query GetProposalTimeRequested($proposalPk: Int!, $instrumentId: Int!) { - getProposalTimeRequested( +query getProposalTimeRequested($proposalPk: Int!, $instrumentId: Int!) { + proposalTimeRequested( proposalPk: $proposalPk, instrumentId: $instrumentId ) From 8de7b1bdcf20b0c9778e18526f9adc819c8c3eb0 Mon Sep 17 00:00:00 2001 From: zachary Date: Wed, 17 Jun 2026 12:18:44 +0100 Subject: [PATCH 7/8] fix bug of question type --- apps/backend/src/datasources/postgres/ProposalDataSource.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index 843c1a3b96..2dda37c21b 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -325,8 +325,9 @@ export default class PostgresProposalDataSource implements ProposalDataSource { }) .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', proposalId) - .where('a.question_id', 'instrument_picker_question') + .where('q.data_type', 'INSTRUMENT_PICKER') .whereRaw("a.answer->'value'->>'instrumentId' = ?", [ instrumentId.toString(), ]) From 1cfc7e6c7b083265039be65cfdf04beed41b37ca Mon Sep 17 00:00:00 2001 From: zachary Date: Mon, 22 Jun 2026 14:21:27 +0100 Subject: [PATCH 8/8] fix proposalID -> proposalPK --- apps/backend/src/datasources/ProposalDataSource.ts | 2 +- apps/backend/src/datasources/mockups/ProposalDataSource.ts | 2 +- apps/backend/src/datasources/postgres/ProposalDataSource.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/datasources/ProposalDataSource.ts b/apps/backend/src/datasources/ProposalDataSource.ts index fce2acd849..d1a046eef0 100644 --- a/apps/backend/src/datasources/ProposalDataSource.ts +++ b/apps/backend/src/datasources/ProposalDataSource.ts @@ -9,7 +9,7 @@ import { PaginationSortDirection } from '../utils/pagination'; import { ProposalsFilter } from './../resolvers/queries/ProposalsQuery'; export interface ProposalDataSource { - getRequestedTime(proposalId: number, instrumentId: number): Promise; + 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 51a89acf13..7c86693090 100644 --- a/apps/backend/src/datasources/mockups/ProposalDataSource.ts +++ b/apps/backend/src/datasources/mockups/ProposalDataSource.ts @@ -376,7 +376,7 @@ export class ProposalDataSourceMock implements ProposalDataSource { } async getRequestedTime( - proposalId: number, + 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 2dda37c21b..91fbf40d84 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -313,7 +313,7 @@ export default class PostgresProposalDataSource implements ProposalDataSource { } async getRequestedTime( - proposalId: number, + proposalPk: number, instrumentId: number ): Promise { //Non-nullable, time is either requested for the instrument or it is zero. @@ -326,7 +326,7 @@ export default class PostgresProposalDataSource implements ProposalDataSource { .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', proposalId) + .where('p.proposal_pk', proposalPk) .where('q.data_type', 'INSTRUMENT_PICKER') .whereRaw("a.answer->'value'->>'instrumentId' = ?", [ instrumentId.toString(),