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 all 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
4 changes: 4 additions & 0 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ class Meta:
def periods(self) -> list[float]:
return [getattr(self, month) for month in MONTHS]

@property
def periods_as_percentage(self) -> list[float]:
return [month * 100 for month in self.periods]

financial_year = models.ForeignKey(FinancialYear, on_delete=models.PROTECT)
apr = models.FloatField(default=1.0)
may = models.FloatField(default=1.0)
Expand Down
115 changes: 88 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,7 +24,19 @@ export default function Payroll() {
vacanciesReducer,
initialVacanciesState,
);
const [payModifiers, dispatchPayModifiers] = useReducer(
payModifiersReducer,
initialPayModifiersState,
);
const [saveSuccess, setSaveSuccess] = useState(false);
const [activeTab, setActiveTab] = useState(() => {
const savedTab = localStorage.getItem("editPayroll.activeTab");
return savedTab ? parseInt(savedTab) : 0;
});

useEffect(() => {
localStorage.setItem("editPayroll.activeTab", activeTab);
}, [activeTab]);

useEffect(() => {
const savedSuccessFlag = localStorage.getItem("saveSuccess");
Expand All @@ -34,6 +49,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 +69,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 +88,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 +106,44 @@ 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 activeTab={activeTab} setActiveTab={setActiveTab}>
<Tab label="Payroll" key="1">
<PayrollTable
payroll={payroll}
headers={payrollHeaders}
onTogglePayPeriods={handleTogglePayPeriods}
RowComponent={EmployeeRow}
/>
</Tab>
<Tab label="Non-payroll" key="2">
<PayrollTable
payroll={nonPayroll}
headers={payrollHeaders}
onTogglePayPeriods={handleTogglePayPeriods}
RowComponent={EmployeeRow}
/>
</Tab>
<Tab label="Vacancies" key="3">
<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="4">
<EditPayModifier
data={payModifiers}
onInputChange={handleUpdatePayModifiers}
/>
</Tab>
</Tabs>
<button className="govuk-button" onClick={handleSavePayroll}>
Save payroll
</button>
Expand Down Expand Up @@ -142,5 +176,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);
46 changes: 46 additions & 0 deletions front_end/src/Components/EditPayroll/EditPayModifier/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { monthsToTitleCase } from "../../../Util";

const EditPayModifier = ({ data, onInputChange }) => {
if (data.length === 0) {
return <p class="govuk-body">No modifiers set</p>;
}
return data.map((row, index) => (
<div className="govuk-form-group" key={index}>
<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">
{row.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(row.id, index, e.target.value)
}
></input>
</td>
);
})}
</tr>
</tbody>
</table>
</div>
));
};

export default EditPayModifier;
40 changes: 40 additions & 0 deletions front_end/src/Components/EditPayroll/Tabs/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export default function Tabs({ activeTab, setActiveTab, children }) {
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={tab.props.label}
>
<h2 className="govuk-heading-m">{tab.props.label}</h2>
{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 - Pay modifier data to be sent.
* @returns {import("../../Util").PostDataResponse} Updated pay modifier 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 {number} id - The pay modifier's pk.
* @property {number[]} 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
Loading
Loading