Skip to content

Commit

Permalink
VF-94 lag nytt utlegg skjema (#256)
Browse files Browse the repository at this point in the history
* Install datepicker and react-hook-form

* Add mobile friendly disbursement form

* Add validation and user feedback

VF-94

* Fix sonarcloud issues

* Fix typing errors from yarn build
  • Loading branch information
ErlendMariusOmmundsen authored Jan 24, 2024
1 parent 3c3a60a commit a9356bd
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 3 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@
"@typescript-eslint/parser": "^5.2.0",
"autoprefixer": "^10.4.10",
"daisyui": "^2.27.0",
"dayjs": "^1.11.10",
"eslint-plugin": "latest",
"eslint-plugin-import": "latest",
"eslint-plugin-jsx-a11y": "latest",
"eslint-plugin-react": "latest",
"eslint-plugin-react-hooks": "latest",
"norwegian-utils": "^0.4.1",
"postcss": "^8.4.16",
"postcss-import": "^15.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.47.0",
"react-router-dom": "^6.0.0",
"react-scripts": "5.0.1",
"react-tailwindcss-datepicker": "^1.6.6",
"tailwindcss": "^3.1.8",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"
Expand Down
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import "./App.css";
import MainPage from "pages/public";
import ControlPanel from "pages/controlpanel";
import Profil from "pages/public/Profil/components/Profil";
import Utlegg from "pages/public/User/Utlegg";
import appRoutes from "./pages/public/routes";
import controlPanelRoutes from "./pages/controlpanel/routes";

Expand Down Expand Up @@ -47,7 +48,7 @@ const routes = createBrowserRouter([
},
],
},
],
{ path: "/utlegg", element: <Utlegg /> }],
},
]);

