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

Feature/salary and benefits calculator #506

Merged
merged 10 commits into from
Aug 26, 2024
8 changes: 7 additions & 1 deletion src/components/forms/inputField/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ interface InputFieldProps {
autoComplete?: HTMLInputAutoCompleteAttribute;
autoCorrect?: string;
type?: HTMLInputTypeAttribute;
max?: number;
min?: number;
spellCheck?: "true" | "false";
autoCapitalize?: string;
value: string;
value: string | number;
onChange: (name: string, value: string) => void;
required?: boolean;
}
Expand All @@ -24,6 +26,8 @@ const InputField = ({
autoComplete,
autoCorrect = "off",
type = "text",
max,
min,
spellCheck,
autoCapitalize,
value,
Expand Down Expand Up @@ -56,6 +60,8 @@ const InputField = ({
autoComplete={autoComplete}
autoCorrect={autoCorrect}
type={type}
max={type === "number" ? max : undefined}
min={type === "number" ? min : undefined}
className={styles.input}
spellCheck={spellCheck}
value={value}
anemne marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
84 changes: 20 additions & 64 deletions src/components/forms/radioButtonGroup/RadioButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,31 @@ import styles from "src/components/forms/radioButtonGroup/radioButtonGroup.modul
import { RadioButton } from "./components/RadioButton";
import textStyles from "src/components/text/text.module.css";

interface IOption {
export interface IOption {
id: string;
label: string;
disabled: boolean;
currentChecked: boolean;
}

interface RenderOptionsProps {
options: IOption[];
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
}

interface RadioButtonGroupProps {
id: string;
label: string;
options: IOption[];
selectedId: string;
onValueChange: (option: IOption) => void;
}

/**
* Important Note on RadioButtons:
*
* - The `RadioButton` component, defined in this code, should not be used in isolation.
* Radio buttons are designed to be part of a group where only one option can be selected at a time.
*
* - When used individually, a radio button loses its intended functionality of providing mutually exclusive choices
* and may confuse users or lead to unexpected behavior.
*
* - Instead, radio buttons should always be used within a group, typically managed by a parent component
* such as `RadioButtonGroup`, which ensures that only one radio button in the group can be selected at any given time.
*
* - The parent component should handle the state management and changes, ensuring that the user can only select
* one option from the group and that this selection is properly communicated back to the application.
*
* - Example of usage within a group:
*
* ```
* const options = [
* { id: 'radio1', label: 'Option 1', value: '1', currentChecked: false },
* { id: 'radio2', label: 'Option 2', value: '2', currentChecked: true },
* ];
*
* <RadioButtonGroup
* id="example-group"
* label="Choose an option"
* options={options}
* onValueChange={(name, value) => console.log(`Selected ${name}: ${value}`)}
* />
* ```
* - In this example, the `RadioButtonGroup` component renders multiple `RadioButton` components
* as part of a cohesive group, enabling proper radio button functionality.
*/

export const RadioButtonGroup = ({
id,
label,
options,
selectedId,
onValueChange,
}: RadioButtonGroupProps) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedOption = options.find((option) => option.id === e.target.id);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedOption = options.find(
(option) => option.id === e.target.value,
);
if (selectedOption) {
onValueChange(selectedOption);
}
Expand All @@ -73,27 +37,19 @@ export const RadioButtonGroup = ({
<fieldset className={styles.fieldset} id={id}>
<legend className={textStyles.h3}>{label}</legend>
<div className={styles.wrapper}>
<RenderOptions options={options} onChange={handleChange} />
{options.map(({ id, label, disabled }) => (
<RadioButton
key={id}
id={id}
label={label}
name="radio"
disabled={disabled}
value={id}
checked={id === selectedId}
onChange={onChange}
/>
))}
</div>
</fieldset>
);
};

const RenderOptions = ({ options, onChange }: RenderOptionsProps) => {
return (
<>
{options.map(({ id, label, disabled, currentChecked }) => (
<RadioButton
key={id}
id={id}
label={label}
name="radio"
disabled={disabled}
value={label}
defaultChecked={currentChecked}
onChange={onChange}
/>
))}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.fieldset {
border: 0 none;
padding: 0;
}

.wrapper {
Expand Down
63 changes: 63 additions & 0 deletions src/salaryAndBenefits/SalaryAndBenefits.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,79 @@
"use client";
import styles from "./salaryAndBenefits.module.css";
import Text from "src/components/text/Text";
import { SalaryAndBenefitsPage } from "studio/lib/payloads/salaryAndBenefits";
import { RichText } from "src/components/richText/RichText";
import SalaryCalculator, {
Degree,
} from "./components/salaryCalculator/SalaryCalculator";
import { useState } from "react";
import { calculatePension, calculateSalary } from "./utils/calculateSalary";

interface SalaryAndBenefitsProps {
salaryAndBenefits: SalaryAndBenefitsPage;
}

interface SalaryCalculatorFormState {
examinationYear: number;
selectedDegree: Degree;
}

const SalaryAndBenefits = ({ salaryAndBenefits }: SalaryAndBenefitsProps) => {
const currentYear = new Date().getFullYear();

const [formState, setFormState] = useState<SalaryCalculatorFormState>({
examinationYear: currentYear - 1,
selectedDegree: "bachelor",
});
const [salary, setSalary] = useState<number | null>(null);

const updateSelectedDegree = (newDegree: Degree) => {
setFormState((prevState) => ({
...prevState,
selectedDegree: newDegree,
}));
};

const updateExaminationYear = (newYear: number) => {
setFormState((prevState) => ({
...prevState,
examinationYear: newYear,
}));
};

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
setSalary(
calculateSalary(
currentYear,
formState.examinationYear,
formState.selectedDegree
)
);
};

return (
<div className={styles.wrapper}>
<Text type="h1">{salaryAndBenefits.basicTitle}</Text>
{salaryAndBenefits.showSalaryCalculator && (
<>
<SalaryCalculator
examinationYearValue={formState.examinationYear}
selectedDegree={formState.selectedDegree}
onDegreeChanged={updateSelectedDegree}
onExaminationYearChanged={updateExaminationYear}
onSubmit={handleSubmit}
/>
{salary !== null ? (
<div aria-live="polite">
<Text> Du vil få en årlig lønn på {salary}</Text>
<Text>
Du vil få en årlig pensjon på omtrent {calculatePension(salary)}
</Text>
</div>
) : null}
</>
)}
<div className={styles.benefits}>
{salaryAndBenefits.benefits.map((benefit) => (
<div key={benefit._key} className={styles.benefitWrapper}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import styles from "./salaryCalculator.module.css";
import InputField from "src/components/forms/inputField/InputField";
import {
IOption,
RadioButtonGroup,
} from "src/components/forms/radioButtonGroup/RadioButtonGroup";
import Button from "src/components/buttons/Button";
import { maxExperience } from "src/salaryAndBenefits/utils/calculateSalary";

export type Degree = "bachelor" | "master";

const degreeOptions: IOption[] = [
{ id: "bachelor", label: "Bachelor" },
{ id: "master", label: "Master" },
];

interface SalaryCalculatorProps {
examinationYearValue: number;
selectedDegree: Degree;
onDegreeChanged: (degree: Degree) => void;
onExaminationYearChanged: (examinationYear: number) => void;
onSubmit: (event: React.FormEvent) => void;
}

export default function SalaryCalculator({
examinationYearValue: yearValue,
selectedDegree,
onDegreeChanged,
onExaminationYearChanged,
onSubmit,
}: SalaryCalculatorProps) {
const currentYear = new Date().getFullYear();
const minExaminationYear = maxExperience(currentYear);
const maxExaminationYear = currentYear - 1;

return (
<form
aria-label="salary calculator"
anemne marked this conversation as resolved.
Show resolved Hide resolved
className={styles.calculator}
onSubmit={onSubmit}
>
<RadioButtonGroup
id="degree-group"
label="Choose your degree"
options={degreeOptions}
selectedId={selectedDegree}
onValueChange={(selectedOption) =>
onDegreeChanged(selectedOption.id as Degree)
}
/>
<InputField
label="Year"
name="examinationYear"
type="number"
min={minExaminationYear}
max={maxExaminationYear}
value={yearValue}
onChange={(_name, value) => onExaminationYearChanged(parseInt(value))}
required
/>
<Button type="secondary" size="small">
Submit
</Button>
</form>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.calculator {
display: flex;
flex-direction: column;
gap: 2rem;
}
28 changes: 28 additions & 0 deletions src/salaryAndBenefits/utils/calculateSalary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { payscale } from "./salaryData";

interface PayScale {
[year: number]: {
[examinationYear: number]: number;
};
}

const salaryPayscale: PayScale = payscale;

export function calculateSalary(
currentYear: number,
examinationYear: number,
degree: string,
): number {
const degreeValue = degree === "bachelor" ? 1 : 0;
const adjustedYear = examinationYear + degreeValue;
return salaryPayscale[currentYear][adjustedYear];
}

export function calculatePension(salary: number): number {
return Math.round(salary * 0.07);
}

export function maxExperience(thisYear: number): number {
const years = Object.keys(salaryPayscale[thisYear]).map(Number);
return Math.min(...years);
}
40 changes: 40 additions & 0 deletions src/salaryAndBenefits/utils/salaryData.ts
anemne marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const payscale = {
2024: {
2024: 600000,
2023: 635833,
2022: 681829,
2021: 734982,
2020: 789982,
2019: 838539,
2018: 879553,
2017: 916886,
2016: 949000,
2015: 977333,
2014: 1005324,
2013: 1031405,
2012: 1064738,
2011: 1091489,
2010: 1113742,
2009: 1138742,
2008: 1166667,
2007: 1192460,
2006: 1210126,
2005: 1233560,
2004: 1264767,
2003: 1289780,
2002: 1299680,
2001: 1295953,
2000: 1305501,
1999: 1328501,
1998: 1349349,
1997: 1365121,
1996: 1384832,
1995: 1399711,
1994: 1422069,
1993: 1429358,
1992: 1452891,
1991: 1458021,
1990: 1467321,
1989: 1484721,
},
};
1 change: 1 addition & 0 deletions studio/lib/payloads/salaryAndBenefits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export interface SalaryAndBenefitsPage {
page: string;
slug: Slug;
benefits: Benefit[];
showSalaryCalculator: boolean;
}
Loading