Skip to content

Commit

Permalink
Adding notes column to the tables
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisDDBT committed Feb 25, 2025
1 parent 3105eeb commit d441dab
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 67 deletions.
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 {
// 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];
34 changes: 34 additions & 0 deletions front_end/styles/notes.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.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);
}
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

0 comments on commit d441dab

Please sign in to comment.