Expand Down
2 changes: 1 addition & 1 deletion src/components/Header/UserAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const UserAvatar = () => {
<a href="#0">Mine søknader</a>
</li>
<li>
<a href="#0">Mine utlegg</a>
<Link reloadDocument to="/utlegg">Mine utlegg</Link>
</li>
<li className="text-red-500">
<a href="#0">Logg ut</a>
Expand Down
245 changes: 245 additions & 0 deletions src/pages/public/User/Utlegg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable react/jsx-props-no-spreading */
import {
faCaretLeft, faCaretRight, faCheckToSlot,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import Datepicker, { DateValueType } from "react-tailwindcss-datepicker";
import validateAccountNumber from "norwegian-utils/validateAccountNumber";

type Inputs = {
amount: number
description: string
receipt: File
bankAccountNumber: string
};

const Utlegg = (): JSX.Element => {
const [currentStep, setCurrentStep] = useState(1);
const [dateValue, setDateValue] = useState<DateValueType>({
startDate: null,
endDate: null,
});
const [file, setFile] = useState(new File([""], "filename"));
const [currentErrorMessage, setCurrentErrorMessage] = useState("");

const handleDateValueChange = (value: DateValueType) => {
setDateValue(value);
};

const validateDate = (date: Date) => {
return date > new Date("01/01/2024");
};

const { register, handleSubmit, formState } = useForm<Inputs>({
mode: "onChange",
reValidateMode: "onChange",
defaultValues: {
amount: 0,
description: "",
receipt: new File([""], "filename"),
bankAccountNumber: "0",
},
});

// dirtyFields må brukes for at den skal oppdateres (minneoptimering react-hook-form)
console.assert(formState.dirtyFields || true);

Check warning on line 48 in src/pages/public/User/Utlegg.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

const onSubmit: SubmitHandler<Inputs> = (data) => {
const formData = { ...data, date: dateValue?.startDate };

// send to backend here..
console.log("Form submitted with the following data:", formData);

Check warning on line 54 in src/pages/public/User/Utlegg.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
};

const isCurrentInputValid = () => {
const date = new Date(dateValue?.startDate?.toString() || "");
switch (currentStep) {
case 1:
if (formState.errors.amount || !formState.dirtyFields.amount) {
setCurrentErrorMessage("Beløp må være et positivt tall.");
return false;
}
break;

case 2:
if (formState.errors.description || !formState.dirtyFields.description) {
setCurrentErrorMessage("Beskrivelse må være lengre enn to bokstaver.");
return false;
}
break;

case 3:
if (!validateDate(date)) {
setCurrentErrorMessage("Utleggsdato må være en gyldig dato etter 1. januar 2024 (DD-MM-YYYY).");
return false;
}
break;

case 4:
if (formState.errors.receipt || file.name === "filename" || !file.type.includes("image")) {
setCurrentErrorMessage("Kvittering må være et opplastet bilde.");
return false;
}
break;

case 5:
if (formState.errors.bankAccountNumber) {
setCurrentErrorMessage("Kontonummer må være et gyldig norsk kontonummer.");
return false;
}
break;

default:
break;
}
setCurrentErrorMessage("");
return true;
};
const handleNext = () => {
if (isCurrentInputValid()) {
setCurrentStep(currentStep + 1);
}
};

const handlePrevious = () => {
if (currentStep !== 1) {
setCurrentStep(currentStep - 1);
}
};

const Amount = (
<input
{...register("amount", { required: true, min: 1 })}
type="number"
id="amount"
name="amount"
placeholder="Beløp"
className="input input-bordered input-secondary text-sm sm:text-base w-full"
/>
);
const Description = (
<textarea
{...register("description", { required: true, minLength: 2 })}
id="description"
name="description"
placeholder="Beskrivelse"
className="textarea textarea-secondary textarea-lg text-sm sm:text-base w-full max-w-xs max-h-20"
/>
);
const DateElement = (
<Datepicker
value={dateValue}
displayFormat="DD-MM-YYYY"
asSingle
useRange={false}
onChange={handleDateValueChange}
inputClassName="border border-vektor-darblue rounded-md w-full px-3 py-2 placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-vektor-darblue focus:border-vektor-darblue text-sm sm:text-base"
/>
);
const ReceiptUpload = (
<input
{...register("receipt", { required: true })}
type="file"
id="receipt"
name="receipt"
accept="image/*"
multiple={false}
onChange={(e) => setFile(e.target.files![0])}
form="disbursementForm"
className="file-input file-input-bordered file-input-md input-secondary w-full"
/>
);
const BankAccountNumber = (
<input
{...register("bankAccountNumber", { required: true, validate: validateAccountNumber })}
type="text"
id="bankAccountNumber"
name="bankAccountNumber"
className="input input-bordered input-md input-secondary w-full"
placeholder="Kontonummer"
/>
);

const inputWrapper = (label: string, input: JSX.Element) => (
<div className="form-control w-full max-w-xs">
<label className="label">
<span className="label-text font-bold">{label}</span>
</label>
{input}
</div>
);

const Next = (
<button type="button" onClick={handleNext} className="btn btn-md hidden" disabled={currentStep === 6}>
<span>Neste</span>
<FontAwesomeIcon className="text-white pl-4" icon={faCaretRight} />
</button>
);

const Confirm = (
<button type="submit" className="btn btn-success btn-md m-6" hidden={currentStep !== 6}>
<span>Bekreft</span>
<FontAwesomeIcon className="text-white pl-4" icon={faCheckToSlot} />
</button>
);

const Back = (
<button type="button" onClick={handlePrevious} className="btn btn-md" disabled={currentStep === 1}>
<FontAwesomeIcon className="text-white pr-4" icon={faCaretLeft} />
<span>Forrige</span>
</button>
);

const stepToComponent = (step: number) => {
switch (step) {
case 1:
return inputWrapper("Utleggsbeløp:", Amount);
case 2:
return inputWrapper("Utleggsbeskrivelse:", Description);
case 3:
return inputWrapper("Dato for utlegg:", DateElement);
case 4:
return inputWrapper("Bilde av kvittering:", ReceiptUpload);
case 5:
return inputWrapper("Utbetalingskonto:", BankAccountNumber);
case 6:
return (
<div className="flex flex-col items-center justify-center">
<h1 className="text-vektor-darblue text-2xl font-bold">Bekrefte utlegg?</h1>
{Confirm}
</div>
);
default:
break;
}
return <div />;
};

return (
<div className="leading-relaxed font-sans max-w-md mx-auto md:max-w-2xl flex flex-col justify-center items-center">
<h1 className="font-sans max-w-2xl mt-10 text-vektor-darblue text-4xl text-center font-bold mx-3">
Utlegg
</h1>
<form id="disbursementForm" name="disbursementForm" className="bg-vektor-blue w-5/6 rounded-xl my-16 flex flex-col pt-8" onSubmit={handleSubmit(onSubmit)}>
<div className="rounded-t-xl w-11/12 h-36 m-6 px-3 justify-center self-center">
<img
className="w-1/4 -mt-6 float-right hidden md:block"
src="images/team/OkonomiTor.png"
alt=""
/>
{stepToComponent(currentStep)}
<p className="text-red-600 text-sm my-4 m-1 w-full max-w-xs">{currentErrorMessage}</p>
</div>
<div className="flex justify-around space-x-6 py-5 bg-slate-800 rounded-b-xl">
{Back}
{Next}
</div>
</form>
</div>
);
};

export default Utlegg;
2 changes: 2 additions & 0 deletions src/pages/public/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Kontakt from "./Kontakt";
import OmOss from "./OmOss";
import ServerOverview from "./StagingServerOverview";
import teamRoutes from "./Team/TeamPages/routes";
import Utlegg from "./User/Utlegg";

// The route with the corresponding component to render in the route
const routes: AppRoute[] = [
Expand All @@ -17,6 +18,7 @@ const routes: AppRoute[] = [
{ path: "/kontakt", element: <Kontakt />, name: "Kontakt" },
{ path: "/om-oss", element: <OmOss />, name: "Om oss" },
{ path: "/team", name: "Team", children: teamRoutes },
{ path: "/utlegg", element: <Utlegg />, name: "Utlegg" },
];

export default routes;
2 changes: 1 addition & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./node_modules/react-tailwindcss-datepicker/dist/index.esm.js",],
darkMode: "class", // or false or 'media'
theme: {
screens: {
Expand Down
Loading

0 comments on commit a9356bd

Please sign in to comment.