Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/backend/src/datasources/mockups/FapDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ export const dummyFapAssignment = new FapAssignment(
false,
null,
false,
null
null,
'dummy comment',
'dummy grade',
1
);

export const anotherDummyFapAssignment = new FapAssignment(
Expand All @@ -129,7 +132,10 @@ export const anotherDummyFapAssignment = new FapAssignment(
false,
null,
false,
null
null,
'dummy comment',
'dummy grade',
1
);

export const dummyFapReview = new Review(
Expand Down
32 changes: 16 additions & 16 deletions apps/backend/src/datasources/postgres/FapDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,13 @@ export default class PostgresFapDataSource implements FapDataSource {
): Promise<FapAssignment[]> {
const fapAssignments: ReviewRecord[] = await database
.from('fap_reviews')
.where('fap_id', fapId)
.andWhere('proposal_pk', proposalPk)
.modify((query) => {
if (reviewerId !== null) {
query.where('user_id', reviewerId);
query.andWhere('user_id', reviewerId);
}
})
.where('fap_id', fapId)
.andWhere('proposal_pk', proposalPk);
});

return fapAssignments.map((fapAssignment) =>
createFapAssignmentObject(fapAssignment)
Expand Down Expand Up @@ -1345,20 +1345,20 @@ export default class PostgresFapDataSource implements FapDataSource {
fapId: number,
instrumentId?: number | null
): Promise<FapReviewsRecord[]> {
if (instrumentId) {
return await database
.select('*')
.from('review_data')
.where('fap_id', fapId)
.andWhere('call_id', callId)
.andWhere('instrument_id', instrumentId);
}

return await database
const query = database
.select('*')
.from('review_data')
.where('fap_id', fapId)
.andWhere('call_id', callId);
.where('fap_id', fapId);

if (callId != 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a legitimate case where callId is 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callId is set to 0 when no calls are selected in the frontend.

query.andWhere('call_id', callId);
}

if (instrumentId != null) {
query.andWhere('instrument_id', instrumentId);
}

return await query;
}

async submitFapMeetings(
Expand Down
5 changes: 4 additions & 1 deletion apps/backend/src/datasources/postgres/records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1191,7 +1191,10 @@ export const createFapAssignmentObject = (fapAssignment: ReviewRecord) => {
fapAssignment.reassigned,
fapAssignment.date_reassigned,
fapAssignment.email_sent,
fapAssignment.rank
fapAssignment.rank,
fapAssignment.comment,
fapAssignment.grade,
fapAssignment.status
);
};

Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/factory/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum XLSXType {
PROPOSAL = 'proposal',
FAP = 'fap',
CALL_FAP = 'call_fap',
FAP_REVIEWS = 'fap_reviews',
TECHNIQUE = 'technique',
MANAGEMENT_DECISION = 'management_decision',
}
Expand Down
31 changes: 31 additions & 0 deletions apps/backend/src/factory/xlsx/FapDataRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,37 @@ export type FapDataRowInput = {

export type FapDataRow = (input: FapDataRowInput) => RowObj | Promise<RowObj>;

export type ReviewRowInput = {
proposalId: number | null;
title: string | null;
instrumentName: string | null;
dateAssigned: Date | string | null;
rank: number | null;
grade: string | null;
comment: string | null;
status: string | null;
};

export const buildReviewRow = ({
proposalId,
title,
instrumentName,
dateAssigned,
rank,
grade,
comment,
status,
}: ReviewRowInput): Array<string | number> => [
proposalId ?? '-',
title ?? '-',
instrumentName ?? '-',
dateAssigned ? String(dateAssigned) : '-',
rank ?? '-',
grade ?? '-',
comment ?? '-',
status ?? '-',
];

export function getDataRow({
proposalPk,
piName,
Expand Down
108 changes: 107 additions & 1 deletion apps/backend/src/factory/xlsx/fap.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { groupBy } from 'lodash';
import { DateTime } from 'luxon';
import { container } from 'tsyringe';

import { collectCallFapXLSXData } from './callFaps';
import { FapDataRow, FapDataRowInput } from './FapDataRow';
import { buildReviewRow, FapDataRow, FapDataRowInput } from './FapDataRow';
import baseContext from '../../buildContext';
import { Tokens } from '../../config/Tokens';
import { FapDataSource } from '../../datasources/FapDataSource';
Expand Down Expand Up @@ -174,3 +175,108 @@ export const collectFapXLSXData = async (
data: transformedData,
};
};
export const collectFapReviewXLSXData = async (
fapId: number,
callId: number,
reviewerProposals: Record<number, number[]>,
user: UserWithRole
): Promise<{ data: FapXLSXData; filename: string }> => {
const reviewData = await fapDataSource.getFapReviewData(callId, fapId);

const data: FapXLSXData = [];
for (const [reviewerIdString, proposalPks] of Object.entries(
reviewerProposals
)) {
const reviewerId = Number(reviewerIdString);

const reviewer = await baseContext.queries.user.getBasic(user, reviewerId);

const reviewerName =
`${reviewer?.firstname ?? ''} ${reviewer?.lastname ?? ''}`.trim();
const rankedRows: Array<{
rank: number | null;
grade: number | null;
row: Array<string | number>;
}> = [];
for (const proposalPk of proposalPks) {
const review = reviewData.find((item) => item.proposal_pk === proposalPk);

if (!review) {
continue;
}

const assignments = await fapDataSource.getFapProposalAssignments(
fapId,
proposalPk,
reviewerId
);

const assignment = assignments.find(
(item) => item.fapMemberUserId === reviewerId
);

if (!assignment) {
continue;
}

const grade = Number(assignment.grade);
rankedRows.push({
rank: assignment.rank,
grade: Number.isFinite(grade) ? grade : null,
row: buildReviewRow({
proposalId: review.proposal_id,
title: review.title ?? '-',
instrumentName: review.instrument_name ?? '-',
dateAssigned: formatDate(assignment.dateAssigned),
rank: assignment.rank ?? null,
grade: assignment.grade,
comment: stripHtml(assignment.comment ?? '-'),
status: assignment.status == 1 ? 'Submitted' : 'Draft',
}),
});
}

const rows = rankedRows
.sort((a, b) => {
if (a.rank === null || b.rank === null) {
if (a.rank !== b.rank) {
return a.rank === null ? 1 : -1;
}
} else if (a.rank !== b.rank) {
return a.rank - b.rank;
}

if (a.grade === null || b.grade === null) {
if (a.grade !== b.grade) {
return a.grade === null ? 1 : -1;
}
} else if (a.grade !== b.grade) {
return b.grade - a.grade;
}

return 0;
})
.map(({ row }) => row);

data.push({
sheetName: reviewerName.substring(0, 31),
rows,
});
}

return {
filename: 'fap_reviews.xlsx',
data,
};
};

const formatDate = (value: string | Date | null | undefined): string => {
if (!value) return '-';

const dt =
typeof value === 'string'
? DateTime.fromISO(value)
: DateTime.fromJSDate(value);

return dt.isValid ? dt.toFormat('dd-MM-yyyy') : '-';
};
78 changes: 77 additions & 1 deletion apps/backend/src/middlewares/factory/xlsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
DefaultCallExtraFapDataColumns,
collectCallFapXLSXData,
} from '../../factory/xlsx/callFaps';
import { collectFapXLSXData } from '../../factory/xlsx/fap';
import {
collectFapXLSXData,
collectFapReviewXLSXData,
} from '../../factory/xlsx/fap';
import { collectManagementDecisionXLSXData } from '../../factory/xlsx/managementDecision';
import {
collectProposalXLSXData,
Expand Down Expand Up @@ -130,6 +133,79 @@ router.get(`/${XLSXType.FAP}/:fap_id/call/:call_id`, async (req, res, next) => {
}
});

router.get(
`/${XLSXType.FAP_REVIEWS}/:fap_id/call/:call_id`,
async (req, res, next) => {
try {
if (!req.user) {
throw new Error('Not authorized');
}

const userWithRole = {
...res.locals.agent,
};
Comment on lines +144 to +146

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to validate we are getting all the information we need from this object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Id is the information we need from userWithRole object which will be used further in the process. The middleware effectively validates the user’s id, it reads req.user.user.id, looks up that user with getAgent(id), and returns an unauthorized response if no user is found. It then puts the resulting user data into res.locals.agent. So I believe a second id check in the XLSX handler would usually be redundant.


const fapId = parseInt(req.params.fap_id);
const callId = parseInt(req.params.call_id);

if (isNaN(fapId) || isNaN(callId)) {
throw new Error(
`Invalid Fap or call ID: Fap ${req.params.fap_id}, Call ${req.params.call_id}`
);
}

const reviewerProposalsParam = req.query.reviewerProposals;

if (typeof reviewerProposalsParam !== 'string') {
throw new Error('Proposal reviewer is required');
}

const reviewerProposals: Record<number, number[]> = JSON.parse(
reviewerProposalsParam
);

const { data, filename } = await collectFapReviewXLSXData(
fapId,
callId,
reviewerProposals,
userWithRole
);

const meta: XLSXMetaBase = {
singleFilename: filename,
collectionFilename: filename,
columns: [
'Proposal ID',
'Proposal title',
'Instrument',
'Date assigned',
'Rank',
'Grade',
'Comment',
'Status',
],
};

const userRole = req.user.currentRole;

callFactoryService(
DownloadType.XLSX,
XLSXType.FAP_REVIEWS,
{
data,
meta,
userRole,
},
req,
res,
next
);
} catch (e) {
next(e);
}
}
);

router.get(`/${XLSXType.CALL_FAP}/:call_id`, async (req, res, next) => {
try {
if (!req.user) {
Expand Down
5 changes: 4 additions & 1 deletion apps/backend/src/models/Fap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export class FapAssignment {
public reassigned: boolean,
public dateReassigned: Date | null,
public emailSent: boolean,
public rank: number | null
public rank: number | null,
public comment: string,
public grade: string,
public status: number
) {}
}
Loading
Loading