From 16402cd6eee41dec5e97225aea30e0d765edf544 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 19 Nov 2024 16:16:40 -0600 Subject: [PATCH 1/7] Switch to server-side markdown. This has not been fully tested. --- server/build.gradle | 2 + .../services/reports/MarkdownGeneration.java | 397 ++++++++++++++++++ .../reports/ReportDataController.java | 52 +-- web-ui/src/api/generic.js | 14 +- web-ui/src/pages/MeritReportPage.jsx | 249 +---------- 5 files changed, 439 insertions(+), 275 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java diff --git a/server/build.gradle b/server/build.gradle index 8c70f18a6f..693b5cf376 100755 --- a/server/build.gradle +++ b/server/build.gradle @@ -12,6 +12,7 @@ group "com.objectcomputing.checkins" repositories { mavenCentral() + maven { url 'https://jitpack.io' } } micronaut { @@ -69,6 +70,7 @@ dependencies { runtimeOnly("org.postgresql:postgresql") compileOnly ("org.projectlombok:lombok") + implementation("com.github.Steppschuh:Java-Markdown-Generator:master-SNAPSHOT") annotationProcessor ("org.projectlombok:lombok") annotationProcessor ("io.micronaut:micronaut-inject-java") diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java new file mode 100644 index 0000000000..d26afd3fed --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java @@ -0,0 +1,397 @@ +package com.objectcomputing.checkins.services.reports; + +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.services.permissions.RequiredPermission; +import com.objectcomputing.checkins.services.kudos.KudosRepository; +import com.objectcomputing.checkins.services.kudos.kudos_recipient.KudosRecipientRepository; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; +import com.objectcomputing.checkins.services.reviews.ReviewPeriodServices; +import com.objectcomputing.checkins.services.feedback_template.FeedbackTemplateServices; +import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestServices; +import com.objectcomputing.checkins.services.feedback_answer.FeedbackAnswerServices; +import com.objectcomputing.checkins.services.feedback_template.template_question.TemplateQuestionServices; +import com.objectcomputing.checkins.services.employee_hours.EmployeeHoursServices; +import com.objectcomputing.checkins.services.file.FileServices; + +import net.steppschuh.markdowngenerator.*; +import net.steppschuh.markdowngenerator.text.heading.Heading; +import net.steppschuh.markdowngenerator.text.emphasis.ItalicText; +import net.steppschuh.markdowngenerator.list.UnorderedList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.TimeUnit; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Comparator; +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.function.Predicate; +import java.io.IOException; + +class MarkdownGeneration { + class AnswerComparator implements java.util.Comparator { + @Override + public int compare(Feedback.Answer a, Feedback.Answer b) { + return a.getNumber() - b.getNumber(); + } + } + + class PositionComparator implements java.util.Comparator { + @Override + public int compare(PositionHistory.Position a, + PositionHistory.Position b) { + LocalDate left = a.date(); + LocalDate right = b.date(); + if (left.isBefore(right)) { + return -1; + } else if (left.isEqual(right)) { + return 0; + } else { + return 1; + } + } + } + + class CompensationComparator implements java.util.Comparator { + @Override + public int compare(CompensationHistory.Compensation a, + CompensationHistory.Compensation b) { + LocalDate left = a.startDate(); + LocalDate right = b.startDate(); + if (left.isBefore(right)) { + return -1; + } else if (left.isEqual(right)) { + return 0; + } else { + return 1; + } + } + } + + private static final Logger LOG = LoggerFactory.getLogger(MarkdownGeneration.class); + private static final String noneAvailable = "None available during the period covered by this review."; + private static final String directory = "merit-reports"; + + private final ReportDataServices reportDataServices; + private final KudosRepository kudosRepository; + private final KudosRecipientRepository kudosRecipientRepository; + private final MemberProfileServices memberProfileServices; + private final ReviewPeriodServices reviewPeriodServices; + private final FeedbackTemplateServices feedbackTemplateServices; + private final FeedbackRequestServices feedbackRequestServices; + private final FeedbackAnswerServices feedbackAnswerServices; + private final TemplateQuestionServices templateQuestionServices; + private final EmployeeHoursServices employeeHoursServices; + private final FileServices fileServices; + + public MarkdownGeneration(ReportDataServices reportDataServices, + KudosRepository kudosRepository, + KudosRecipientRepository kudosRecipientRepository, + MemberProfileServices memberProfileServices, + ReviewPeriodServices reviewPeriodServices, + FeedbackTemplateServices feedbackTemplateServices, + FeedbackRequestServices feedbackRequestServices, + FeedbackAnswerServices feedbackAnswerServices, + TemplateQuestionServices templateQuestionServices, + EmployeeHoursServices employeeHoursServices, + FileServices fileServices) { + this.reportDataServices = reportDataServices; + this.kudosRepository = kudosRepository; + this.kudosRecipientRepository = kudosRecipientRepository; + this.memberProfileServices = memberProfileServices; + this.reviewPeriodServices = reviewPeriodServices; + this.feedbackTemplateServices = feedbackTemplateServices; + this.feedbackRequestServices = feedbackRequestServices; + this.feedbackAnswerServices = feedbackAnswerServices; + this.templateQuestionServices = templateQuestionServices; + this.employeeHoursServices = employeeHoursServices; + this.fileServices = fileServices; + } + + void upload(List memberIds, UUID reviewPeriodId) { + for (UUID memberId : memberIds) { + ReportDataCollation data = new ReportDataCollation( + memberId, reviewPeriodId, + kudosRepository, + kudosRecipientRepository, + memberProfileServices, + reviewPeriodServices, + reportDataServices, + feedbackTemplateServices, + feedbackRequestServices, + feedbackAnswerServices, + templateQuestionServices, + employeeHoursServices); + generateAndStore(data); + } + } + + void generateAndStore(ReportDataCollation data) { + final String markdown = generate(data); + store(data, markdown); + } + + String generate(ReportDataCollation data) { + StringBuilder sb = new StringBuilder(); + title(data, sb); + currentInfo(data, sb); + kudos(data, sb); + reviewsImpl("Self-Review", data.getSelfReviews(), false, sb); + reviewsImpl("Reviews", data.getReviews(), true, sb); + feedback(data, sb); + titleHistory(data, sb); + employeeHours(data, sb); + compensation(data, sb); + compensationHistory(data, sb); + sb.append(new Heading("Reviewer Notes", 4)).append("\n"); + return sb.toString(); + } + + void store(ReportDataCollation data, String markdown) { + // Send this text over to be uploaded to the google drive. + fileServices.uploadDocument(directory, + data.getMemberProfile().getWorkEmail(), + markdown); + } + + private String formatDate(LocalDate date) { + return date.format(DateTimeFormatter.ofPattern("MM/dd/yyyy")); + } + + private void title(ReportDataCollation data, StringBuilder sb) { + MemberProfile profile = data.getMemberProfile(); + sb.append(new Heading(MemberProfileUtils.getFullName(profile), 1)) + .append("\n") + .append(profile.getTitle()).append("\n\n") + .append("Review Period: ") + .append(formatDate(data.getStartDate())) + .append(" - ") + .append(formatDate(data.getEndDate())).append("\n"); + } + + private void currentInfo(ReportDataCollation data, StringBuilder sb) { + MemberProfile profile = data.getMemberProfile(); + CurrentInformation.Information current = data.getCurrentInformation(); + String bio = current.biography(); + ZonedDateTime zdt = ZonedDateTime.of( + profile.getStartDate().atTime(0, 0), + ZoneId.systemDefault()); + long ms = System.currentTimeMillis() - zdt.toInstant().toEpochMilli(); + double years = TimeUnit.DAYS.convert(ms, TimeUnit.MILLISECONDS) / 365.25; + sb.append(new Heading("Current Information", 1)).append("\n") + .append(String.format("%.1f", years)).append(" years\n\n") + .append(new Heading("Biographical Notes", 2)).append("\n") + .append(bio.isEmpty() ? noneAvailable : bio).append("\n\n"); + } + + private void kudos(ReportDataCollation data, StringBuilder sb) { + List received = data.getKudos(); + sb.append(new Heading("Kudos", 1)).append("\n\n"); + if (received.isEmpty()) { + sb.append(noneAvailable).append("\n\n"); + } else { + for (ReportKudos kudo : received) { + sb.append(kudo.message()).append("\n\n") + .append("     ") + .append(new ItalicText("Submitted on " + + formatDate(kudo.dateCreated()) + + ", by " + kudo.sender())) + .append("\n\n\n"); + } + } + } + + private Map getUniqueMembers(List answers) { + Map members = new HashMap<>(); + List sorted = new ArrayList<>(answers); + Collections.sort(sorted, new AnswerComparator()); + for (Feedback.Answer answer : sorted) { + if (!members.containsKey(answer.getMemberName())) { + members.put(answer.getMemberName(), answer.getSubmitted()); + } + } + return members; + } + + private Map>> getUniqueQuestions(List answers) { + Map>> questions = new HashMap<>(); + for (Feedback.Answer answer : answers) { + String key = answer.getQuestion(); + if (!questions.containsKey(key)) { + questions.put(key, new ArrayList>()); + } + List list = new ArrayList(); + list.add(answer.getMemberName()); + list.add(answer.getAnswer()); + questions.get(key).add(list); + } + return questions; + } + + private void reviewsImpl(String title, List feedbackList, boolean listMembers, StringBuilder sb) { + sb.append(new Heading(title, 1)).append("\n"); + if (feedbackList.isEmpty()) { + sb.append(noneAvailable).append("\n\n"); + } else { + for (Feedback feedback : feedbackList) { + Map members = + getUniqueMembers(feedback.getAnswers()); + for(Map.Entry entry : members.entrySet()) { + if (listMembers) { + sb.append(entry.getKey()).append(": "); + } + sb.append("Submitted - ") + .append(formatDate(entry.getValue())).append("\n\n"); + } + sb.append("\n"); + + Map>> questions = + getUniqueQuestions(feedback.getAnswers()); + for (Map.Entry>> question : questions.entrySet()) { + sb.append(new Heading(question.getKey(), 4)).append("\n"); + for (List answer : question.getValue()) { + if (listMembers) { + sb.append(answer.get(0)).append(": "); + } + sb.append(answer.get(1)).append("\n\n"); + } + sb.append("\n"); + } + } + sb.append("\n"); + } + } + + private void feedback(ReportDataCollation data, StringBuilder sb) { + sb.append(new Heading("Feedback", 1)).append("\n"); + List feedbackList = data.getFeedback(); + if (feedbackList.isEmpty()) { + sb.append(noneAvailable).append("\n\n"); + } else { + for (Feedback feedback : feedbackList) { + sb.append(new Heading("Template: " + feedback.getName(), 2)) + .append("\n"); + + Map members = + getUniqueMembers(feedback.getAnswers()); + for (Map.Entry entry : members.entrySet()) { + sb.append(entry.getKey()).append(": "); + sb.append(formatDate(entry.getValue())).append("\n\n"); + } + sb.append("\n"); + + Map>> questions = + getUniqueQuestions(feedback.getAnswers()); + for (Map.Entry>> question : questions.entrySet()) { + sb.append(new Heading(question.getKey(), 4)).append("\n"); + for (List answer : question.getValue()) { + sb.append(answer.get(0)).append(": "); + sb.append(answer.get(1)).append("\n\n"); + } + sb.append("\n"); + } + } + sb.append("\n"); + } + } + + private void titleHistory(ReportDataCollation data, StringBuilder sb) { + List posHistory = + new ArrayList<>(data.getPositionHistory()); + Collections.sort(posHistory, new PositionComparator()); + sb.append(new Heading("Title History", 2)).append("\n"); + List positions = new ArrayList<>(); + for (PositionHistory.Position position : posHistory) { + positions.add(String.valueOf(position.date().getYear()) + " - " + + position.title()); + } + sb.append(new UnorderedList<>(positions)).append("\n\n"); + } + + private void compensation(ReportDataCollation data, StringBuilder sb) { + CurrentInformation.Information current = data.getCurrentInformation(); + sb.append(new Heading("Compensation and Commitments", 2)).append("\n") + .append("$").append(String.format("%.2f", current.salary())) + .append(" Base Salary\n\n") + .append("OCI Range for role: ").append(current.range()) + .append("\n\n"); + String commitments = current.commitments(); + if (commitments == null || commitments.isEmpty()) { + sb.append("No current bonus commitments\n"); + } else { + sb.append("Commitments: ").append(current.commitments()) + .append("\n"); + } + sb.append("\n"); + } + + private List prepareCompHistory(ReportDataCollation data, Predicate fn) { + List comp = + data.getCompensationHistory().stream() + .filter(fn).collect(Collectors.toList()); + Collections.sort(comp, new CompensationComparator()); + return comp.subList(0, Math.min(3, comp.size())); + } + + private void compensationHistory(ReportDataCollation data, StringBuilder sb) { + List compBase = + prepareCompHistory(data, comp -> comp.amount() != null && + !comp.amount().isEmpty()); + List compTotal = + prepareCompHistory(data, comp -> comp.totalComp() != null && + !comp.totalComp().isEmpty()); + sb.append(new Heading("Compensation History", 2)).append("\n") + .append(new Heading("Base Compensation (annual or hourly)", 3)) + .append("\n"); + List list = new ArrayList<>(); + for (CompensationHistory.Compensation comp : compBase) { + String value = comp.amount(); + try { + double val = Double.parseDouble(value); + value = String.format("%.2f", val); + } catch (Exception e) { + } + list.add(formatDate(comp.startDate()) + " - $" + value); + } + sb.append(new UnorderedList<>(list)).append("\n\n") + .append(new Heading("Total Compensation", 3)) + .append("\n"); + list.clear(); + for (CompensationHistory.Compensation comp : compTotal) { + LocalDate startDate = comp.startDate(); + String date = startDate.getMonthValue() == 0 && + startDate.getDayOfMonth() == 1 ? + String.valueOf(startDate.getYear()) : + formatDate(startDate); + list.add(date + " - " + comp.totalComp()); + } + sb.append(new UnorderedList<>(list)).append("\n\n"); + } + + private void employeeHours(ReportDataCollation data, StringBuilder sb) { + sb.append(new Heading("Employee Hours", 2)).append("\n"); + List list = new ArrayList<>(); + ReportHours hours = data.getReportHours(); + list.add("Contribution Hours: " + + String.format("%f", hours.contributionHours())); + list.add("PTO Hours: " + String.format("%f", hours.ptoHours())); + list.add("Overtime Hours: " + + String.format("%f", hours.overtimeHours())); + list.add("Billable Utilization: " + + String.format("%f", hours.billableUtilization())); + sb.append(new UnorderedList<>(list)).append("\n\n"); + } +} + diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java index 15647cede2..406701b092 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java @@ -12,6 +12,8 @@ import com.objectcomputing.checkins.services.feedback_answer.FeedbackAnswerServices; import com.objectcomputing.checkins.services.feedback_template.template_question.TemplateQuestionServices; import com.objectcomputing.checkins.services.employee_hours.EmployeeHoursServices; +import com.objectcomputing.checkins.services.file.FileServices; + import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; @@ -51,6 +53,7 @@ public class ReportDataController { private final FeedbackAnswerServices feedbackAnswerServices; private final TemplateQuestionServices templateQuestionServices; private final EmployeeHoursServices employeeHoursServices; + private final FileServices fileServices; public ReportDataController(ReportDataServices reportDataServices, KudosRepository kudosRepository, @@ -61,7 +64,8 @@ public ReportDataController(ReportDataServices reportDataServices, FeedbackRequestServices feedbackRequestServices, FeedbackAnswerServices feedbackAnswerServices, TemplateQuestionServices templateQuestionServices, - EmployeeHoursServices employeeHoursServices) { + EmployeeHoursServices employeeHoursServices, + FileServices fileServices) { this.reportDataServices = reportDataServices; this.kudosRepository = kudosRepository; this.kudosRecipientRepository = kudosRecipientRepository; @@ -72,6 +76,7 @@ public ReportDataController(ReportDataServices reportDataServices, this.feedbackAnswerServices = feedbackAnswerServices; this.templateQuestionServices = templateQuestionServices; this.employeeHoursServices = employeeHoursServices; + this.fileServices = fileServices; } @Post(uri="/upload", consumes = MediaType.MULTIPART_FORM_DATA) @@ -115,35 +120,22 @@ private String uploadHelper(ReportDataServices.DataType dataType, } } - @Get + @Get(uri="/generate") @RequiredPermission(Permission.CAN_CREATE_MERIT_REPORT) - public List get(@NotNull List memberIds, - @NotNull UUID reviewPeriodId) { - List list = new ArrayList(); - for (UUID memberId : memberIds) { - ReportDataCollation data = new ReportDataCollation( - memberId, reviewPeriodId, - kudosRepository, - kudosRecipientRepository, - memberProfileServices, - reviewPeriodServices, - reportDataServices, - feedbackTemplateServices, - feedbackRequestServices, - feedbackAnswerServices, - templateQuestionServices, - employeeHoursServices); - list.add(new ReportDataDTO(memberId, reviewPeriodId, - data.getStartDate(), data.getEndDate(), - data.getMemberProfile(), data.getKudos(), - data.getCompensationHistory(), - data.getCurrentInformation(), - data.getPositionHistory(), - data.getSelfReviews(), - data.getReviews(), - data.getFeedback(), - data.getReportHours())); - } - return list; + public void generate(@NotNull List memberIds, + @NotNull UUID reviewPeriodId) { + MarkdownGeneration markdown = + new MarkdownGeneration(reportDataServices, + kudosRepository, + kudosRecipientRepository, + memberProfileServices, + reviewPeriodServices, + feedbackTemplateServices, + feedbackRequestServices, + feedbackAnswerServices, + templateQuestionServices, + employeeHoursServices, + fileServices); + markdown.upload(memberIds, reviewPeriodId); } } diff --git a/web-ui/src/api/generic.js b/web-ui/src/api/generic.js index 85bc63bc2d..712f072e04 100644 --- a/web-ui/src/api/generic.js +++ b/web-ui/src/api/generic.js @@ -20,6 +20,18 @@ export const downloadData = (url, cookie, params) => { 'X-CSRF-Header': cookie, Accept: 'application/json', }, - url: url //fullURL + url: url + }); +}; + +export const initiate = (url, cookie, params) => { + return resolve({ + method: 'GET', + params: params, + headers: { + 'X-CSRF-Header': cookie, + Accept: 'application/json', + }, + url: url }); }; diff --git a/web-ui/src/pages/MeritReportPage.jsx b/web-ui/src/pages/MeritReportPage.jsx index a590607277..008e78458c 100644 --- a/web-ui/src/pages/MeritReportPage.jsx +++ b/web-ui/src/pages/MeritReportPage.jsx @@ -2,7 +2,7 @@ import React, { useContext, useRef, useState, useEffect } from 'react'; import { Autocomplete, Button, TextField } from '@mui/material'; -import { uploadData, downloadData } from '../api/generic'; +import { uploadData, initiate } from '../api/generic'; import { getReviewPeriods } from '../api/reviewperiods'; import { UPDATE_TOAST } from '../context/actions'; import { AppContext } from '../context/AppContext'; @@ -148,7 +148,7 @@ const MeritReportPage = () => { } }; - const download = async () => { + const createReportMarkdownDocuments = async () => { let data; let error; @@ -166,9 +166,9 @@ const MeritReportPage = () => { } if (!error) { - const res = await downloadData("/services/report/data", - csrf, {memberIds: selected, - reviewPeriodId: reviewPeriodId.id}); + const res = await initiate("/services/report/data/generate", + csrf, {memberIds: selected, + reviewPeriodId: reviewPeriodId.id}); error = res?.error?.message; data = res?.payload?.data; } @@ -236,245 +236,6 @@ const MeritReportPage = () => { } }; - const markdownTitle = (data) => { - const memberProfile = data.memberProfile; - const startDate = dateFromArray(data.startDate); - const endDate = dateFromArray(data.endDate); - let text = markdown.headers.h1(memberProfile.firstName + " " + - memberProfile.lastName); - text += memberProfile.title + "\n\n"; - text += "Review Period: " + - formatDate(startDate) + " - " + formatDate(endDate) + "\n\n"; - return text; - }; - - const markdownCurrentInformation = (data) => { - const memberProfile = data.memberProfile; - const currentInfo = data.currentInformation; - const startDate = dateFromArray(memberProfile.startDate); - const years = (Date.now() - startDate) / (1000 * 60 * 60 * 24 * 365.25); - let text = markdown.headers.h1("Current Information"); - text += years.toFixed(1) + " years\n\n"; - text += markdown.headers.h2("Biographical Notes"); - text += currentInfo.biography + "\n\n"; - return text; - }; - - const markdownKudos = (data) => { - const kudosList = data.kudos; - let text = markdown.headers.h1("Kudos"); - for (let kudos of kudosList) { - const date = dateFromArray(kudos.dateCreated); - text += kudos.message + "\n\n"; - text += "     " + - markdown.emphasis.i("Submitted on " + formatDate(date) + - ", by " + kudos.sender) + - "\n\n\n"; - } - return text; - }; - - const markdownReviewsImpl = (title, feedbackList, listMembers) => { - let text = markdown.headers.h1(title); - for(let feedback of feedbackList) { - const members = getUniqueMembers(feedback.answers); - for(let member of Object.keys(members)) { - if (listMembers) { - text += member + ": "; - } - text += "Submitted - " + formatDate(members[member]) + "\n\n"; - } - text += "\n"; - - const questions = getUniqueQuestions(feedback.answers); - for(let question of Object.keys(questions)) { - text += markdown.headers.h4(question) + "\n"; - for(let answer of questions[question]) { - if (listMembers) { - text += answer[0] + ": "; - } - text += answer[1] + "\n\n"; - } - text += "\n"; - } - } - text += "\n"; - return text; - } - - const markdownSelfReviews = (data) => { - return markdownReviewsImpl("Self-Review", data.selfReviews, false); - } - - const markdownReviews = (data) => { - return markdownReviewsImpl("Reviews", data.reviews, true); - }; - - const getUniqueMembers = (answers) => { - let members = {}; - for(let answer of answers) { - const key = answer.memberName; - if (!(key in members)) { - // Put in member name and date - members[key] = dateFromArray(answer.submitted); - } - } - return members; - }; - - const getAnswerText = (answer) => { - return answer.answer; - }; - - const getUniqueQuestions = (answers) => { - let questions = {}; - answers = answers.sort((a, b) => { - return a.number - b.number; - }); - - for(let answer of answers) { - const key = answer.question; - if (!(key in questions)) { - // Put in member name and answer - questions[key] = []; - } - const text = getAnswerText(answer); - questions[key].push([answer.memberName, text]); - } - return questions; - }; - - const markdownFeedback = (data) => { - let text = markdown.headers.h1("Feedback"); - const feedbackList = data.feedback; - for(let feedback of feedbackList) { - text += markdown.headers.h2("Template: " + feedback.name); - const members = getUniqueMembers(feedback.answers); - for(let member of Object.keys(members)) { - text += member + ": " + formatDate(members[member]) + "\n\n"; - } - text += "\n"; - - const questions = getUniqueQuestions(feedback.answers); - for(let question of Object.keys(questions)) { - text += markdown.headers.h4(question) + "\n"; - for(let answer of questions[question]) { - text += answer[0] + ": " + answer[1] + "\n\n"; - } - text += "\n"; - } - } - text += "\n"; - return text; - }; - - const markdownTitleHistory = (data) => { - // Get the position history sorted latest to earliest - const posHistory = data.positionHistory.sort((a, b) => { - for(let i = 0; i < a.length; i++) { - if (a.date[i] != b.date[i]) { - return b.date[i] - a.date[i]; - } - } - return 0; - }); - - let text = markdown.headers.h2("Title History"); - text += markdown.lists.ul(posHistory, - (position) => position.date[0] + " - " + - position.title); - return text; - }; - - const markdownCompensation = (data) => { - const currentInfo = data.currentInformation; - let text = markdown.headers.h2("Compensation and Commitments"); - text += "$" + currentInfo.salary.toFixed(2) + " Base Salary\n\n"; - text += "OCI Range for role: " + currentInfo.range + "\n\n"; - text += "National Range for role: " + currentInfo.nationalRange + "\n\n"; - if (currentInfo.commitments) { - text += "Commitments: " + currentInfo.commitments + "\n"; - } else { - text += "No current bonus commitments\n"; - } - text += "\n"; - return text; - }; - - const prepareCompensationHistory = (data, fn) => { - return data.compensationHistory.filter(fn).sort((a, b) => { - for(let i = 0; i < a.startDate.length; i++) { - if (a.startDate[i] != b.startDate[i]) { - return b.startDate[i] - a.startDate[i]; - } - } - return 0; - }).slice(0, 3); - }; - - const markdownCompensationHistory = (data) => { - // Sort them latest to oldest and truncate to the first 3. - const compBase = prepareCompensationHistory(data, (comp) => !!comp.amount); - const compTotal = prepareCompensationHistory(data, (comp) => !!comp.totalComp); - - let text = markdown.headers.h2("Compensation History"); - text += markdown.headers.h3("Base Compensation (annual or hourly)"); - text += markdown.lists.ul(compBase, - (comp) => formatDate(dateFromArray(comp.startDate)) + " - " + - "$" + parseFloat(comp.amount).toFixed(2)); - text += markdown.headers.h3("Total Compensation") - text += markdown.lists.ul(compTotal, - (comp) => { - var date = dateFromArray(comp.startDate); - date = date.getMonth() === 0 && date.getDate() === 1 ? date.getFullYear() : formatDate(date); - return date + " - " + comp.totalComp; - }); - return text; - }; - - const markdownEmployeeHours = (data) => { - let text = markdown.headers.h2("Employee Hours"); - let hours = { - 'Contribution Hours': data.hours.contributionHours, - 'PTO Hours': data.hours.ptoHours, - 'Overtime Hours': data.hours.overtimeHours, - 'Billable Utilization': data.hours.billableUtilization, - }; - text += markdown.lists.ul(Object.keys(hours), - (key) => key + ": " + hours[key]); - return text; - }; - - const markdownReviewerNotes = (data) => { - let text = markdown.headers.h4("Reviewer Notes"); - return text; - }; - - const createReportMarkdownDocuments = async () => { - const dataSet = await download(); - if (dataSet) { - for (let data of dataSet) { - // Generate markdown - let text = markdownTitle(data); - text += markdownCurrentInformation(data); - text += markdownKudos(data); - text += markdownSelfReviews(data); - text += markdownReviews(data); - text += markdownFeedback(data); - text += markdownTitleHistory(data); - text += markdownEmployeeHours(data); - text += markdownCompensation(data); - text += markdownCompensationHistory(data); - text += markdownReviewerNotes(data); - - // Store the markdown on the google drive. - const directory = "merit-reports"; - const fileName = data.memberProfile.workEmail; - uploadDocument(directory, fileName, text); - } - } - }; - const onReviewPeriodChange = (event, newValue) => { setReviewPeriodId(newValue); }; From ad2b6d54be3c284e7b4f51dea2a63fe249d99e75 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 16 Dec 2024 12:10:19 -0600 Subject: [PATCH 2/7] Use version 1.3.1.1 of the markdowngenerator from mavenCentral. --- server/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 693b5cf376..857628bfa3 100755 --- a/server/build.gradle +++ b/server/build.gradle @@ -12,7 +12,6 @@ group "com.objectcomputing.checkins" repositories { mavenCentral() - maven { url 'https://jitpack.io' } } micronaut { @@ -70,7 +69,6 @@ dependencies { runtimeOnly("org.postgresql:postgresql") compileOnly ("org.projectlombok:lombok") - implementation("com.github.Steppschuh:Java-Markdown-Generator:master-SNAPSHOT") annotationProcessor ("org.projectlombok:lombok") annotationProcessor ("io.micronaut:micronaut-inject-java") @@ -81,6 +79,7 @@ dependencies { yarnBuildElements(project(":web-ui")) + implementation("net.steppschuh.markdowngenerator:markdowngenerator:1.3.1.1") implementation("io.micronaut:micronaut-jackson-databind") implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut:micronaut-management") From aa60fef35b5b9283ff98056ad54442e8c2866e05 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 16 Dec 2024 12:11:06 -0600 Subject: [PATCH 3/7] Provide an indicator for when document generation is complete. --- web-ui/src/pages/MeritReportPage.jsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/web-ui/src/pages/MeritReportPage.jsx b/web-ui/src/pages/MeritReportPage.jsx index 008e78458c..d98675fb72 100644 --- a/web-ui/src/pages/MeritReportPage.jsx +++ b/web-ui/src/pages/MeritReportPage.jsx @@ -149,14 +149,13 @@ const MeritReportPage = () => { }; const createReportMarkdownDocuments = async () => { - let data; let error; // Get the list of selected member ids. - let selected = selectedMembers.reduce((result, item) => { - result.push(item.id); - return result; - }, []); + const selected = selectedMembers.reduce((result, item) => { + result.push(item.id); + return result; + }, []); // Check for required parameters before calling the server. if (selected.length == 0) { @@ -170,7 +169,6 @@ const MeritReportPage = () => { csrf, {memberIds: selected, reviewPeriodId: reviewPeriodId.id}); error = res?.error?.message; - data = res?.payload?.data; } // Display the error, if there was one. @@ -182,9 +180,16 @@ const MeritReportPage = () => { toast: error } }); + } else { + dispatch({ + type: UPDATE_TOAST, + payload: { + severity: 'success', + toast: selected.length == 1 ? 'The report has been generated' + : 'The reports have been generated' + } + }); } - - return data; }; const uploadDocument = async (directory, name, text) => { From f145ced89972d8d12d8172fe1253f09976a22187 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Mon, 16 Dec 2024 12:25:44 -0600 Subject: [PATCH 4/7] Added the review period name to the review period pull down. --- web-ui/src/pages/MeritReportPage.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web-ui/src/pages/MeritReportPage.jsx b/web-ui/src/pages/MeritReportPage.jsx index d98675fb72..9ccb98acfb 100644 --- a/web-ui/src/pages/MeritReportPage.jsx +++ b/web-ui/src/pages/MeritReportPage.jsx @@ -79,7 +79,8 @@ const MeritReportPage = () => { if (data) { let periods = data.reduce((result, item) => { if (item.closeDate) { - result.push({label: formatReviewDate(item.closeDate), + result.push({label: item.name + " - " + + formatReviewDate(item.closeDate), id: item.id}); } return result; @@ -309,7 +310,7 @@ const MeritReportPage = () => { )} From 819a7464b58a815bc10938f589fa79ecc9fd1da4 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 17 Dec 2024 09:58:09 -0600 Subject: [PATCH 5/7] Corrected hour format of the generated markdown and updated the ReportDataController test. --- .../services/reports/MarkdownGeneration.java | 12 +- .../reports/ReportDataController.java | 6 +- .../reports/FileServicesImplReplacement.java | 59 +++++++++ .../reports/ReportDataControllerTest.java | 119 +++++++----------- 4 files changed, 117 insertions(+), 79 deletions(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java index d26afd3fed..a1eea89b78 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/MarkdownGeneration.java @@ -356,11 +356,12 @@ private void compensationHistory(ReportDataCollation data, StringBuilder sb) { .append(new Heading("Base Compensation (annual or hourly)", 3)) .append("\n"); List list = new ArrayList<>(); + final String compFormat = "%.2f"; for (CompensationHistory.Compensation comp : compBase) { String value = comp.amount(); try { double val = Double.parseDouble(value); - value = String.format("%.2f", val); + value = String.format(compFormat, val); } catch (Exception e) { } list.add(formatDate(comp.startDate()) + " - $" + value); @@ -384,13 +385,14 @@ private void employeeHours(ReportDataCollation data, StringBuilder sb) { sb.append(new Heading("Employee Hours", 2)).append("\n"); List list = new ArrayList<>(); ReportHours hours = data.getReportHours(); + final String hourFormat = "%.2f"; list.add("Contribution Hours: " + - String.format("%f", hours.contributionHours())); - list.add("PTO Hours: " + String.format("%f", hours.ptoHours())); + String.format(hourFormat, hours.contributionHours())); + list.add("PTO Hours: " + String.format(hourFormat, hours.ptoHours())); list.add("Overtime Hours: " + - String.format("%f", hours.overtimeHours())); + String.format(hourFormat, hours.overtimeHours())); list.add("Billable Utilization: " + - String.format("%f", hours.billableUtilization())); + String.format(hourFormat, hours.billableUtilization())); sb.append(new UnorderedList<>(list)).append("\n\n"); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java index 406701b092..ddf448a30a 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java @@ -15,6 +15,7 @@ import com.objectcomputing.checkins.services.file.FileServices; import io.micronaut.http.MediaType; +import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Get; @@ -122,8 +123,8 @@ private String uploadHelper(ReportDataServices.DataType dataType, @Get(uri="/generate") @RequiredPermission(Permission.CAN_CREATE_MERIT_REPORT) - public void generate(@NotNull List memberIds, - @NotNull UUID reviewPeriodId) { + public HttpStatus generate(@NotNull List memberIds, + @NotNull UUID reviewPeriodId) { MarkdownGeneration markdown = new MarkdownGeneration(reportDataServices, kudosRepository, @@ -137,5 +138,6 @@ public void generate(@NotNull List memberIds, employeeHoursServices, fileServices); markdown.upload(memberIds, reviewPeriodId); + return HttpStatus.OK; } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java new file mode 100644 index 0000000000..68b6ec4ea1 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/reports/FileServicesImplReplacement.java @@ -0,0 +1,59 @@ +package com.objectcomputing.checkins.services.reports; + +/************************************************************************* + * + * This class is here due to the fact that the ReportDataController now + * references the FileServices. The real FileServicesImpl requires the + * GoogleApiAccess class that does not exist during testing. + * + * This replacement class does not require that and can help us test the + * output of the MarkdownGeneration class. + * + ************************************************************************/ + +import com.objectcomputing.checkins.services.file.FileInfoDTO; +import com.objectcomputing.checkins.services.file.FileServices; +import com.objectcomputing.checkins.services.file.FileServicesImpl; + +import io.micronaut.http.multipart.CompletedFileUpload; + +import java.io.File; +import java.util.Set; +import java.util.HashSet; +import java.util.UUID; + +import jakarta.inject.Singleton; +import io.micronaut.context.env.Environment; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; + +@Singleton +@Replaces(FileServicesImpl.class) +@Requires(env = Environment.TEST) +public class FileServicesImplReplacement implements FileServices { + public String documentName = ""; + public String documentText = ""; + + public Set findFiles(UUID checkInId) { + return new HashSet(); + } + + public File downloadFiles(String uploadDocId) { + return null; + } + + public FileInfoDTO uploadFile(UUID checkInID, CompletedFileUpload file) { + return new FileInfoDTO(); + } + + public FileInfoDTO uploadDocument(String directory, + String name, String text) { + documentName = name; + documentText = text; + return new FileInfoDTO(); + } + + public boolean deleteFile(String uploadDocId) { + return true; + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java index d88012c0b9..baed8cbe3e 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java @@ -1,9 +1,5 @@ package com.objectcomputing.checkins.services.reports; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.core.JsonProcessingException; import com.objectcomputing.checkins.services.kudos.Kudos; import com.objectcomputing.checkins.services.TestContainersSuite; import com.objectcomputing.checkins.services.fixture.KudosFixture; @@ -22,6 +18,8 @@ import com.objectcomputing.checkins.services.feedback_template.template_question.TemplateQuestion; import com.objectcomputing.checkins.services.employee_hours.EmployeeHours; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Property; import io.micronaut.http.HttpRequest; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; @@ -34,6 +32,7 @@ import java.io.File; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; @@ -44,12 +43,16 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@Property(name = "replace.fileservicesimpl", value = StringUtils.TRUE) class ReportDataControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, KudosFixture, ReviewPeriodFixture, FeedbackTemplateFixture, FeedbackRequestFixture, TemplateQuestionFixture, FeedbackAnswerFixture, EmployeeHoursFixture { @Inject @Client("/services/report/data") HttpClient client; + @Inject + private FileServicesImplReplacement fileServices; + private EmployeeHours employeeHours; private FeedbackTemplate feedbackTemplate; private ReviewPeriod reviewPeriod; @@ -58,6 +61,7 @@ class ReportDataControllerTest extends TestContainersSuite implements MemberProf private TemplateQuestion questionTwo; private MemberProfile regular; private MemberProfile admin; + private Kudos kudos; private final String basePath = "src/test/java/com/objectcomputing/checkins/services/reports/"; @BeforeEach @@ -79,7 +83,7 @@ void createRolesAndPermissions() { saveSampleFeedbackAnswer(questionOne.getId(), feedbackRequest.getId()); saveSampleFeedbackAnswer(questionTwo.getId(), feedbackRequest.getId()); - Kudos kudos = createApprovedKudos(admin.getId()); + kudos = createApprovedKudos(admin.getId()); createKudosRecipient(kudos.getId(), regular.getId()); employeeHours = new EmployeeHours(regular.getEmployeeId(), @@ -106,45 +110,27 @@ void uploadReportDataWithoutPermission() { } @Test - void getReportData() throws JsonProcessingException { + void processReportData() { MemberProfile target = regular; HttpRequest request = postData(admin, ADMIN_ROLE); final String response = client.toBlocking().retrieve(request); assertNotNull(response); request = HttpRequest.GET( - String.format("/?memberIds=%s&reviewPeriodId=%s", + String.format("/generate?memberIds=%s&reviewPeriodId=%s", target.getId(), reviewPeriod.getId().toString())) .basicAuth(admin.getWorkEmail(), ADMIN_ROLE); - final String data = client.toBlocking().retrieve(request); - ObjectMapper objectMapper = new ObjectMapper(); - assertNotNull(data); - - // Perform minimal validation of returned data - JsonNode root = objectMapper.readTree(data); - assertTrue(root.isArray()); - assertFalse(root.isEmpty()); - - ArrayNode arrayNode = (ArrayNode)root; - JsonNode first = arrayNode.get(0); - assertNotNull(first.get("memberProfile")); - assertNotNull(first.get("kudos")); - assertNotNull(first.get("compensationHistory")); - assertNotNull(first.get("currentInformation")); - assertNotNull(first.get("positionHistory")); - assertNotNull(first.get("selfReviews")); - assertNotNull(first.get("reviews")); - assertNotNull(first.get("feedback")); - assertNotNull(first.get("hours")); - - validateReportData(first, target); + client.toBlocking().exchange(request); + + validateReportData(fileServices.documentName, + fileServices.documentText, target); } @Test - void getReportDataWithoutPermission() { + void processReportDataWithoutPermission() { final HttpRequest request = HttpRequest.GET( - String.format("/?memberIds=%s&reviewPeriodId=%s", + String.format("/generate?memberIds=%s&reviewPeriodId=%s", regular.getId(), reviewPeriod.getId())) .basicAuth(regular.getWorkEmail(), MEMBER_ROLE); @@ -168,54 +154,43 @@ HttpRequest postData(MemberProfile user, String role) { .contentType(MULTIPART_FORM_DATA); } - void validateReportData(JsonNode node, MemberProfile user) { - final String memberId = user.getId().toString(); + private String formatDate(LocalDate date) { + return date.format(DateTimeFormatter.ofPattern("MM/dd/yyyy")); + } + + void validateReportData(String filename, String text, MemberProfile user) { + assertEquals(user.getWorkEmail(), filename); + + // Review Period + assertTrue(text.contains( + formatDate(reviewPeriod.getPeriodStartDate().toLocalDate()))); + assertTrue(text.contains( + formatDate(reviewPeriod.getPeriodEndDate().toLocalDate()))); // Member Info - assertEquals(memberId, node.get("memberId").asText()); - JsonNode profile = node.get("memberProfile"); - assertEquals(user.getFirstName(), profile.get("firstName").asText()); - assertEquals(user.getLastName(), profile.get("lastName").asText()); - assertEquals(user.getTitle(), profile.get("title").asText()); + assertTrue(text.contains(user.getFirstName())); + assertTrue(text.contains(user.getLastName())); + assertTrue(text.contains(user.getTitle())); // Kudos - ArrayNode kudos = (ArrayNode)node.get("kudos"); - assertEquals(1, kudos.size()); - assertEquals("Default Kudos", kudos.get(0).get("message").asText()); - - // Compensation History - ArrayNode comp = (ArrayNode)node.get("compensationHistory"); - assertEquals(10, comp.size()); - assertEquals(memberId, comp.get(0).get("memberId").asText()); - assertTrue(comp.get(0).get("amount").asDouble() > 0); - assertFalse(comp.get(9).get("totalComp").asText().isEmpty()); - - // Current Information - JsonNode curr = node.get("currentInformation"); - assertEquals(memberId, curr.get("memberId").asText()); - assertTrue(curr.get("salary").asDouble() > 0); - assertEquals("$90000 - $150000", curr.get("range").asText()); - assertEquals("$89000 - $155000", curr.get("nationalRange").asText()); - - // Position History - ArrayNode pos = (ArrayNode)node.get("positionHistory"); - assertEquals(3, pos.size()); - assertEquals(memberId, pos.get(2).get("memberId").asText()); - assertEquals("Software Engineer", pos.get(2).get("title").asText()); + assertTrue(text.contains(kudos.getMessage())); // Feedback - ArrayNode feedback = (ArrayNode)node.get("feedback"); - assertEquals(1, feedback.size()); - ArrayNode answers = (ArrayNode)feedback.get(0).get("answers"); - assertEquals(2, answers.size()); - assertEquals("TEXT", answers.get(0).get("type").asText()); - assertEquals(1, answers.get(0).get("number").asInt()); + assertTrue(text.contains(feedbackTemplate.getTitle())); + assertTrue(text.contains(questionOne.getQuestion())); + assertTrue(text.contains(questionTwo.getQuestion())); + assertTrue(text.contains( + formatDate(feedbackRequest.getSubmitDate()))); // Hours - JsonNode hours = node.get("hours"); - assertEquals(employeeHours.getContributionHours(), hours.get("contributionHours").asDouble(), 0.0); - assertEquals(employeeHours.getPtoHours(), hours.get("ptoHours").asDouble(), 0.0); - assertEquals(employeeHours.getOvertimeWorked(), hours.get("overtimeHours").asDouble(), 0.0); - assertEquals(employeeHours.getBillableUtilization(), hours.get("billableUtilization").asDouble(), 0.0); + final String format = "%.2f"; + assertTrue(text.contains( + String.format(format, employeeHours.getContributionHours()))); + assertTrue(text.contains( + String.format(format, employeeHours.getPtoHours()))); + assertTrue(text.contains( + String.format(format, employeeHours.getOvertimeWorked()))); + assertTrue(text.contains( + String.format(format, employeeHours.getBillableUtilization()))); } } From 5a76dd4ed16f8ea30df5dbc02a5741a0c0558765 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 17 Dec 2024 10:24:25 -0600 Subject: [PATCH 6/7] Updated to reflect the label change. --- web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap b/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap index 470a70b25a..b85accd776 100644 --- a/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/MeritReportPage.test.jsx.snap @@ -173,7 +173,7 @@ exports[`renders correctly 1`] = ` for="reviewPeriodSelect" id="reviewPeriodSelect-label" > - ReviewPeriod + Review Period
- ReviewPeriod + Review Period From b5e49776be1fa99a42a8828a404777a285b6637e Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Tue, 17 Dec 2024 11:54:59 -0600 Subject: [PATCH 7/7] Changed report generation to a POST so that member ids could be part of the body instead of a URL parameter. --- .../reports/ReportDataController.java | 9 +++-- .../services/reports/ReportDataDTO.java | 40 +------------------ .../reports/ReportDataControllerTest.java | 22 ++++++---- web-ui/src/api/generic.js | 7 ++-- 4 files changed, 24 insertions(+), 54 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java index ddf448a30a..1a5a15626b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataController.java @@ -17,6 +17,7 @@ import io.micronaut.http.MediaType; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Get; import io.micronaut.http.multipart.CompletedFileUpload; @@ -30,6 +31,7 @@ import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; import jakarta.validation.constraints.NotNull; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,10 +123,9 @@ private String uploadHelper(ReportDataServices.DataType dataType, } } - @Get(uri="/generate") + @Post(uri="/generate") @RequiredPermission(Permission.CAN_CREATE_MERIT_REPORT) - public HttpStatus generate(@NotNull List memberIds, - @NotNull UUID reviewPeriodId) { + public HttpStatus generate(@Body @Valid ReportDataDTO dto) { MarkdownGeneration markdown = new MarkdownGeneration(reportDataServices, kudosRepository, @@ -137,7 +138,7 @@ public HttpStatus generate(@NotNull List memberIds, templateQuestionServices, employeeHoursServices, fileServices); - markdown.upload(memberIds, reviewPeriodId); + markdown.upload(dto.getMemberIds(), dto.getReviewPeriodId()); return HttpStatus.OK; } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java index 2d290145eb..d9fa1566b2 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reports/ReportDataDTO.java @@ -1,58 +1,20 @@ package com.objectcomputing.checkins.services.reports; -import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import io.micronaut.core.annotation.Introspected; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; -import java.time.LocalDate; import java.util.List; import java.util.UUID; @Getter @Setter -@AllArgsConstructor @Introspected public class ReportDataDTO { - @NotNull - private UUID memberId; + private List memberIds; @NotNull private UUID reviewPeriodId; - - @NotNull - private LocalDate startDate; - - @NotNull - private LocalDate endDate; - - @NotNull - private MemberProfile memberProfile; - - @NotNull - private List kudos; - - @NotNull - private List compensationHistory; - - @NotNull - private CurrentInformation.Information currentInformation; - - @NotNull - private List positionHistory; - - @NotNull - private List selfReviews; - - @NotNull - private List reviews; - - @NotNull - private List feedback; - - @NotNull - private ReportHours hours; } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java index baed8cbe3e..06c9cb7fa5 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/reports/ReportDataControllerTest.java @@ -31,6 +31,8 @@ import org.junit.jupiter.api.Test; import java.io.File; +import java.util.UUID; +import java.util.ArrayList; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -116,10 +118,12 @@ void processReportData() { final String response = client.toBlocking().retrieve(request); assertNotNull(response); - request = HttpRequest.GET( - String.format("/generate?memberIds=%s&reviewPeriodId=%s", - target.getId(), - reviewPeriod.getId().toString())) + ReportDataDTO dto = new ReportDataDTO(); + ArrayList memberIds = new ArrayList<>(); + memberIds.add(target.getId()); + dto.setReviewPeriodId(reviewPeriod.getId()); + dto.setMemberIds(memberIds); + request = HttpRequest.POST("/generate", dto) .basicAuth(admin.getWorkEmail(), ADMIN_ROLE); client.toBlocking().exchange(request); @@ -129,10 +133,12 @@ void processReportData() { @Test void processReportDataWithoutPermission() { - final HttpRequest request = HttpRequest.GET( - String.format("/generate?memberIds=%s&reviewPeriodId=%s", - regular.getId(), - reviewPeriod.getId())) + ReportDataDTO dto = new ReportDataDTO(); + ArrayList memberIds = new ArrayList<>(); + memberIds.add(regular.getId()); + dto.setReviewPeriodId(reviewPeriod.getId()); + dto.setMemberIds(memberIds); + final HttpRequest request = HttpRequest.POST("/generate", dto) .basicAuth(regular.getWorkEmail(), MEMBER_ROLE); HttpClientResponseException responseException = assertThrows(HttpClientResponseException.class, diff --git a/web-ui/src/api/generic.js b/web-ui/src/api/generic.js index 712f072e04..23998c6403 100644 --- a/web-ui/src/api/generic.js +++ b/web-ui/src/api/generic.js @@ -26,12 +26,13 @@ export const downloadData = (url, cookie, params) => { export const initiate = (url, cookie, params) => { return resolve({ - method: 'GET', - params: params, + method: 'POST', headers: { 'X-CSRF-Header': cookie, Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' }, - url: url + url: url, + data: params, }); };