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-141: UI Pay Modifiers / attrition #579

Merged
merged 21 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
106 changes: 79 additions & 27 deletions front_end/src/Apps/Payroll.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
import EmployeeRow from "../Components/EditPayroll/EmployeeRow";
import VacancyRow from "../Components/EditPayroll/VacancyRow";
import PayrollTable from "../Components/EditPayroll/PayrollTable";
import Tabs, { Tab } from "../Components/EditPayroll/Tabs";
import EditPayModifier from "../Components/EditPayroll/EditPayModifier";

const initialPayrollState = [];
const initialVacanciesState = [];
const initialPayModifiersState = [];

export default function Payroll() {
const [allPayroll, dispatch] = useReducer(
Expand All @@ -21,6 +24,10 @@ export default function Payroll() {
vacanciesReducer,
initialVacanciesState,
);
const [payModifiers, dispatchPayModifiers] = useReducer(
payModifiersReducer,
initialPayModifiersState,
);
const [saveSuccess, setSaveSuccess] = useState(false);

useEffect(() => {
Expand All @@ -34,6 +41,9 @@ export default function Payroll() {
api
.getVacancyData()
.then((data) => dispatchVacancies({ type: "fetched", data }));
api
.getPayModifierData()
.then((data) => dispatchPayModifiers({ type: "fetched", data }));
}, []);

// Computed properties
Expand All @@ -51,6 +61,7 @@ export default function Payroll() {
try {
await api.postPayrollData(allPayroll);
await api.postVacancyData(vacancies);
await api.postPayModifierData(payModifiers);

setSaveSuccess(true);
localStorage.setItem("saveSuccess", "true");
Expand All @@ -69,6 +80,10 @@ export default function Payroll() {
dispatchVacancies({ type: "updatePayPeriods", id, index, enabled });
}

function handleUpdatePayModifiers(id, index, value) {
dispatchPayModifiers({ type: "updatePayModifiers", id, index, value });
}

return (
<>
{saveSuccess && (
Expand All @@ -83,33 +98,43 @@ export default function Payroll() {
</div>
</div>
)}
<h2 className="govuk-heading-m">Payroll</h2>
<PayrollTable
payroll={payroll}
headers={payrollHeaders}
onTogglePayPeriods={handleTogglePayPeriods}
RowComponent={EmployeeRow}
/>
<h2 className="govuk-heading-m">Non-payroll</h2>
<PayrollTable
payroll={nonPayroll}
headers={payrollHeaders}
onTogglePayPeriods={handleTogglePayPeriods}
RowComponent={EmployeeRow}
/>
<h2 className="govuk-heading-m">Vacancies</h2>
<PayrollTable
payroll={vacancies}
headers={vacancyHeaders}
onTogglePayPeriods={handleToggleVacancyPayPeriods}
RowComponent={VacancyRow}
/>
<a
className="govuk-button govuk-!-margin-right-2 govuk-button--secondary"
href={window.addVacancyUrl}
>
Add Vacancy
</a>
<Tabs>
<Tab label="Dashboard" key="1">
<h2 className="govuk-heading-m">Payroll</h2>
<PayrollTable
payroll={payroll}
headers={payrollHeaders}
onTogglePayPeriods={handleTogglePayPeriods}
RowComponent={EmployeeRow}
/>
<h2 className="govuk-heading-m">Non-payroll</h2>
<PayrollTable
payroll={nonPayroll}
headers={payrollHeaders}
onTogglePayPeriods={handleTogglePayPeriods}
RowComponent={EmployeeRow}
/>
<h2 className="govuk-heading-m">Vacancies</h2>
<PayrollTable
payroll={vacancies}
headers={vacancyHeaders}
onTogglePayPeriods={handleToggleVacancyPayPeriods}
RowComponent={VacancyRow}
/>
<a
className="govuk-button govuk-!-margin-right-2 govuk-button--secondary"
href={window.addVacancyUrl}
>
Add Vacancy
</a>
</Tab>
<Tab label="Pay Modifiers" key="2">
<EditPayModifier
data={payModifiers}
onInputChange={handleUpdatePayModifiers}
/>
</Tab>
</Tabs>
<button className="govuk-button" onClick={handleSavePayroll}>
Save payroll
</button>
Expand Down Expand Up @@ -142,5 +167,32 @@ const positionReducer = (data, action) => {
}
};

const payModifiersReducer = (data, action) => {
switch (action.type) {
case "fetched": {
return action.data;
}
case "updatePayModifiers": {
return data.map((row) => {
if (row.id === action.id) {
const updatedPayModifier = row.pay_modifiers.map(
(modifier, index) => {
if (index === action.index) {
return parseFloat(action.value);
}
return modifier;
},
);
return {
...row,
pay_modifiers: updatedPayModifier,
};
}
return row;
});
}
}
};

const payrollReducer = (data, action) => positionReducer(data, action);
const vacanciesReducer = (data, action) => positionReducer(data, action);
45 changes: 45 additions & 0 deletions front_end/src/Components/EditPayroll/EditPayModifier/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { monthsToTitleCase } from "../../../Util";

const EditPayModifier = ({ data, onInputChange }) => {
return (
data.length > 0 && (
<div className="govuk-form-group">
<table className="govuk-table">
<thead className="govuk-table__head">
<tr className="govuk-table__row">
{monthsToTitleCase.map((header) => {
return (
<th scope="col" className="govuk-table__header" key={header}>
{header}
</th>
);
})}
</tr>
</thead>
<tbody className="govuk-table__body">
<tr className="govuk-table__row">
{data[0].pay_modifiers.map((value, index) => {
return (
<td className="govuk-table__cell" key={index}>
<input
className="govuk-input"
id={`modifier-${index}`}
name={`modifier-${index}`}
type="number"
defaultValue={value}
onChange={(e) =>
onInputChange(data[0].id, index, e.target.value)
}
></input>
</td>
);
})}
</tr>
</tbody>
</table>
</div>
)
);
};

export default EditPayModifier;
42 changes: 42 additions & 0 deletions front_end/src/Components/EditPayroll/Tabs/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from "react";

export default function Tabs({ children }) {
const [activeTab, setActiveTab] = useState(0);
const tabs = Array.isArray(children) ? children : [children];
return (
<>
<div className="govuk-tabs" data-module="govuk-tabs">
<h2 className="govuk-tabs__title">Contents</h2>
<ul className="govuk-tabs__list">
{tabs.map((tab, index) => (
<li
className={`govuk-tabs__list-item ${activeTab === index ? "govuk-tabs__list-item--selected" : ""}`}
key={index}
>
<a
className="govuk-tabs__tab"
href="#"
onClick={() => setActiveTab(index)}
>
{tab.props.label}
</a>
</li>
))}
</ul>
{tabs.map((tab, index) => (
<div
className={`govuk-tabs__panel ${activeTab === index ? "" : "govuk-tabs__panel--hidden"}`}
key={index}
id="dashboard"
>
{tab.props.children}
</div>
))}
</div>
</>
);
}

export const Tab = ({ children }) => {
return <div>{{ children }}</div>;
};
30 changes: 28 additions & 2 deletions front_end/src/Components/EditPayroll/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { getData, postData } from "../../Util";

import * as types from "./types";

const vacanciesSlug = "vacancies/";
const payModifiersSlug = "pay_modifiers/";

/**
* Fetch payroll data and return it as a promise.
* @returns {Promise<types.PayrollData[]>} A promise resolving to an array of objects containing employee information.
Expand All @@ -25,7 +28,7 @@ export function postPayrollData(payrollData) {
* @returns {Promise<types.VacancyData[]>} A promise resolving to an array of objects containing vacancy information.
*/
export function getVacancyData() {
return getData(getPayrollApiUrl() + "vacancies/").then((data) => data.data);
return getData(getPayrollApiUrl() + vacanciesSlug).then((data) => data.data);
}

/**
Expand All @@ -36,11 +39,34 @@ export function getVacancyData() {
*/
export function postVacancyData(vacancyData) {
return postData(
getPayrollApiUrl() + "vacancies/",
getPayrollApiUrl() + vacanciesSlug,
JSON.stringify(vacancyData),
);
}

/**
* Fetch pay modifier data and return it as a promise.
* @returns {Promise<types.PayModifierData[]>} A promise resolving to an array of objects containing pay modifier information.
*/
export function getPayModifierData() {
return getData(getPayrollApiUrl() + payModifiersSlug).then(
(data) => data.data,
);
}

/**
* Post modified pay modifiers data.
*
* @param {types.PayModifierData[]} payModifierData - Vacancy data to be sent.
* @returns {import("../../Util").PostDataResponse} Updated vacancy data received.
*/
export function postPayModifierData(payModifierData) {
return postData(
getPayrollApiUrl() + payModifiersSlug,
JSON.stringify(payModifierData),
);
}

/**
* Return the payroll API URL.
*
Expand Down
6 changes: 6 additions & 0 deletions front_end/src/Components/EditPayroll/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@
* @property {boolean[]} pay_periods - Whether the vacancy is being paid in periods.
*/

/**
* @typedef {Object} PayModifierData
* @property {string} id - The pay modifier's pk.
* @property {float[]} pay_modifiers - The pay modifier's monthly percentages
*/

export const Types = {};
4 changes: 4 additions & 0 deletions front_end/src/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const months = [
"mar",
];

export const monthsToTitleCase = months.map(
(x) => x[0].toUpperCase() + x.slice(1),
);

function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== "") {
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading