diff --git a/apps/backend/src/datasources/CallDataSource.ts b/apps/backend/src/datasources/CallDataSource.ts index e008bf853e..f0c5948ce4 100644 --- a/apps/backend/src/datasources/CallDataSource.ts +++ b/apps/backend/src/datasources/CallDataSource.ts @@ -39,4 +39,5 @@ export interface CallDataSource { getCallByAnswerIdProposal(answerId: number): Promise; getProposalWorkflowByCall(callId: number): Promise; getExperimentWorkflowByCall(callId: number): Promise; + getCallsOfFaps(fapIds: number[]): Promise; } diff --git a/apps/backend/src/datasources/InstrumentDataSource.ts b/apps/backend/src/datasources/InstrumentDataSource.ts index d134c05f6b..b1885de401 100644 --- a/apps/backend/src/datasources/InstrumentDataSource.ts +++ b/apps/backend/src/datasources/InstrumentDataSource.ts @@ -95,4 +95,5 @@ export interface InstrumentDataSource { instrumentId: number, proposalPk: number ): Promise; + getInstrumentsByFapIds(fapId: number[]): Promise; } diff --git a/apps/backend/src/datasources/mockups/CallDataSource.ts b/apps/backend/src/datasources/mockups/CallDataSource.ts index 982429fa07..09ca53980d 100644 --- a/apps/backend/src/datasources/mockups/CallDataSource.ts +++ b/apps/backend/src/datasources/mockups/CallDataSource.ts @@ -221,4 +221,8 @@ export class CallDataSourceMock implements CallDataSource { async getExperimentWorkflowByCall(callId: number): Promise { return dummyWorkflow; } + + getCallsOfFaps(fapIds: number[]): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/apps/backend/src/datasources/mockups/InstrumentDataSource.ts b/apps/backend/src/datasources/mockups/InstrumentDataSource.ts index 7760ad3399..13b9f40276 100644 --- a/apps/backend/src/datasources/mockups/InstrumentDataSource.ts +++ b/apps/backend/src/datasources/mockups/InstrumentDataSource.ts @@ -277,4 +277,8 @@ export class InstrumentDataSourceMock implements InstrumentDataSource { ): Promise { throw new Error('Method not implemented.'); } + + getInstrumentsByFapIds(fapId: number[]): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/apps/backend/src/datasources/postgres/CallDataSource.ts b/apps/backend/src/datasources/postgres/CallDataSource.ts index 4bdfe0cd04..ac732a8594 100644 --- a/apps/backend/src/datasources/postgres/CallDataSource.ts +++ b/apps/backend/src/datasources/postgres/CallDataSource.ts @@ -640,4 +640,15 @@ export default class PostgresCallDataSource implements CallDataSource { : null ); } + + async getCallsOfFaps(fapIds: number[]): Promise { + return database + .distinct('call.*') + .from('call') + .join('call_has_faps as chf', 'chf.call_id', 'call.call_id') + .whereIn('chf.fap_id', fapIds) + .then((calls: CallRecord[]) => { + return calls.map((call) => createCallObject(call)); + }); + } } diff --git a/apps/backend/src/datasources/postgres/InstrumentDataSource.ts b/apps/backend/src/datasources/postgres/InstrumentDataSource.ts index 9d9b55da68..eafdce770d 100644 --- a/apps/backend/src/datasources/postgres/InstrumentDataSource.ts +++ b/apps/backend/src/datasources/postgres/InstrumentDataSource.ts @@ -796,4 +796,20 @@ export default class PostgresInstrumentDataSource return result?.count === '1'; }); } + + async getInstrumentsByFapIds(fapId: number[]): Promise { + return database + .select('i.*') + .from('fap_proposals as fp') + .join('instruments as i', { 'fp.instrument_id': 'i.instrument_id' }) + .where('fp.fap_id', 'in', fapId) + .distinct() + .then((instruments: InstrumentRecord[]) => { + const result = instruments.map((instrument) => + this.createInstrumentObject(instrument) + ); + + return result; + }); + } } diff --git a/apps/backend/src/queries/CallQueries.ts b/apps/backend/src/queries/CallQueries.ts index f6d3b4c93f..6b784097ae 100644 --- a/apps/backend/src/queries/CallQueries.ts +++ b/apps/backend/src/queries/CallQueries.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'tsyringe'; import { UserAuthorization } from '../auth/UserAuthorization'; import { Tokens } from '../config/Tokens'; import { CallDataSource } from '../datasources/CallDataSource'; +import { FapDataSource } from '../datasources/FapDataSource'; import { Authorized } from '../decorators'; import { Roles } from '../models/Role'; import { UserWithRole } from '../models/User'; @@ -13,7 +14,9 @@ import { PaginationSortDirection } from '../utils/pagination'; export default class CallQueries { constructor( @inject(Tokens.CallDataSource) public dataSource: CallDataSource, - @inject(Tokens.UserAuthorization) private userAuth: UserAuthorization + @inject(Tokens.UserAuthorization) private userAuth: UserAuthorization, + @inject(Tokens.FapDataSource) + public fapDataSource: FapDataSource ) {} @Authorized() @@ -67,4 +70,18 @@ export default class CallQueries { async getCallOfAnswersProposal(user: UserWithRole | null, answerId: number) { return this.dataSource.getCallByAnswerIdProposal(answerId); } + + @Authorized([Roles.FAP_REVIEWER, Roles.FAP_CHAIR, Roles.FAP_SECRETARY]) + async getCallsOfReviewer(agent: UserWithRole | null) { + if (!agent || !agent.id || !agent.currentRole) { + return []; + } + + const faps = await this.fapDataSource.getUserFapsByRoleAndFapId( + agent.id, + agent.currentRole + ); + + return this.dataSource.getCallsOfFaps(faps.map((fap) => fap.id)); + } } diff --git a/apps/backend/src/queries/InstrumentQueries.ts b/apps/backend/src/queries/InstrumentQueries.ts index caa1143a8a..ab932ae427 100644 --- a/apps/backend/src/queries/InstrumentQueries.ts +++ b/apps/backend/src/queries/InstrumentQueries.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'tsyringe'; import { UserAuthorization } from '../auth/UserAuthorization'; import { Tokens } from '../config/Tokens'; +import { FapDataSource } from '../datasources/FapDataSource'; import { InstrumentDataSource } from '../datasources/InstrumentDataSource'; import { Authorized } from '../decorators'; import { Instrument, InstrumentWithManagementTime } from '../models/Instrument'; @@ -14,7 +15,9 @@ export default class InstrumentQueries { @inject(Tokens.InstrumentDataSource) public dataSource: InstrumentDataSource, @inject(Tokens.UserAuthorization) - private userAuth: UserAuthorization + private userAuth: UserAuthorization, + @inject(Tokens.FapDataSource) + public fapDataSource: FapDataSource ) {} @Authorized() @@ -32,13 +35,7 @@ export default class InstrumentQueries { return await this.dataSource.getInstrumentsByIds(instrumentIds); } - @Authorized([ - Roles.USER_OFFICER, - Roles.FAP_REVIEWER, - Roles.FAP_CHAIR, - Roles.FAP_SECRETARY, - Roles.INSTRUMENT_SCIENTIST, - ]) + @Authorized([Roles.USER_OFFICER, Roles.INSTRUMENT_SCIENTIST]) async getAll(agent: UserWithRole | null, callIds: number[]) { if (!callIds || callIds.length === 0) { return await this.dataSource.getInstruments(); @@ -98,6 +95,20 @@ export default class InstrumentQueries { } } + @Authorized([Roles.FAP_REVIEWER, Roles.FAP_CHAIR, Roles.FAP_SECRETARY]) + async getFapReviewerInstruments(agent: UserWithRole | null) { + if (!agent || !agent.id || !agent.currentRole) { + return []; + } + + const faps = await this.fapDataSource.getUserFapsByRoleAndFapId( + agent.id, + agent.currentRole + ); + + return this.dataSource.getInstrumentsByFapIds(faps.map((fap) => fap.id)); + } + @Authorized() async hasInstrumentScientistInstrument( agent: UserWithRole | null, diff --git a/apps/backend/src/resolvers/queries/CallsQuery.ts b/apps/backend/src/resolvers/queries/CallsQuery.ts index 6ece709d08..d0b83e90d8 100644 --- a/apps/backend/src/resolvers/queries/CallsQuery.ts +++ b/apps/backend/src/resolvers/queries/CallsQuery.ts @@ -105,4 +105,9 @@ export class CallsQuery { scientistId ); } + + @Query(() => [Call], { nullable: true }) + callsOfReviewer(@Ctx() context: ResolverContext) { + return context.queries.call.getCallsOfReviewer(context.user); + } } diff --git a/apps/backend/src/resolvers/queries/InstrumentQuery.ts b/apps/backend/src/resolvers/queries/InstrumentQuery.ts index 887c62b9c0..d69b8593dd 100644 --- a/apps/backend/src/resolvers/queries/InstrumentQuery.ts +++ b/apps/backend/src/resolvers/queries/InstrumentQuery.ts @@ -90,4 +90,11 @@ export class InstrumentQuery { proposalPk ); } + + @Query(() => [Instrument], { nullable: true }) + async getInstrumentsOfReviewer( + @Ctx() context: ResolverContext + ): Promise { + return context.queries.instrument.getFapReviewerInstruments(context.user); + } } diff --git a/apps/frontend/src/components/review/ProposalTableReviewer.tsx b/apps/frontend/src/components/review/ProposalTableReviewer.tsx index 4dc9e19fe7..83f298279a 100644 --- a/apps/frontend/src/components/review/ProposalTableReviewer.tsx +++ b/apps/frontend/src/components/review/ProposalTableReviewer.tsx @@ -23,9 +23,9 @@ import { UserRole, UserWithReviewsQuery, } from 'generated/sdk'; -import { useCallsData } from 'hooks/call/useCallsData'; +import { useReviewerCallsData } from 'hooks/call/useReviewerCallData'; import { useCheckAccess } from 'hooks/common/useCheckAccess'; -import { useInstrumentsMinimalData } from 'hooks/instrument/useInstrumentsMinimalData'; +import { useReviewerInstrumentData } from 'hooks/instrument/useReviewerInstrumentData'; import { useDownloadPDFProposal } from 'hooks/proposal/useDownloadPDFProposal'; import { useUserWithReviewsData } from 'hooks/user/useUserData'; import { capitalize, setSortDirectionOnSortField } from 'utils/helperFunctions'; @@ -84,8 +84,8 @@ const columns: ( const ProposalTableReviewer = ({ confirm }: { confirm: WithConfirmType }) => { const downloadPDFProposal = useDownloadPDFProposal(); - const { calls, loadingCalls } = useCallsData(); - const { instruments, loadingInstruments } = useInstrumentsMinimalData(); + const { calls, loadingCalls } = useReviewerCallsData(); + const { instruments, loadingInstruments } = useReviewerInstrumentData(); const { api } = useDataApiWithFeedback(); const { t } = useTranslation(); const isFapReviewer = useCheckAccess([UserRole.FAP_REVIEWER]); diff --git a/apps/frontend/src/graphql/call/getReviewerCalls.graphql b/apps/frontend/src/graphql/call/getReviewerCalls.graphql new file mode 100644 index 0000000000..ab124a9df7 --- /dev/null +++ b/apps/frontend/src/graphql/call/getReviewerCalls.graphql @@ -0,0 +1,6 @@ +query getReviewerCalls { + callsOfReviewer { + id + shortCode + } +} diff --git a/apps/frontend/src/graphql/instrument/getReviewerInstruments.graphql b/apps/frontend/src/graphql/instrument/getReviewerInstruments.graphql new file mode 100644 index 0000000000..fd37a8144a --- /dev/null +++ b/apps/frontend/src/graphql/instrument/getReviewerInstruments.graphql @@ -0,0 +1,5 @@ +query getReviewerInstruments { + getInstrumentsOfReviewer { + ...instrumentMinimal + } +} diff --git a/apps/frontend/src/hooks/call/useReviewerCallData.ts b/apps/frontend/src/hooks/call/useReviewerCallData.ts new file mode 100644 index 0000000000..4da518c015 --- /dev/null +++ b/apps/frontend/src/hooks/call/useReviewerCallData.ts @@ -0,0 +1,47 @@ +import { useEffect, useState, SetStateAction } from 'react'; + +import { Call } from 'generated/sdk'; +import { useDataApi } from 'hooks/common/useDataApi'; + +export enum CallsDataQuantity { + EXTENDED, + MINIMAL, +} + +export function useReviewerCallsData() { + const [calls, setCalls] = useState([]); + const [loadingCalls, setLoadingCalls] = useState(true); + + const api = useDataApi(); + + const setCallsWithLoading = (data: SetStateAction) => { + setLoadingCalls(true); + setCalls(data); + setLoadingCalls(false); + }; + + useEffect(() => { + let unmounted = false; + + setLoadingCalls(true); + + api() + .getReviewerCalls() + .then((data) => { + if (unmounted) { + return; + } + + if (data.callsOfReviewer) { + setCalls(data.callsOfReviewer as Call[]); + } + setLoadingCalls(false); + }); + + return () => { + unmounted = true; + }; + }, [api]); + + return { loadingCalls, calls, setCallsWithLoading }; +} diff --git a/apps/frontend/src/hooks/instrument/useInstrumentsMinimalData.ts b/apps/frontend/src/hooks/instrument/useInstrumentsMinimalData.ts index c6b0f2ddcc..851b2a23ea 100644 --- a/apps/frontend/src/hooks/instrument/useInstrumentsMinimalData.ts +++ b/apps/frontend/src/hooks/instrument/useInstrumentsMinimalData.ts @@ -29,13 +29,7 @@ export function useInstrumentsMinimalData(callIds?: number[]): { setLoadingInstruments(true); if ( currentRole && - [ - UserRole.USER_OFFICER, - UserRole.FAP_REVIEWER, - UserRole.FAP_CHAIR, - UserRole.FAP_SECRETARY, - UserRole.USER, - ].includes(currentRole) + [UserRole.USER_OFFICER, UserRole.USER].includes(currentRole) ) { api() .getInstrumentsMinimal({ callIds }) diff --git a/apps/frontend/src/hooks/instrument/useReviewerInstrumentData.ts b/apps/frontend/src/hooks/instrument/useReviewerInstrumentData.ts new file mode 100644 index 0000000000..51b97fdffd --- /dev/null +++ b/apps/frontend/src/hooks/instrument/useReviewerInstrumentData.ts @@ -0,0 +1,47 @@ +import { useEffect, useState, SetStateAction, Dispatch } from 'react'; + +import { InstrumentMinimalFragment } from 'generated/sdk'; +import { useDataApi } from 'hooks/common/useDataApi'; + +export function useReviewerInstrumentData(): { + loadingInstruments: boolean; + instruments: InstrumentMinimalFragment[]; + setInstruments: Dispatch>; +} { + const api = useDataApi(); + + const [instruments, setInstruments] = useState( + [] + ); + const [loadingInstruments, setLoadingInstruments] = useState(true); + + useEffect(() => { + let unmounted = false; + + setLoadingInstruments(true); + + api() + .getReviewerInstruments() + .then((data) => { + if (unmounted) { + return; + } + + if (data.getInstrumentsOfReviewer) { + setInstruments(data.getInstrumentsOfReviewer); + } + setLoadingInstruments(false); + }); + + return () => { + // used to avoid unmounted component state update error + unmounted = true; + }; + }, [api]); + + return { + loadingInstruments, + instruments, + setInstruments, + }; +}