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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
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("activeTab");
return savedTab ? parseInt(savedTab) : 0;
});

useEffect(() => {
localStorage.setItem("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="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">
<h2 className="govuk-heading-m">Attrition</h2>
<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);
47 changes: 47 additions & 0 deletions front_end/src/Components/EditPayroll/EditPayModifier/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { monthsToTitleCase } from "../../../Util";

const EditPayModifier = ({ data, onInputChange }) => {
return (
data.length > 0 &&
data.map((row, index) => (
<div className="govuk-form-group" key={index}>
{console.log(row)}
<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;
41 changes: 41 additions & 0 deletions front_end/src/Components/EditPayroll/Tabs/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from "react";

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}
>
{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 {float[]} pay_modifiers - The pay modifier's monthly percentages
CaitBarnard marked this conversation as resolved.
Show resolved Hide resolved
*/

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