Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FFT-187 Adding notes api for payroll #624

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
16 changes: 9 additions & 7 deletions front_end/src/Components/Common/ErrorSummary/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export default function ErrorSummary({ errors }) {
</h2>
<div className="govuk-error-summary__body">
<ul className="govuk-list govuk-error-summary__list">
{errors.map((error) => {
return (
<li key={error.label}>
<a href={`#${error.label}`}>{error.message}</a>
</li>
);
})}
{errors &&
Array.isArray(errors) &&
errors.map((error) => {
return (
<li key={error.label}>
<a href={`#${error.label}`}>{error.message}</a>
</li>
);
})}
</ul>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions front_end/src/Components/EditPayroll/EmployeeRow/index.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import NotesCell from "../../Notes/NotesCell";
import PayPeriods from "../PayPeriods";

const EmployeeRow = ({
Expand All @@ -22,6 +23,9 @@ const EmployeeRow = ({
previousMonths={previousMonths}
showPreviousMonths={showPreviousMonths}
/>
<td className="govuk-table__cell">
<NotesCell notes={row.notes} employee_no={row.employee_no} />
</td>
</tr>
);
};
Expand Down
4 changes: 4 additions & 0 deletions front_end/src/Components/EditPayroll/VacancyRow/index.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import NotesCell from "../../Notes/NotesCell";
import PayPeriods from "../PayPeriods";

const VacancyRow = ({
Expand Down Expand Up @@ -31,6 +32,9 @@ const VacancyRow = ({
previousMonths={previousMonths}
showPreviousMonths={showPreviousMonths}
/>
<td className="govuk-table__cell">
<NotesCell notes={row.notes} employee_no={row.employee_no} />
</td>
</tr>
);
};
Expand Down
4 changes: 2 additions & 2 deletions front_end/src/Components/EditPayroll/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export function getPayrollData() {
* @returns {import("../../Util").PostDataResponse} Updated payroll data received.
*/
export function postPayrollData(payrollData) {
return postJsonData(getPayrollApiUrl(), JSON.stringify(payrollData));
return postJsonData(getPayrollApiUrl(), payrollData);
}

/**
* Create default pay modifier object
*/
export function createPayModifiers() {
return postJsonData(getPayrollApiUrl() + "pay_modifiers/", {});
return postJsonData(getPayrollApiUrl() + "pay_modifiers/");
}

/**
Expand Down
8 changes: 6 additions & 2 deletions front_end/src/Components/EditPayroll/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export const payrollHeaders = [
"Programme Code",
"Budget Type",
"Assignment Status",
].concat(monthHeaders);
]
.concat(monthHeaders)
.concat(["Notes"]);

export const vacancyHeaders = [
"Manage",
Expand All @@ -33,4 +35,6 @@ export const vacancyHeaders = [
"Hiring Manager",
"HR Ref",
"Recruitment Stage",
].concat(monthHeaders);
]
.concat(monthHeaders)
.concat(["Notes"]);
169 changes: 169 additions & 0 deletions front_end/src/Components/Notes/NotesCell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useState } from "react";
import { getURLSegment, postJsonData } from "../../Util";
import ErrorSummary from "../Common/ErrorSummary";

const Modal = ({ isOpen, notes, employee_no, onClose, onSave }) => {
const charLimit = 200;
const errorMsg =
"There’s a problem with the system it can’t save this note, try again later.";
const currentLimit = charLimit - (notes?.length || 0);
const [currentNotes, setCurrentNotes] = useState(notes || "");
const [charLeft, setCharLeft] = useState(currentLimit);
const [errorMessage, setErrorMessage] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const financialYear = String(window.financialYear);
const costCentre = String(window.costCentreCode);

if (!isOpen) return null;

const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setErrorMessage("");

try {
const response = await postJsonData(
`/payroll/api/${costCentre}/${financialYear}/employees/notes`,
{
employee_no,
notes: currentNotes,
},
);

if (response.status >= 200 && response.status < 300) {
onSave(currentNotes);
onClose();
} else {
setErrorMessage(errorMsg);
}
} catch (error) {
console.error("Error saving notes:", error);
setErrorMessage(errorMsg);
} finally {
setIsSubmitting(false);
}
};

const handleTextChange = (e) => {
setCurrentNotes(e.target.value);
setCharLeft(charLimit - e.target.value.length);
};

const isExceeded = charLeft < 0;
const formGroupClasses = `govuk-form-group ${errorMessage ? "govuk-form-group--error" : ""}`;

return (
<dialog className="govuk-modal-dialog" aria-labelledby="modal-title" open>
<div className="govuk-modal-overlay">
<div className="govuk-modal">
<div className="govuk-modal__header">
<h2 className="govuk-heading-m" id="modal-title">
{notes ? "Edit note" : "Add note"}
</h2>
<span className="govuk-hint">
Notes will be reset at the end of the {financialYear} financial
year
</span>
</div>

<form onSubmit={handleSubmit} noValidate>
{errorMessage && (
<ErrorSummary errors={[{ message: errorMessage, label: "" }]} />
)}

<div className={formGroupClasses}>
<div
className="govuk-character-count"
data-module="govuk-character-count"
data-maxlength={charLimit}
>
<div className="govuk-form-group">
<label className="govuk-label" htmlFor="notes">
Notes
</label>
{isExceeded && (
<p id="notes-error" className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
Notes must be {charLimit} characters or fewer
</p>
)}
<textarea
className={`govuk-textarea govuk-js-character-count ${isExceeded ? "govuk-textarea--error" : ""}`}
id="notes"
name="notes"
rows="5"
aria-describedby="notes-hint notes-info"
value={currentNotes}
onChange={handleTextChange}
maxLength={charLimit}
/>
</div>
<div
id="notes-info"
className={`govuk-hint govuk-character-count__message ${isExceeded ? "govuk-error-message" : ""}`}
aria-live="polite"
>
You have {charLeft} characters remaining
</div>
</div>
</div>

<div className="govuk-button-group">
<button
type="submit"
className="govuk-button"
data-module="govuk-button"
disabled={isSubmitting || isExceeded}
>
Save
</button>
<button
type="button"
onClick={onClose}
className="govuk-button govuk-button--secondary"
data-module="govuk-button"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</dialog>
);
};

const NotesCell = ({ notes = "", employee_no }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentNotes, setCurrentNotes] = useState(notes);

const handleSave = (newNotes) => {
setCurrentNotes(newNotes);
};

return (
<>
<a
href="#"
title={currentNotes}
className="govuk-link"
onClick={(e) => {
e.preventDefault();
setIsModalOpen(true);
}}
>
{currentNotes ? "Edit note" : "Add note"}
</a>
<span className="truncate govuk-!-margin-left-3">{currentNotes}</span>
<Modal
isOpen={isModalOpen}
notes={currentNotes}
employee_no={employee_no}
onClose={() => setIsModalOpen(false)}
onSave={handleSave}
/>
</>
);
};

export default NotesCell;
86 changes: 62 additions & 24 deletions front_end/src/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,34 +81,69 @@ export async function getData(url) {
* @param {?string} content_type - Content-Type header for the body.
* @returns {PostDataResponse}
*/
export async function postData(url = "", data = {}, headers = {}) {
export async function postData(url, data = {}, headers = {}) {
if (!url) {
throw new Error("URL is required");
}
const csrftoken = window.CSRF_TOKEN;
try {
Copy link
Contributor

Choose a reason for hiding this comment

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

i feel uneasy about some of the changes made here. i need to spend more time here.

// Default options are marked with *
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "same-origin", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"X-CSRFToken": csrftoken,
...headers,
},
redirect: "follow", // manual, *follow, error
referrer: "no-referrer", // no-referrer, *client
body: data, // body data type must match "Content-Type" header
});

// Status codes that typically don't have response bodies
const noContentCodes = [204, 205, 304];

// Check if the response status indicates no content
if (noContentCodes.includes(response.status)) {
return {
status: response.status,
data: null, // No content to parse
};
}

// Default options are marked with *
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "same-origin", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"X-CSRFToken": csrftoken,
...headers,
},
redirect: "follow", // manual, *follow, error
referrer: "no-referrer", // no-referrer, *client
body: data, // body data type must match "Content-Type" header
});

let jsonData = await response.json();

return {
status: response.status,
data: jsonData, // parses JSON response into native JavaScript objects
};
// For responses that might have content, try to parse JSON
try {
const data = await response.json();
return {
status: response.status,
data,
};
} catch (jsonError) {
// Handle case where response exists but isn't valid JSON
return {
status: response.status,
data: null,
parseError: true,
};
}
} catch (e) {
return {
status: e.status || 500,
data: {
error: true,
message: e.message || "Unknown error occurred",
name: e.name || "Error",
},
};
}
}

export async function postJsonData(url = "", data = {}) {
return postData(url, data, { "Content-Type": "application/json" });
export async function postJsonData(url, data = {}) {
return postData(url, JSON.stringify(data), {
"Content-Type": "application/json",
});
}

export const processForecastData = (forecastData) => {
Expand Down Expand Up @@ -205,3 +240,6 @@ export function getScriptJsonData(id) {
resolve(json);
});
}

export const getURLSegment = (index = 0) =>
window.location.pathname.split("/").filter(Boolean).reverse()[index];
37 changes: 37 additions & 0 deletions front_end/styles/notes.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.govuk-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}

.govuk-modal {
background: #ffffff;
padding: 30px;
margin: 30px;
width: 100%;
max-width: 640px;
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.3);
}

.govuk-modal__header {
margin-bottom: 20px;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
width: 200px;
display: inline-block;
transform: translateY(6px);
Copy link
Contributor

Choose a reason for hiding this comment

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

this feels a bit magic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it is, Paul has asked for ellipsis which is not active if your element is inline, and if I convert it to inline-block, line heights etc are getting different to the next link element. I could use flex or grid but they also come with different caveats.

Copy link
Contributor

Choose a reason for hiding this comment

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

idea: could we handle it in javascript and truncate the string there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a rule of thumb, I try to avoid css task to be outsourced to js. Having said that, if you are keen to do it in js, sure, I can add some function to truncate the text and add ... if content is more thatn 20 chars

}
.truncate:empty {
display: none;
}
Loading