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
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
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"]);
185 changes: 185 additions & 0 deletions front_end/src/Components/Notes/NotesCell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import React, { useState } from "react";
import { getURLSegment, postJsonData } from "../../Util";

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);

if (!isOpen) return null;

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

try {
const financialYear = getURLSegment(0);
const costCentre = getURLSegment(1);
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 {getURLSegment(0)} financial
year
</span>
</div>

<form onSubmit={handleSubmit} noValidate>
{errorMessage && (
<div
className="govuk-error-summary"
aria-labelledby="error-summary-title"
role="alert"
tabIndex="-1"
data-module="govuk-error-summary"
>
<h2
className="govuk-error-summary__title"
id="error-summary-title"
>
There is a problem
</h2>
<div className="govuk-error-summary__body">
<ul className="govuk-list govuk-error-summary__list">
<li>{errorMessage}</li>
</ul>
</div>
</div>
)}
<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;
79 changes: 57 additions & 22 deletions front_end/src/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,64 @@ export async function getData(url) {
*/
export async function postData(url = "", data = {}, headers = {}) {
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" });
return postData(url, JSON.stringify(data), {
"Content-Type": "application/json",
});
}

export const processForecastData = (forecastData) => {
Expand Down Expand Up @@ -205,3 +237,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;
}
2 changes: 1 addition & 1 deletion front_end/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ $govuk-page-width: 1800px;
@import "./action-bar.scss";
@import "./table.scss";
@import "./cost-centre-filter.scss";

@import "./notes.scss";
body {
font-size: 16px;
}
Expand Down
Loading