├── .gitignore
├── README.md
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── src
├── components
│ └── link.tsx
├── features
│ ├── confirm
│ │ ├── components
│ │ │ ├── context.ts
│ │ │ └── provider.tsx
│ │ ├── hooks
│ │ │ └── useContext.ts
│ │ └── types
│ │ │ └── options.ts
│ ├── employee
│ │ ├── additional-info
│ │ │ ├── components
│ │ │ │ └── references.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useQueries.ts
│ │ │ │ └── useStore.ts
│ │ │ ├── page.tsx
│ │ │ ├── types
│ │ │ │ └── schema.ts
│ │ │ └── utils
│ │ │ │ └── api.ts
│ │ ├── history
│ │ │ ├── components
│ │ │ │ ├── educational-institutions.tsx
│ │ │ │ └── previous-employers.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useQueries.ts
│ │ │ │ └── useStore.ts
│ │ │ ├── page.tsx
│ │ │ ├── types
│ │ │ │ ├── apiTypes.ts
│ │ │ │ └── schema.ts
│ │ │ └── utils
│ │ │ │ └── api.ts
│ │ ├── personal-info
│ │ │ ├── hooks
│ │ │ │ ├── useQueries.ts
│ │ │ │ └── useStore.ts
│ │ │ ├── page.tsx
│ │ │ ├── types
│ │ │ │ └── schema.ts
│ │ │ └── utils
│ │ │ │ └── api.ts
│ │ ├── review
│ │ │ ├── hooks
│ │ │ │ ├── useQueries.ts
│ │ │ │ └── useStore.ts
│ │ │ ├── page.tsx
│ │ │ ├── types
│ │ │ │ └── schema.ts
│ │ │ └── utils
│ │ │ │ └── api.ts
│ │ ├── skills
│ │ │ ├── components
│ │ │ │ ├── proficiency-levels.tsx
│ │ │ │ ├── skill-set.tsx
│ │ │ │ └── skill-sets.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useQueries.ts
│ │ │ │ └── useStore.ts
│ │ │ ├── page.tsx
│ │ │ ├── types
│ │ │ │ ├── apiTypes.ts
│ │ │ │ └── schema.ts
│ │ │ └── utils
│ │ │ │ └── api.ts
│ │ └── wrapper
│ │ │ ├── components
│ │ │ ├── stepper.tsx
│ │ │ └── summary-dialog.tsx
│ │ │ ├── hooks
│ │ │ ├── useMutations.ts
│ │ │ └── useStore.ts
│ │ │ ├── page.tsx
│ │ │ ├── types
│ │ │ └── schema.ts
│ │ │ └── utils
│ │ │ └── api.ts
│ ├── form
│ │ ├── components
│ │ │ ├── controllers
│ │ │ │ ├── autocomplete.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── date-picker.tsx
│ │ │ │ ├── menu.tsx
│ │ │ │ ├── slider.tsx
│ │ │ │ └── text-field.tsx
│ │ │ ├── error-message.tsx
│ │ │ ├── form-error-summary.tsx
│ │ │ └── form.tsx
│ │ ├── hooks
│ │ │ ├── useFormContext.ts
│ │ │ └── useFormLogger.ts
│ │ └── types
│ │ │ └── formContext.ts
│ └── layout
│ │ ├── components
│ │ ├── dashboard-layout.tsx
│ │ └── theme-toggle.tsx
│ │ ├── hooks
│ │ └── useStore.ts
│ │ └── utils
│ │ └── constants.ts
├── main.tsx
├── routes.tsx
├── utils
│ ├── calculatePastDate.ts
│ ├── createStore.ts
│ ├── dictionary.ts
│ ├── formatErrors.ts
│ ├── getErrorMessage.ts
│ ├── humanizeFieldName.ts
│ ├── regex.ts
│ ├── showSnack.tsx
│ ├── theme.ts
│ ├── wait.ts
│ └── zodConfig.ts
└── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ["./tsconfig.node.json", "./tsconfig.app.json"],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | });
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from "eslint-plugin-react";
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: "18.3" } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs["jsx-runtime"].rules,
48 | },
49 | });
50 | ```
51 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.14.0",
14 | "@emotion/styled": "^11.14.0",
15 | "@hookform/error-message": "^2.0.1",
16 | "@hookform/resolvers": "^3.9.1",
17 | "@mui/icons-material": "^6.3.0",
18 | "@mui/lab": "^6.0.0-beta.21",
19 | "@mui/material": "^6.3.0",
20 | "@mui/x-date-pickers": "^7.23.3",
21 | "@tanstack/react-query": "^5.62.10",
22 | "date-fns": "^4.1.0",
23 | "immer": "^10.1.1",
24 | "material-ui-popup-state": "^5.3.3",
25 | "notistack": "^3.0.1",
26 | "react": "^18.3.1",
27 | "react-dom": "^18.3.1",
28 | "react-hook-form": "^7.54.2",
29 | "react-number-format": "^5.4.3",
30 | "react-router": "^7.1.1",
31 | "validator": "^13.12.0",
32 | "zod": "^3.24.1",
33 | "zustand": "^5.0.2"
34 | },
35 | "devDependencies": {
36 | "@eslint/js": "^9.17.0",
37 | "@types/node": "^22.10.2",
38 | "@types/react": "^18.3.12",
39 | "@types/react-dom": "^18.3.1",
40 | "@types/validator": "^13.12.2",
41 | "@vitejs/plugin-react-swc": "^3.7.2",
42 | "eslint": "^9.17.0",
43 | "eslint-plugin-react-hooks": "^5.1.0",
44 | "eslint-plugin-react-refresh": "^0.4.16",
45 | "globals": "^15.14.0",
46 | "path": "^0.12.7",
47 | "typescript": "~5.7.2",
48 | "typescript-eslint": "^8.18.2",
49 | "vite": "^6.0.5"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/link.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Link as ReactRouterLink,
3 | LinkProps as ReactRouterLinkProps,
4 | } from "react-router";
5 |
6 | import { forwardRef, ReactElement, Ref } from "react";
7 |
8 | type LinkProps = Omit & {
9 | href: ReactRouterLinkProps["to"];
10 | };
11 |
12 | const Link = forwardRef(
13 | ({ href, ...linkProps }: LinkProps, ref: Ref) => {
14 | return ;
15 | }
16 | ) as (props: LinkProps & { ref?: Ref }) => ReactElement;
17 |
18 | export { Link };
19 |
--------------------------------------------------------------------------------
/src/features/confirm/components/context.ts:
--------------------------------------------------------------------------------
1 | import { Options } from "@/features/confirm/types/options";
2 | import { createContext } from "react";
3 |
4 | type ContextState = (optionsArg: Options) => Promise;
5 |
6 | export const Context = createContext(undefined);
7 |
--------------------------------------------------------------------------------
/src/features/confirm/components/provider.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, ReactNode, useCallback, useState } from "react";
2 |
3 | import { DialogContentText, ModalProps } from "@mui/material";
4 |
5 | import { LoadingButton } from "@mui/lab";
6 | import { Dialog as MuiDialog } from "@mui/material";
7 | import Button from "@mui/material/Button";
8 | import DialogActions from "@mui/material/DialogActions";
9 | import DialogContent from "@mui/material/DialogContent";
10 | import DialogTitle from "@mui/material/DialogTitle";
11 | import { Options } from "@/features/confirm/types/options";
12 | import { Context } from "@/features/confirm/components/context";
13 | import { d } from "@/utils/dictionary";
14 |
15 | type DialogProps = {
16 | open: boolean;
17 | options: Options;
18 | onCancel: () => void;
19 | onConfirm: () => Promise;
20 | onClose: ModalProps["onClose"];
21 | loading?: boolean;
22 | };
23 |
24 | const Dialog = ({
25 | open,
26 | options,
27 | onCancel,
28 | onConfirm,
29 | onClose,
30 | loading,
31 | }: DialogProps) => {
32 | const { title, content, confirmationText, cancellationText } = options;
33 |
34 | const handleConfirm = async () => {
35 | await onConfirm();
36 | };
37 |
38 | return (
39 |
46 | {title && {title}}
47 | {content && (
48 |
49 | {content}
50 |
51 | )}
52 |
53 |
54 |
57 |
63 | {confirmationText}
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | type ProviderProps = {
71 | children: ReactNode;
72 | defaultOptions?: Options;
73 | };
74 |
75 | const Provider = ({ children, defaultOptions = {} }: ProviderProps) => {
76 | const [options, setOptions] = useState({});
77 | const [isLoading, setIsLoading] = useState(false);
78 |
79 | const [resolveReject, setResolveReject] = useState<
80 | ((value?: unknown) => void)[]
81 | >([]);
82 |
83 | const [resolve, reject] = resolveReject;
84 |
85 | const confirm = useCallback((optionsArg = {}) => {
86 | return new Promise((resolveArg, rejectArg) => {
87 | setOptions(optionsArg);
88 | setResolveReject([resolveArg, rejectArg]);
89 | });
90 | }, []);
91 |
92 | const handleClose = useCallback(() => {
93 | setIsLoading(false);
94 | setResolveReject([]);
95 | if (options.onClose) {
96 | options.onClose();
97 | }
98 | }, [options]);
99 |
100 | const handleCancel = useCallback(() => {
101 | if (reject) {
102 | reject();
103 | handleClose();
104 | }
105 | }, [reject, handleClose]);
106 |
107 | const handleConfirm = useCallback(async () => {
108 | setIsLoading(true);
109 | if (resolve) {
110 | resolve();
111 | }
112 | if (options.onConfirm) {
113 | await options.onConfirm();
114 | handleClose();
115 | }
116 | }, [handleClose, options, resolve]);
117 |
118 | return (
119 |
120 | {children}
121 |
136 |
137 | );
138 | };
139 |
140 | export { Provider as ConfirmProvider };
141 |
--------------------------------------------------------------------------------
/src/features/confirm/hooks/useContext.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "@/features/confirm/components/context";
2 | import { useContext as useReactContext } from "react";
3 |
4 | const useContext = () => {
5 | const context = useReactContext(Context);
6 |
7 | if (context === undefined) {
8 | throw new Error("useContext must be used with a context(confirm)");
9 | }
10 |
11 | return context;
12 | };
13 |
14 | export { useContext as useConfirm };
15 |
--------------------------------------------------------------------------------
/src/features/confirm/types/options.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | type Options = {
4 | title?: ReactNode;
5 | content?: ReactNode;
6 | confirmationText?: ReactNode;
7 | cancellationText?: ReactNode;
8 | onClose?: () => void;
9 | onConfirm?: () => Promise | void;
10 | };
11 |
12 | export { type Options };
13 |
--------------------------------------------------------------------------------
/src/features/employee/additional-info/components/references.tsx:
--------------------------------------------------------------------------------
1 | import { useRelationships } from "@/features/employee/additional-info/hooks/useQueries";
2 |
3 | import { Schema } from "@/features/employee/additional-info/types/schema";
4 | import { Autocomplete } from "@/features/form/components/controllers/autocomplete";
5 | import { TextField } from "@/features/form/components/controllers/text-field";
6 | import { ErrorMessage } from "@/features/form/components/error-message";
7 | import { useFormContext } from "@/features/form/hooks/useFormContext";
8 | import { d } from "@/utils/dictionary";
9 | import AddCircleRoundedIcon from "@mui/icons-material/AddCircleRounded";
10 | import RemoveCircleOutlineRoundedIcon from "@mui/icons-material/RemoveCircleOutlineRounded";
11 | import { Chip, IconButton, Typography } from "@mui/material";
12 | import Grid from "@mui/material/Grid2";
13 | import { useFieldArray } from "react-hook-form";
14 | import { Fragment } from "react/jsx-runtime";
15 |
16 | const References = () => {
17 | const relationshipsQuery = useRelationships();
18 |
19 | const { control, readOnly } = useFormContext();
20 |
21 | const { fields, append, remove } = useFieldArray({
22 | control,
23 | name: "references",
24 | });
25 |
26 | const handleAddClick = () => {
27 | append({
28 | name: "",
29 | relationship: "",
30 | contactInformation: "",
31 | });
32 | };
33 |
34 | const handleRemoveClick = (index: number) => {
35 | remove(index);
36 | };
37 |
38 | return (
39 | <>
40 |
45 | {d.references}:
46 | {!readOnly && (
47 |
48 |
49 |
50 | )}
51 |
52 | {fields.map((field, index) => (
53 |
54 |
58 |
63 |
64 | {!readOnly && (
65 | handleRemoveClick(index)}
68 | >
69 |
70 |
71 | )}
72 |
73 |
74 |
75 | name={`references.${index}.name`}
76 | label={d.name}
77 | />
78 |
79 |
80 |
81 | options={relationshipsQuery.data}
82 | name={`references.${index}.relationship`}
83 | textFieldProps={{ label: d.relationship }}
84 | />
85 |
86 |
87 |
88 | name={`references.${index}.contactInformation`}
89 | label={d.contactInformation}
90 | />
91 |
92 |
93 | ))}
94 |
95 | name="references" />
96 |
97 | >
98 | );
99 | };
100 |
101 | export { References };
102 |
--------------------------------------------------------------------------------
/src/features/employee/additional-info/hooks/useQueries.ts:
--------------------------------------------------------------------------------
1 | import { getRelationships } from "@/features/employee/additional-info/utils/api";
2 | import { useQuery } from "@tanstack/react-query";
3 |
4 | const useRelationships = () => {
5 | return useQuery({
6 | queryKey: ["relationships"],
7 | queryFn: getRelationships,
8 | });
9 | };
10 |
11 | export { useRelationships };
12 |
--------------------------------------------------------------------------------
/src/features/employee/additional-info/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultValues,
3 | Schema,
4 | } from "@/features/employee/additional-info/types/schema";
5 | import { createStore } from "@/utils/createStore";
6 |
7 | type State = {
8 | formData: Schema;
9 | };
10 |
11 | type Actions = {
12 | updateFormData: (data: State["formData"]) => void;
13 | };
14 |
15 | type Store = State & Actions;
16 |
17 | const useStore = createStore(
18 | (set) => ({
19 | formData: defaultValues,
20 | updateFormData: (data) =>
21 | set((state) => {
22 | state.formData = data;
23 | }),
24 | }),
25 | {
26 | name: "employee-additional-info-store",
27 | }
28 | );
29 |
30 | export { useStore, useStore as useEmployeeAdditionalInfoStore };
31 |
--------------------------------------------------------------------------------
/src/features/employee/additional-info/page.tsx:
--------------------------------------------------------------------------------
1 | import { References } from "@/features/employee/additional-info/components/references";
2 |
3 | import ArrowForwardIosRoundedIcon from "@mui/icons-material/ArrowForwardIosRounded";
4 | import { useStore } from "@/features/employee/additional-info/hooks/useStore";
5 | import {
6 | defaultValues,
7 | schema,
8 | Schema,
9 | } from "@/features/employee/additional-info/types/schema";
10 | import { DatePicker } from "@/features/form/components/controllers/date-picker";
11 | import { Slider } from "@/features/form/components/controllers/slider";
12 | import { TextField } from "@/features/form/components/controllers/text-field";
13 | import { Form } from "@/features/form/components/form";
14 | import { d } from "@/utils/dictionary";
15 | import Grid from "@mui/material/Grid2";
16 | import { startOfToday } from "date-fns";
17 | import { SubmitHandler } from "react-hook-form";
18 | import { useNavigate } from "react-router";
19 |
20 | const Page = () => {
21 | return (
22 | <>
23 |
24 | name="portfolioLink" label={d.portfolioLink} />
25 |
26 |
27 |
28 |
29 | name="availabilityToStart"
30 | label={d.availabilityToStart}
31 | minDate={startOfToday()}
32 | />
33 |
34 |
35 |
36 |
37 | name="salaryExpectations"
38 | label={d.salaryExpectations}
39 | min={30000}
40 | max={200000}
41 | unit="$"
42 | step={10000}
43 | valueLabelDisplay="on"
44 | />
45 |
46 |
47 |
48 | >
49 | );
50 | };
51 |
52 | type ProviderProps = {
53 | readOnly?: boolean;
54 | };
55 | const Provider = ({ readOnly }: ProviderProps) => {
56 | const navigate = useNavigate();
57 |
58 | const { formData, updateFormData } = useStore();
59 |
60 | const handleSubmit: SubmitHandler = (data) => {
61 | updateFormData(data);
62 | navigate("/employee/review");
63 | };
64 |
65 | return (
66 | },
70 | }}
71 | schema={schema}
72 | values={formData}
73 | defaultValues={defaultValues}
74 | onSubmit={handleSubmit}
75 | readOnly={readOnly}
76 | title={d.additionalInfo}
77 | >
78 |
79 |
80 | );
81 | };
82 |
83 | export { Provider as EmployeeAdditionalInfo };
84 |
--------------------------------------------------------------------------------
/src/features/employee/additional-info/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import validator from "validator";
3 | import { regex } from "@/utils/regex";
4 | import { startOfToday } from "date-fns";
5 |
6 | const referencesSchema = z.object({
7 | name: z.string().min(1),
8 | relationship: z.string().min(1),
9 | contactInformation: z
10 | .string()
11 | .min(1)
12 | .refine((val) => validator.isEmail(val) || validator.isMobilePhone(val)),
13 | });
14 |
15 | const schema = z.object({
16 | portfolioLink: z.union([z.string().regex(regex.link), z.literal("")]),
17 | availabilityToStart: z.coerce.date().refine((date) => date >= startOfToday()),
18 | salaryExpectations: z.number().min(30000).max(200000),
19 | references: z.array(referencesSchema).min(1),
20 | });
21 |
22 | type Schema = z.infer;
23 |
24 | const defaultValues: Schema = {
25 | availabilityToStart: new Date(),
26 | references: [],
27 | salaryExpectations: 100000,
28 | portfolioLink: "",
29 | };
30 |
31 | export {
32 | schema,
33 | schema as employeeAdditionalInfoSchema,
34 | type Schema,
35 | defaultValues,
36 | };
37 |
--------------------------------------------------------------------------------
/src/features/employee/additional-info/utils/api.ts:
--------------------------------------------------------------------------------
1 | import { AutocompleteOption } from "@/features/form/components/controllers/autocomplete";
2 | import { wait } from "@/utils/wait";
3 |
4 | const getRelationships = async (): Promise => {
5 | await wait();
6 | return [
7 | { label: "Former Manager", value: "1" },
8 | { label: "Current Manager", value: "2" },
9 | { label: "Direct Supervisor", value: "3" },
10 | { label: "Team Lead", value: "4" },
11 | { label: "Project Manager", value: "5" },
12 | { label: "Colleague", value: "6" },
13 | { label: "Senior Colleague", value: "7" },
14 | { label: "Department Head", value: "8" },
15 | { label: "Professional Mentor", value: "9" },
16 | { label: "Client", value: "10" },
17 | ];
18 | };
19 |
20 | export { getRelationships };
21 |
--------------------------------------------------------------------------------
/src/features/employee/history/components/educational-institutions.tsx:
--------------------------------------------------------------------------------
1 | import { DatePicker } from "@/features/form/components/controllers/date-picker";
2 | import { ErrorMessage } from "@/features/form/components/error-message";
3 | import { TextField } from "@/features/form/components/controllers/text-field";
4 |
5 | import { Schema } from "@/features/employee/history/types/schema";
6 | import { calculatePastDate } from "@/utils/calculatePastDate";
7 | import { d } from "@/utils/dictionary";
8 | import AddCircleRoundedIcon from "@mui/icons-material/AddCircleRounded";
9 | import RemoveCircleOutlineRoundedIcon from "@mui/icons-material/RemoveCircleOutlineRounded";
10 | import { Chip, IconButton, Typography } from "@mui/material";
11 | import Grid from "@mui/material/Grid2";
12 | import { useFieldArray } from "react-hook-form";
13 | import { useFormContext } from "@/features/form/hooks/useFormContext";
14 |
15 | const EducationalInstitutions = () => {
16 | const { control, readOnly } = useFormContext();
17 |
18 | const { fields, append, remove } = useFieldArray({
19 | control,
20 | name: "educationalInstitutions",
21 | });
22 |
23 | const handleAddClick = () => {
24 | append({
25 | degree: "",
26 | fieldOfStudy: "",
27 | graduationYear: calculatePastDate(1),
28 | institutionName: "",
29 | });
30 | };
31 |
32 | const handleRemoveClick = (index: number) => {
33 | remove(index);
34 | };
35 |
36 | return (
37 | <>
38 |
43 |
44 | {d.educationalInstitutions}:
45 |
46 | {!readOnly && (
47 |
48 |
49 |
50 | )}
51 |
52 | {fields.map((field, index) => (
53 |
54 |
58 |
63 | {!readOnly && (
64 | handleRemoveClick(index)}
67 | >
68 |
69 |
70 | )}
71 |
72 |
73 |
74 | name={`educationalInstitutions.${index}.degree`}
75 | label={d.degree}
76 | />
77 |
78 |
79 |
80 | name={`educationalInstitutions.${index}.fieldOfStudy`}
81 | label={d.fieldOfStudy}
82 | />
83 |
84 |
85 |
86 | name={`educationalInstitutions.${index}.institutionName`}
87 | label={d.institutionName}
88 | />
89 |
90 |
91 |
92 | name={`educationalInstitutions.${index}.graduationYear`}
93 | maxDate={new Date()}
94 | label={d.graduationYear}
95 | minDate={calculatePastDate(100)}
96 | />
97 |
98 |
99 | ))}
100 |
101 | name="educationalInstitutions" />
102 |
103 | >
104 | );
105 | };
106 |
107 | export { EducationalInstitutions };
108 |
--------------------------------------------------------------------------------
/src/features/employee/history/components/previous-employers.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorMessage } from "@/features/form/components/error-message";
2 | import { TextField } from "@/features/form/components/controllers/text-field";
3 |
4 | import { Schema } from "@/features/employee/history/types/schema";
5 | import { d } from "@/utils/dictionary";
6 | import AddCircleRoundedIcon from "@mui/icons-material/AddCircleRounded";
7 | import RemoveCircleOutlineRoundedIcon from "@mui/icons-material/RemoveCircleOutlineRounded";
8 | import { Chip, IconButton, Typography } from "@mui/material";
9 | import Grid from "@mui/material/Grid2";
10 | import { useFieldArray } from "react-hook-form";
11 | import { Fragment } from "react/jsx-runtime";
12 | import { useFormContext } from "@/features/form/hooks/useFormContext";
13 |
14 | const PreviousEmployers = () => {
15 | const { control, readOnly } = useFormContext();
16 |
17 | const { fields, append, remove } = useFieldArray({
18 | control,
19 | name: "previousEmployers",
20 | });
21 |
22 | const handleAddClick = () => {
23 | append({ jobTitle: "", employerName: "", responsibilities: "" });
24 | };
25 |
26 | const handleRemoveClick = (index: number) => {
27 | remove(index);
28 | };
29 |
30 | return (
31 | <>
32 |
37 | {d.previousEmployers}:
38 | {!readOnly && (
39 |
40 |
41 |
42 | )}
43 |
44 | {fields.map((field, index) => (
45 |
46 |
50 |
55 | {!readOnly && (
56 | handleRemoveClick(index)}
59 | >
60 |
61 |
62 | )}
63 |
64 |
65 |
66 | name={`previousEmployers.${index}.employerName`}
67 | label={d.employerName}
68 | />
69 |
70 |
71 |
72 | name={`previousEmployers.${index}.jobTitle`}
73 | label={d.jobTitle}
74 | />
75 |
76 |
77 |
78 | name={`previousEmployers.${index}.responsibilities`}
79 | label={d.responsibilities}
80 | multiline
81 | maxRows={4}
82 | />
83 |
84 |
85 | ))}
86 |
87 | name="previousEmployers" />
88 |
89 | >
90 | );
91 | };
92 |
93 | export { PreviousEmployers };
94 |
--------------------------------------------------------------------------------
/src/features/employee/history/hooks/useQueries.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getDegrees,
3 | getEmploymentStatuses,
4 | getReasonsForLeaving,
5 | } from "@/features/employee/history/utils/api";
6 | import { useQuery } from "@tanstack/react-query";
7 |
8 | const useEmploymentStatuses = () => {
9 | return useQuery({
10 | queryKey: ["employmentStatuses"],
11 | queryFn: getEmploymentStatuses,
12 | });
13 | };
14 |
15 | const useReasonsForLeaving = () => {
16 | return useQuery({
17 | queryKey: ["reasonsForLeaving"],
18 | queryFn: getReasonsForLeaving,
19 | });
20 | };
21 |
22 | const useDegrees = () => {
23 | return useQuery({
24 | queryKey: ["degrees"],
25 | queryFn: getDegrees,
26 | });
27 | };
28 |
29 | export { useEmploymentStatuses, useReasonsForLeaving, useDegrees };
30 |
--------------------------------------------------------------------------------
/src/features/employee/history/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultValues,
3 | Schema,
4 | } from "@/features/employee/history/types/schema";
5 | import { createStore } from "@/utils/createStore";
6 |
7 | type State = {
8 | formData: Schema;
9 | };
10 |
11 | type Actions = {
12 | updateFormData: (data: State["formData"]) => void;
13 | };
14 |
15 | type Store = State & Actions;
16 |
17 | const useStore = createStore(
18 | (set) => ({
19 | formData: defaultValues,
20 | updateFormData: (data) =>
21 | set((state) => {
22 | state.formData = data;
23 | }),
24 | }),
25 | {
26 | name: "employee-history-store",
27 | }
28 | );
29 |
30 | export { useStore, useStore as useEmployeeHistoryStore };
31 |
--------------------------------------------------------------------------------
/src/features/employee/history/page.tsx:
--------------------------------------------------------------------------------
1 | import { Autocomplete } from "@/features/form/components/controllers/autocomplete";
2 | import ArrowForwardIosRoundedIcon from "@mui/icons-material/ArrowForwardIosRounded";
3 |
4 | import { Form } from "@/features/form/components/form";
5 | import { TextField } from "@/features/form/components/controllers/text-field";
6 | import { EducationalInstitutions } from "@/features/employee/history/components/educational-institutions";
7 | import { PreviousEmployers } from "@/features/employee/history/components/previous-employers";
8 | import {
9 | useDegrees,
10 | useEmploymentStatuses,
11 | useReasonsForLeaving,
12 | } from "@/features/employee/history/hooks/useQueries";
13 | import { useStore } from "@/features/employee/history/hooks/useStore";
14 | import {
15 | defaultValues,
16 | ReasonForLeavingEnum,
17 | schema,
18 | Schema,
19 | } from "@/features/employee/history/types/schema";
20 | import { d } from "@/utils/dictionary";
21 | import Grid from "@mui/material/Grid2";
22 | import { SubmitHandler, useWatch } from "react-hook-form";
23 | import { useNavigate } from "react-router";
24 | import { useFormContext } from "@/features/form/hooks/useFormContext";
25 |
26 | const Page = () => {
27 | const employmentStatusesQuery = useEmploymentStatuses();
28 | const reasonsForLeavingQuery = useReasonsForLeaving();
29 | const degreesQuery = useDegrees();
30 |
31 | const { control } = useFormContext();
32 |
33 | const reasonsForLeavingPreviousJobs = useWatch({
34 | control,
35 | name: "reasonsForLeavingPreviousJobs",
36 | });
37 |
38 | return (
39 | <>
40 |
41 |
42 | name="currentEmploymentStatus"
43 | options={employmentStatusesQuery.data}
44 | textFieldProps={{ label: d.currentEmploymentStatus }}
45 | />
46 |
47 |
48 |
49 | name="highestDegreeObtained"
50 | options={degreesQuery.data}
51 | textFieldProps={{ label: d.highestDegreeObtained }}
52 | />
53 |
54 |
55 |
56 |
57 | name="reasonsForLeavingPreviousJobs"
58 | options={reasonsForLeavingQuery.data}
59 | textFieldProps={{ label: d.reasonsForLeavingPreviousJobs }}
60 | multiple={true}
61 | />
62 |
63 |
64 |
65 | {reasonsForLeavingPreviousJobs.includes(
66 | ReasonForLeavingEnum.enum.OTHER
67 | ) && (
68 |
69 | name="otherReasonsForLeaving"
70 | label={d.otherReasonsForLeaving}
71 | multiline
72 | maxRows={4}
73 | />
74 | )}
75 |
76 |
77 |
78 |
79 | >
80 | );
81 | };
82 |
83 | type ProviderProps = {
84 | readOnly?: boolean;
85 | };
86 | const Provider = ({ readOnly }: ProviderProps) => {
87 | const navigate = useNavigate();
88 |
89 | const { formData, updateFormData } = useStore();
90 |
91 | const handleSubmit: SubmitHandler = (data) => {
92 | updateFormData(data);
93 | navigate("/employee/skills");
94 | };
95 |
96 | return (
97 | },
101 | }}
102 | schema={schema}
103 | values={formData}
104 | defaultValues={defaultValues}
105 | onSubmit={handleSubmit}
106 | readOnly={readOnly}
107 | title={d.history}
108 | >
109 |
110 |
111 | );
112 | };
113 |
114 | export { Provider as EmployeeHistory };
115 |
--------------------------------------------------------------------------------
/src/features/employee/history/types/apiTypes.ts:
--------------------------------------------------------------------------------
1 | enum ApiReasonForLeavingEnum {
2 | CAREER_ADVANCEMENT = "1",
3 | RELOCATION = "2",
4 | PERSONAL_REASONS = "3",
5 | OTHER = "4",
6 | }
7 |
8 | export { ApiReasonForLeavingEnum };
9 |
--------------------------------------------------------------------------------
/src/features/employee/history/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { ApiReasonForLeavingEnum } from "@/features/employee/history/types/apiTypes";
2 | import { calculatePastDate } from "@/utils/calculatePastDate";
3 | import { z } from "zod";
4 |
5 | const ReasonForLeavingEnum = z.nativeEnum(ApiReasonForLeavingEnum);
6 |
7 | const previousEmployerSchema = z.object({
8 | employerName: z.string().min(1),
9 | jobTitle: z.string().min(1),
10 | responsibilities: z.string().max(1000),
11 | });
12 |
13 | const educationalInstitutionsSchema = z.object({
14 | institutionName: z.string().min(1),
15 | degree: z.string().min(1),
16 | fieldOfStudy: z.string().min(1),
17 | graduationYear: z.coerce.date().max(new Date()).min(calculatePastDate(100)),
18 | });
19 |
20 | const schema = z
21 | .object({
22 | currentEmploymentStatus: z.string().min(1),
23 | reasonsForLeavingPreviousJobs: z.array(ReasonForLeavingEnum).min(1),
24 | otherReasonsForLeaving: z.string().optional(),
25 | highestDegreeObtained: z.string().min(1),
26 | previousEmployers: z.array(previousEmployerSchema).min(1),
27 | educationalInstitutions: z.array(educationalInstitutionsSchema).min(1),
28 | })
29 | .superRefine((data, ctx) => {
30 | const hasOtherReasonsForLeavingPreviousJobs =
31 | data.reasonsForLeavingPreviousJobs.includes(
32 | ReasonForLeavingEnum.enum.OTHER
33 | );
34 |
35 | if (hasOtherReasonsForLeavingPreviousJobs && !data.otherReasonsForLeaving) {
36 | ctx.addIssue({
37 | code: z.ZodIssueCode.custom,
38 | message: "Required",
39 | path: ["otherReasonsForLeaving"],
40 | });
41 | }
42 | });
43 |
44 | type Schema = z.infer;
45 |
46 | const defaultValues: Schema = {
47 | currentEmploymentStatus: "",
48 | educationalInstitutions: [],
49 | highestDegreeObtained: "",
50 | previousEmployers: [],
51 | reasonsForLeavingPreviousJobs: [],
52 | otherReasonsForLeaving: "",
53 | };
54 |
55 | export {
56 | defaultValues,
57 | ReasonForLeavingEnum,
58 | schema,
59 | schema as employeeHistorySchema,
60 | type Schema,
61 | };
62 |
--------------------------------------------------------------------------------
/src/features/employee/history/utils/api.ts:
--------------------------------------------------------------------------------
1 | import { AutocompleteOption } from "@/features/form/components/controllers/autocomplete";
2 | import { wait } from "@/utils/wait";
3 |
4 | const getEmploymentStatuses = async (): Promise => {
5 | await wait();
6 | return [
7 | {
8 | label: "Employed Full Time",
9 | value: "1",
10 | },
11 | {
12 | label: "Employed Half Time",
13 | value: "2",
14 | },
15 | {
16 | label: "Unemployed",
17 | value: "3",
18 | },
19 | {
20 | label: "Student",
21 | value: "4",
22 | },
23 | {
24 | label: "Other",
25 | value: "5",
26 | },
27 | ];
28 | };
29 |
30 | const getReasonsForLeaving = async (): Promise => {
31 | await wait();
32 | return [
33 | {
34 | label: "Career Advancement",
35 | value: "1",
36 | },
37 | {
38 | label: "Relocation",
39 | value: "2",
40 | },
41 | {
42 | label: "Personal Reasons",
43 | value: "3",
44 | },
45 | {
46 | label: "Other",
47 | value: "4",
48 | },
49 | ];
50 | };
51 |
52 | const getDegrees = async (): Promise => {
53 | await wait();
54 | return [
55 | {
56 | label: "High School Diploma",
57 | value: "1",
58 | },
59 | {
60 | label: "Associate Degree",
61 | value: "2",
62 | },
63 | {
64 | label: "Bachelor Degree",
65 | value: "3",
66 | },
67 | {
68 | label: "Master Degree",
69 | value: "4",
70 | },
71 | {
72 | label: "Doctorate",
73 | value: "5",
74 | },
75 | ];
76 | };
77 |
78 | export { getDegrees, getEmploymentStatuses, getReasonsForLeaving };
79 |
--------------------------------------------------------------------------------
/src/features/employee/personal-info/hooks/useQueries.ts:
--------------------------------------------------------------------------------
1 | import { Schema } from "@/features/employee/personal-info/types/schema";
2 | import {
3 | getCities,
4 | getStates,
5 | } from "@/features/employee/personal-info/utils/api";
6 | import { useFormContext } from "@/features/form/hooks/useFormContext";
7 | import { useQuery } from "@tanstack/react-query";
8 | import { useWatch } from "react-hook-form";
9 |
10 | const useStates = () => {
11 | return useQuery({
12 | queryKey: ["states"],
13 | queryFn: getStates,
14 | });
15 | };
16 |
17 | const useCities = () => {
18 | const { control } = useFormContext();
19 | const state = useWatch({ control, name: "state" });
20 |
21 | return useQuery({
22 | queryKey: ["cities", { state }],
23 | queryFn: () => getCities(state),
24 | });
25 | };
26 |
27 | export { useStates, useCities };
28 |
--------------------------------------------------------------------------------
/src/features/employee/personal-info/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultValues,
3 | Schema,
4 | } from "@/features/employee/personal-info/types/schema";
5 | import { createStore } from "@/utils/createStore";
6 |
7 | type State = {
8 | formData: Schema;
9 | };
10 |
11 | type Actions = {
12 | updateFormData: (data: State["formData"]) => void;
13 | };
14 |
15 | type Store = State & Actions;
16 |
17 | const useStore = createStore(
18 | (set) => ({
19 | formData: defaultValues,
20 | updateFormData: (data) =>
21 | set((state) => {
22 | state.formData = data;
23 | }),
24 | }),
25 | {
26 | name: "employee-personal-info-store",
27 | }
28 | );
29 |
30 | export { useStore, useStore as useEmployeePersonalInfoStore };
31 |
--------------------------------------------------------------------------------
/src/features/employee/personal-info/page.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from "@/features/form/components/form";
2 |
3 | import ArrowForwardIosRoundedIcon from "@mui/icons-material/ArrowForwardIosRounded";
4 | import { DatePicker } from "@/features/form/components/controllers/date-picker";
5 | import { TextField } from "@/features/form/components/controllers/text-field";
6 | import {
7 | useCities,
8 | useStates,
9 | } from "@/features/employee/personal-info/hooks/useQueries";
10 | import { useStore } from "@/features/employee/personal-info/hooks/useStore";
11 | import {
12 | defaultValues,
13 | schema,
14 | Schema,
15 | } from "@/features/employee/personal-info/types/schema";
16 | import { calculatePastDate } from "@/utils/calculatePastDate";
17 | import { d } from "@/utils/dictionary";
18 | import Grid from "@mui/material/Grid2";
19 | import { SubmitHandler, useWatch } from "react-hook-form";
20 | import { useNavigate } from "react-router";
21 | import {
22 | Autocomplete,
23 | AutocompleteOption,
24 | } from "@/features/form/components/controllers/autocomplete";
25 | import { useFormContext } from "@/features/form/hooks/useFormContext";
26 |
27 | const Page = () => {
28 | const statesQuery = useStates();
29 | const citiesQuery = useCities();
30 |
31 | const { control, setValue } = useFormContext();
32 | const state = useWatch({ control, name: "state" });
33 |
34 | const handleOptionSelect = (option: AutocompleteOption | null) => {
35 | if (!option) {
36 | setValue("city", "");
37 | }
38 | };
39 |
40 | return (
41 | <>
42 |
43 | name="firstName" label={d.firstName} />
44 |
45 |
46 | name="lastName" label={d.lastName} />
47 |
48 |
49 |
50 | name="dateOfBirth"
51 | label={d.dateOfBirth}
52 | maxDate={calculatePastDate(18)}
53 | minDate={calculatePastDate(100)}
54 | />
55 |
56 |
57 | name="email" label={d.email} />
58 |
59 |
60 |
61 | name="phoneNumber"
62 | label={d.phoneNumber}
63 | format="phoneNumber"
64 | />
65 |
66 |
67 |
68 | name="socialSecurityNumber"
69 | label={d.socialSecurityNumber}
70 | format="socialSecurity"
71 | />
72 |
73 |
74 |
75 | name="state"
76 | options={statesQuery.data}
77 | loading={statesQuery.isLoading}
78 | textFieldProps={{ label: d.state }}
79 | onOptionSelect={handleOptionSelect}
80 | />
81 |
82 |
83 | {!!state && (
84 |
85 | name="city"
86 | options={citiesQuery.data}
87 | loading={citiesQuery.isLoading}
88 | textFieldProps={{ label: d.city }}
89 | />
90 | )}
91 |
92 |
93 |
94 | name="streetAddress"
95 | label={d.streetAddress}
96 | multiline
97 | maxRows={4}
98 | />
99 |
100 | >
101 | );
102 | };
103 |
104 | type ProviderProps = { readOnly?: boolean };
105 | const Provider = ({ readOnly }: ProviderProps) => {
106 | const navigate = useNavigate();
107 |
108 | const { formData, updateFormData } = useStore();
109 |
110 | const handleSubmit: SubmitHandler = (data) => {
111 | updateFormData(data);
112 | navigate("/employee/history");
113 | };
114 |
115 | return (
116 | },
120 | }}
121 | schema={schema}
122 | values={formData}
123 | defaultValues={defaultValues}
124 | onSubmit={handleSubmit}
125 | readOnly={readOnly}
126 | title={d.personalInfo}
127 | >
128 |
129 |
130 | );
131 | };
132 |
133 | export { Provider as EmployeePersonalInfo };
134 |
--------------------------------------------------------------------------------
/src/features/employee/personal-info/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { calculatePastDate } from "@/utils/calculatePastDate";
2 | import { regex } from "@/utils/regex";
3 | import validator from "validator";
4 | import { z } from "zod";
5 |
6 | const schema = z.object({
7 | firstName: z.string().min(1).max(50),
8 | lastName: z.string().min(1).max(50),
9 | email: z.string().email(),
10 | phoneNumber: z
11 | .string()
12 | .min(1)
13 | .refine((val) => validator.isMobilePhone(val, "en-US")),
14 | dateOfBirth: z.coerce
15 | .date()
16 | .max(calculatePastDate(18))
17 | .min(calculatePastDate(100)),
18 | state: z.string().min(1),
19 | city: z.string().min(1),
20 | streetAddress: z.string().min(1),
21 | socialSecurityNumber: z.union([
22 | z.string().regex(regex.socialSecurityNumber),
23 | z.literal(""),
24 | ]),
25 | });
26 |
27 | type Schema = z.infer;
28 |
29 | const defaultValues: Schema = {
30 | city: "",
31 | dateOfBirth: calculatePastDate(18),
32 | email: "",
33 | firstName: "",
34 | lastName: "",
35 | phoneNumber: "",
36 | state: "",
37 | streetAddress: "",
38 | socialSecurityNumber: "",
39 | };
40 |
41 | export {
42 | schema,
43 | schema as employeePersonalInfoSchema,
44 | type Schema,
45 | defaultValues,
46 | };
47 |
--------------------------------------------------------------------------------
/src/features/employee/personal-info/utils/api.ts:
--------------------------------------------------------------------------------
1 | import { AutocompleteOption } from "@/features/form/components/controllers/autocomplete";
2 | import { wait } from "@/utils/wait";
3 |
4 | const states = [
5 | {
6 | label: "California",
7 | value: "CA",
8 | cities: [
9 | { label: "Los Angeles", value: "los_angeles" },
10 | { label: "San Francisco", value: "san_francisco" },
11 | { label: "San Diego", value: "san_diego" },
12 | { label: "San Jose", value: "san_jose" },
13 | ],
14 | },
15 | {
16 | label: "Texas",
17 | value: "TX",
18 | cities: [
19 | { label: "Houston", value: "houston" },
20 | { label: "Dallas", value: "dallas" },
21 | { label: "Austin", value: "austin" },
22 | { label: "San Antonio", value: "san_antonio" },
23 | ],
24 | },
25 | {
26 | label: "New York",
27 | value: "NY",
28 | cities: [
29 | { label: "New York City", value: "new_york_city" },
30 | { label: "Buffalo", value: "buffalo" },
31 | { label: "Rochester", value: "rochester" },
32 | { label: "Albany", value: "albany" },
33 | ],
34 | },
35 | {
36 | label: "Florida",
37 | value: "FL",
38 | cities: [
39 | { label: "Miami", value: "miami" },
40 | { label: "Orlando", value: "orlando" },
41 | { label: "Tampa", value: "tampa" },
42 | { label: "Jacksonville", value: "jacksonville" },
43 | ],
44 | },
45 | {
46 | label: "Illinois",
47 | value: "IL",
48 | cities: [
49 | { label: "Chicago", value: "chicago" },
50 | { label: "Aurora", value: "aurora" },
51 | { label: "Naperville", value: "naperville" },
52 | { label: "Springfield", value: "springfield" },
53 | ],
54 | },
55 | ];
56 |
57 | const getStates = async (): Promise => {
58 | await wait();
59 | return states.map((item) => ({
60 | label: item.label,
61 | value: item.value,
62 | }));
63 | };
64 |
65 | const getCities = async (state: string): Promise => {
66 | await wait();
67 |
68 | return (
69 | states
70 | .find((item) => item.value === state)
71 | ?.cities.map((item) => ({
72 | label: item.label,
73 | value: item.value,
74 | })) ?? []
75 | );
76 | };
77 |
78 | export { getStates, getCities };
79 |
--------------------------------------------------------------------------------
/src/features/employee/review/hooks/useQueries.ts:
--------------------------------------------------------------------------------
1 | import { getTermsAndConditions } from "@/features/employee/review/utils/api";
2 | import { useQuery } from "@tanstack/react-query";
3 |
4 | const useTermsAndConditions = () => {
5 | return useQuery({
6 | queryKey: ["termsAndConditions"],
7 | queryFn: getTermsAndConditions,
8 | });
9 | };
10 |
11 | export { useTermsAndConditions };
12 |
--------------------------------------------------------------------------------
/src/features/employee/review/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import { defaultValues, Schema } from "@/features/employee/review/types/schema";
2 | import { createStore } from "@/utils/createStore";
3 |
4 | type State = {
5 | formData: Schema;
6 | isSubmitted: boolean;
7 | };
8 |
9 | type Actions = {
10 | updateFormData: (data: State["formData"]) => void;
11 | updateIsSubmitted: (is: State["isSubmitted"]) => void;
12 | };
13 |
14 | type Store = State & Actions;
15 |
16 | const useStore = createStore(
17 | (set) => ({
18 | formData: defaultValues,
19 | updateFormData: (data) =>
20 | set((state) => {
21 | state.formData = data;
22 | }),
23 | isSubmitted: false,
24 | updateIsSubmitted: (is) =>
25 | set((state) => {
26 | state.isSubmitted = is;
27 | }),
28 | }),
29 |
30 | {
31 | name: "employee-review-store",
32 | }
33 | );
34 |
35 | export { useStore, useStore as useEmployeeReviewStore };
36 |
--------------------------------------------------------------------------------
/src/features/employee/review/page.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from "@/features/form/components/form";
2 |
3 | import SendOutlinedIcon from "@mui/icons-material/SendOutlined";
4 |
5 | import { useTermsAndConditions } from "@/features/employee/review/hooks/useQueries";
6 | import { useStore } from "@/features/employee/review/hooks/useStore";
7 | import {
8 | defaultValues,
9 | schema,
10 | Schema,
11 | } from "@/features/employee/review/types/schema";
12 | import { useEmployeeWrapperStore } from "@/features/employee/wrapper/hooks/useStore";
13 | import { Checkbox } from "@/features/form/components/controllers/checkbox";
14 | import { d } from "@/utils/dictionary";
15 | import { Box, Stack, Typography } from "@mui/material";
16 | import Grid from "@mui/material/Grid2";
17 | import { SubmitHandler } from "react-hook-form";
18 |
19 | const Page = () => {
20 | const termsAndConditionsQuery = useTermsAndConditions();
21 |
22 | return (
23 | <>
24 |
25 |
32 | {termsAndConditionsQuery.data?.map((item) => (
33 |
34 | {item.title}
35 | {item.content}
36 |
37 | ))}
38 |
39 |
40 |
41 |
42 | name="termsAndConditionsAccepted"
43 | label={`${d.iAcceptTermsAndConditions}.`}
44 | />
45 |
46 | >
47 | );
48 | };
49 |
50 | type ProviderProps = {
51 | readOnly?: boolean;
52 | };
53 | const Provider = ({ readOnly }: ProviderProps) => {
54 | const { updateSummaryDialogOpen } = useEmployeeWrapperStore();
55 | const { formData, updateFormData, updateIsSubmitted } = useStore();
56 |
57 | const handleSubmit: SubmitHandler = (data) => {
58 | updateFormData(data);
59 | updateSummaryDialogOpen(true);
60 | updateIsSubmitted(true);
61 | };
62 |
63 | const handleError = () => {
64 | updateIsSubmitted(true);
65 | };
66 |
67 | return (
68 | },
72 | }}
73 | values={formData}
74 | defaultValues={defaultValues}
75 | onSubmit={handleSubmit}
76 | onError={handleError}
77 | readOnly={readOnly}
78 | title={d.review}
79 | >
80 |
81 |
82 | );
83 | };
84 |
85 | export { Provider as EmployeeReview };
86 |
--------------------------------------------------------------------------------
/src/features/employee/review/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { d } from "@/utils/dictionary";
2 | import { z } from "zod";
3 |
4 | const schema = z.object({
5 | termsAndConditionsAccepted: z.boolean().refine((val) => val === true, {
6 | message: `${d.youMustAcceptTermsAndConditions}.`,
7 | }),
8 | });
9 |
10 | type Schema = z.infer;
11 |
12 | const defaultValues: Schema = {
13 | termsAndConditionsAccepted: false,
14 | };
15 |
16 | export { defaultValues, schema as employeeReviewSchema, schema, type Schema };
17 |
--------------------------------------------------------------------------------
/src/features/employee/review/utils/api.ts:
--------------------------------------------------------------------------------
1 | const getTermsAndConditions = async () => {
2 | return [
3 | {
4 | title: "Acceptance of Terms",
5 | content:
6 | "By accessing or using app, you agree to these Terms and Conditions. If you do not agree, please do not use the application.",
7 | },
8 | {
9 | title: "Use of the Application",
10 | content:
11 | "You are granted a limited, non-exclusive, non-transferable license to use app for personal and educational purposes. You may not use the application for any illegal or unauthorized purpose.",
12 | },
13 | {
14 | title: "User Responsibilities",
15 | content:
16 | "You are responsible for maintaining the confidentiality of your account information and for all activities that occur under your account. You agree to notify us immediately of any unauthorized use of your account.",
17 | },
18 | {
19 | title: "Data Handling",
20 | content:
21 | "The application allows you to create, read, update, and delete employee records. We do not store any personal data beyond what is necessary for the functionality of the application. Please ensure that you have the right to use any data you input into the application.",
22 | },
23 | {
24 | title: "Limitation of Liability",
25 | content:
26 | "In no event shall app be liable for any direct, indirect, incidental, or consequential damages arising out of the use of or inability to use the application.",
27 | },
28 | {
29 | title: "Changes to Terms",
30 | content:
31 | "We reserve the right to modify these Terms and Conditions at any time. Any changes will be effective immediately upon posting on this page. Your continued use of the application after any changes signifies your acceptance of the new terms.",
32 | },
33 | {
34 | title: "Contact Information",
35 | content:
36 | "If you have any questions about these Terms and Conditions, please contact us at foo@bar.com.",
37 | },
38 | ];
39 | };
40 |
41 | export { getTermsAndConditions };
42 |
--------------------------------------------------------------------------------
/src/features/employee/skills/components/proficiency-levels.tsx:
--------------------------------------------------------------------------------
1 | import { Autocomplete } from "@/features/form/components/controllers/autocomplete";
2 | import { useProficiencyLevels } from "@/features/employee/skills/hooks/useQueries";
3 |
4 | import { Schema } from "@/features/employee/skills/types/schema";
5 | import { d } from "@/utils/dictionary";
6 | import { Typography } from "@mui/material";
7 | import Grid from "@mui/material/Grid2";
8 |
9 | const ProficiencyLevels = () => {
10 | const proficiencyLevelsQuery = useProficiencyLevels();
11 |
12 | return (
13 | <>
14 |
15 | {d.proficiencyLevels}:
16 |
17 |
18 |
19 | name="proficiencyLevels.projectManagement"
20 | options={proficiencyLevelsQuery.data}
21 | textFieldProps={{ label: d.projectManagement }}
22 | loading={proficiencyLevelsQuery.isLoading}
23 | />
24 |
25 |
26 |
27 | name="proficiencyLevels.communication"
28 | options={proficiencyLevelsQuery.data}
29 | textFieldProps={{ label: d.communication }}
30 | loading={proficiencyLevelsQuery.isLoading}
31 | />
32 |
33 |
34 |
35 | name="proficiencyLevels.technicalSkills"
36 | options={proficiencyLevelsQuery.data}
37 | textFieldProps={{ label: d.technicalSkills }}
38 | loading={proficiencyLevelsQuery.isLoading}
39 | />
40 |
41 |
42 |
43 | name="proficiencyLevels.leadership"
44 | options={proficiencyLevelsQuery.data}
45 | textFieldProps={{ label: d.leadership }}
46 | loading={proficiencyLevelsQuery.isLoading}
47 | />
48 |
49 |
50 |
51 | name="proficiencyLevels.problemSolving"
52 | options={proficiencyLevelsQuery.data}
53 | textFieldProps={{ label: d.problemSolving }}
54 | loading={proficiencyLevelsQuery.isLoading}
55 | />
56 |
57 | >
58 | );
59 | };
60 |
61 | export { ProficiencyLevels };
62 |
--------------------------------------------------------------------------------
/src/features/employee/skills/components/skill-set.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useSkillCategories,
3 | useSkills,
4 | useSkillSubcategories,
5 | } from "@/features/employee/skills/hooks/useQueries";
6 | import { Autocomplete } from "@/features/form/components/controllers/autocomplete";
7 | import { TextField } from "@/features/form/components/controllers/text-field";
8 |
9 | import { Schema } from "@/features/employee/skills/types/schema";
10 | import { useFormContext } from "@/features/form/hooks/useFormContext";
11 | import { d } from "@/utils/dictionary";
12 | import RemoveCircleOutlineRoundedIcon from "@mui/icons-material/RemoveCircleOutlineRounded";
13 | import { Chip, IconButton } from "@mui/material";
14 | import Grid from "@mui/material/Grid2";
15 | import { UseFieldArrayRemove, useWatch } from "react-hook-form";
16 |
17 | type SkillSetProps = {
18 | fieldIndex: number;
19 | fieldRemove: UseFieldArrayRemove;
20 | };
21 | const SkillSet = ({ fieldIndex, fieldRemove }: SkillSetProps) => {
22 | const { control, setValue, readOnly } = useFormContext();
23 |
24 | const category = useWatch({
25 | control,
26 | name: `skillSets.${fieldIndex}.category`,
27 | });
28 | const subcategory = useWatch({
29 | control,
30 | name: `skillSets.${fieldIndex}.subcategory`,
31 | });
32 |
33 | const skillCategoriesQuery = useSkillCategories();
34 | const skillSubcategoriesQuery = useSkillSubcategories(category);
35 | const skillsQuery = useSkills(subcategory);
36 |
37 | const handleRemoveClick = () => {
38 | fieldRemove(fieldIndex);
39 | };
40 |
41 | const handleCategoryChange = () => {
42 | setValue(`skillSets.${fieldIndex}.subcategory`, "");
43 | setValue(`skillSets.${fieldIndex}.skills`, []);
44 | };
45 |
46 | const handleSubcategoryChange = () => {
47 | setValue(`skillSets.${fieldIndex}.skills`, []);
48 | };
49 |
50 | return (
51 | <>
52 |
53 |
58 | {!readOnly && (
59 |
60 |
61 |
62 | )}
63 |
64 |
65 |
66 | name={`skillSets.${fieldIndex}.category`}
67 | options={skillCategoriesQuery.data}
68 | textFieldProps={{ label: d.category }}
69 | onOptionSelect={handleCategoryChange}
70 | />
71 |
72 |
73 | {!!category && (
74 |
75 | name={`skillSets.${fieldIndex}.subcategory`}
76 | options={skillSubcategoriesQuery.data}
77 | textFieldProps={{ label: d.subCategory }}
78 | onOptionSelect={handleSubcategoryChange}
79 | />
80 | )}
81 |
82 |
83 | {!!subcategory && (
84 |
85 | name={`skillSets.${fieldIndex}.skills`}
86 | options={skillsQuery.data}
87 | textFieldProps={{ label: d.skills }}
88 | multiple
89 | />
90 | )}
91 |
92 |
93 |
94 | name={`skillSets.${fieldIndex}.yearsOfExperience`}
95 | label={d.yearsOfExperience}
96 | type="number"
97 | />
98 |
99 |
100 |
101 | name={`skillSets.${fieldIndex}.description`}
102 | label={d.description}
103 | multiline
104 | maxRows={4}
105 | />
106 |
107 | >
108 | );
109 | };
110 |
111 | export { SkillSet };
112 |
--------------------------------------------------------------------------------
/src/features/employee/skills/components/skill-sets.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorMessage } from "@/features/form/components/error-message";
2 | import { SkillSet } from "@/features/employee/skills/components/skill-set";
3 |
4 | import { Schema } from "@/features/employee/skills/types/schema";
5 | import { d } from "@/utils/dictionary";
6 | import AddCircleRoundedIcon from "@mui/icons-material/AddCircleRounded";
7 | import { IconButton, Typography } from "@mui/material";
8 | import Grid from "@mui/material/Grid2";
9 | import { useFieldArray } from "react-hook-form";
10 | import { useFormContext } from "@/features/form/hooks/useFormContext";
11 |
12 | const SkillSets = () => {
13 | const { control, readOnly } = useFormContext();
14 |
15 | const { fields, append, remove } = useFieldArray({
16 | control,
17 | name: "skillSets",
18 | });
19 |
20 | const handleAddClick = () => {
21 | append({
22 | category: "",
23 | subcategory: "",
24 | skills: [],
25 | description: "",
26 | yearsOfExperience: 2,
27 | });
28 | };
29 |
30 | return (
31 | <>
32 |
37 | {d.skillSets}:
38 | {!readOnly && (
39 |
40 |
41 |
42 | )}
43 |
44 | {fields.map((field, index) => (
45 |
46 | ))}
47 |
48 | name="skillSets" />
49 |
50 | >
51 | );
52 | };
53 |
54 | export { SkillSets };
55 |
--------------------------------------------------------------------------------
/src/features/employee/skills/hooks/useQueries.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getCoreCompetencies,
3 | getLanguages,
4 | getProficiencyLevels,
5 | getSkillCategories,
6 | getSkills,
7 | getSkillSubcategories,
8 | } from "@/features/employee/skills/utils/api";
9 | import { useQuery } from "@tanstack/react-query";
10 |
11 | const useCoreCompetencies = () => {
12 | return useQuery({
13 | queryKey: ["coreCompetencies"],
14 | queryFn: getCoreCompetencies,
15 | });
16 | };
17 |
18 | const useProficiencyLevels = () => {
19 | return useQuery({
20 | queryKey: ["proficiencyLevels"],
21 | queryFn: getProficiencyLevels,
22 | });
23 | };
24 |
25 | const useSkillCategories = () => {
26 | return useQuery({
27 | queryKey: ["skillCategories"],
28 | queryFn: getSkillCategories,
29 | });
30 | };
31 |
32 | const useSkillSubcategories = (category: string) => {
33 | return useQuery({
34 | queryKey: ["skillSubcategories", { category }],
35 | queryFn: () => getSkillSubcategories(category),
36 | enabled: !!category,
37 | });
38 | };
39 |
40 | const useSkills = (subcategory: string) => {
41 | return useQuery({
42 | queryKey: ["skills", { subcategory }],
43 | queryFn: () => getSkills(subcategory),
44 | enabled: !!subcategory,
45 | });
46 | };
47 |
48 | const useLanguages = () => {
49 | return useQuery({
50 | queryKey: ["languages"],
51 | queryFn: getLanguages,
52 | });
53 | };
54 |
55 | export {
56 | useCoreCompetencies,
57 | useProficiencyLevels,
58 | useSkillCategories,
59 | useSkills,
60 | useSkillSubcategories,
61 | useLanguages,
62 | };
63 |
--------------------------------------------------------------------------------
/src/features/employee/skills/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import { defaultValues, Schema } from "@/features/employee/skills/types/schema";
2 | import { createStore } from "@/utils/createStore";
3 |
4 | type State = {
5 | formData: Schema;
6 | };
7 |
8 | type Actions = {
9 | updateFormData: (data: State["formData"]) => void;
10 | };
11 |
12 | type Store = State & Actions;
13 |
14 | const useStore = createStore(
15 | (set) => ({
16 | formData: defaultValues,
17 | updateFormData: (data) =>
18 | set((state) => {
19 | state.formData = data;
20 | }),
21 | }),
22 | {
23 | name: "employee-skills-store",
24 | }
25 | );
26 |
27 | export { useStore, useStore as useEmployeeSkillsStore };
28 |
--------------------------------------------------------------------------------
/src/features/employee/skills/page.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from "@/features/form/components/form";
2 |
3 | import ArrowForwardIosRoundedIcon from "@mui/icons-material/ArrowForwardIosRounded";
4 | import { Autocomplete } from "@/features/form/components/controllers/autocomplete";
5 | import { TextField } from "@/features/form/components/controllers/text-field";
6 | import { ProficiencyLevels } from "@/features/employee/skills/components/proficiency-levels";
7 | import { SkillSets } from "@/features/employee/skills/components/skill-sets";
8 | import {
9 | useCoreCompetencies,
10 | useLanguages,
11 | } from "@/features/employee/skills/hooks/useQueries";
12 | import { useStore } from "@/features/employee/skills/hooks/useStore";
13 | import {
14 | CoreCompetencyEnum,
15 | defaultValues,
16 | schema,
17 | Schema,
18 | } from "@/features/employee/skills/types/schema";
19 | import { d } from "@/utils/dictionary";
20 | import Grid from "@mui/material/Grid2";
21 | import { SubmitHandler, useWatch } from "react-hook-form";
22 | import { useNavigate } from "react-router";
23 | import { useFormContext } from "@/features/form/hooks/useFormContext";
24 |
25 | const Page = () => {
26 | const coreCompetenciesQuery = useCoreCompetencies();
27 | const languagesQuery = useLanguages();
28 |
29 | const { control } = useFormContext();
30 |
31 | const coreCompetencies = useWatch({
32 | control,
33 | name: "coreCompetencies",
34 | });
35 |
36 | return (
37 | <>
38 |
39 |
40 | name="coreCompetencies"
41 | options={coreCompetenciesQuery.data}
42 | textFieldProps={{ label: d.coreCompetencies }}
43 | multiple
44 | />
45 |
46 |
47 |
48 | {coreCompetencies.includes(CoreCompetencyEnum.enum.OTHER) && (
49 |
50 | name="otherCoreCompetencies"
51 | label={d.otherCoreCompetencies}
52 | multiline
53 | maxRows={4}
54 | />
55 | )}
56 |
57 |
58 |
59 |
60 | name="languagesSpoken"
61 | options={languagesQuery.data}
62 | textFieldProps={{ label: d.languagesSpoken }}
63 | multiple
64 | />
65 |
66 |
67 |
68 |
69 | >
70 | );
71 | };
72 |
73 | type ProviderProps = {
74 | readOnly?: boolean;
75 | };
76 | const Provider = ({ readOnly }: ProviderProps) => {
77 | const navigate = useNavigate();
78 |
79 | const { formData, updateFormData } = useStore();
80 |
81 | const handleSubmit: SubmitHandler = (data) => {
82 | updateFormData(data);
83 | navigate("/employee/additional-info");
84 | };
85 |
86 | return (
87 | },
91 | }}
92 | schema={schema}
93 | values={formData}
94 | defaultValues={defaultValues}
95 | onSubmit={handleSubmit}
96 | readOnly={readOnly}
97 | title={d.skills}
98 | >
99 |
100 |
101 | );
102 | };
103 |
104 | export { Provider as EmployeeSkills };
105 |
--------------------------------------------------------------------------------
/src/features/employee/skills/types/apiTypes.ts:
--------------------------------------------------------------------------------
1 | enum ApiCoreCompetencyEnum {
2 | PROJECT_MANAGEMENT = "1",
3 | COMMUNICATION = "2",
4 | TECHNICAL_SKILLS = "3",
5 | LEADERSHIP = "4",
6 | PROBLEM_SOLVING = "5",
7 | OTHER = "6",
8 | }
9 |
10 | export { ApiCoreCompetencyEnum };
11 |
--------------------------------------------------------------------------------
/src/features/employee/skills/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { ApiCoreCompetencyEnum } from "@/features/employee/skills/types/apiTypes";
2 | import { z } from "zod";
3 |
4 | const CoreCompetencyEnum = z.nativeEnum(ApiCoreCompetencyEnum);
5 |
6 | const skillSetSchema = z.object({
7 | category: z.string().min(1),
8 | subcategory: z.string().min(1),
9 | skills: z.array(z.string()).min(1),
10 | yearsOfExperience: z.coerce.number().min(0).max(50),
11 | description: z.string(),
12 | });
13 | const schema = z
14 | .object({
15 | coreCompetencies: z.array(CoreCompetencyEnum).min(1),
16 | otherCoreCompetencies: z.string().optional(),
17 | proficiencyLevels: z.object({
18 | projectManagement: z.string().min(1),
19 | communication: z.string().min(1),
20 | technicalSkills: z.string().min(1),
21 | leadership: z.string().min(1),
22 | problemSolving: z.string().min(1),
23 | }),
24 | skillSets: z.array(skillSetSchema).min(1).max(5),
25 | languagesSpoken: z.array(z.string().min(1)).min(1),
26 | })
27 | .superRefine((data, ctx) => {
28 | const hasOtherCoreCompetencies = data.coreCompetencies.includes(
29 | CoreCompetencyEnum.enum.OTHER
30 | );
31 |
32 | if (hasOtherCoreCompetencies && !data.otherCoreCompetencies) {
33 | ctx.addIssue({
34 | code: z.ZodIssueCode.custom,
35 | message: "Required",
36 | path: ["otherCoreCompetencies"],
37 | });
38 | }
39 | });
40 |
41 | type Schema = z.infer;
42 |
43 | const defaultValues: Schema = {
44 | coreCompetencies: [],
45 | languagesSpoken: [],
46 | proficiencyLevels: {
47 | communication: "",
48 | leadership: "",
49 | problemSolving: "",
50 | projectManagement: "",
51 | technicalSkills: "",
52 | },
53 | skillSets: [],
54 | otherCoreCompetencies: "",
55 | };
56 |
57 | export {
58 | defaultValues,
59 | schema,
60 | schema as employeeSkillsSchema,
61 | type Schema,
62 | CoreCompetencyEnum,
63 | };
64 |
--------------------------------------------------------------------------------
/src/features/employee/skills/utils/api.ts:
--------------------------------------------------------------------------------
1 | import { AutocompleteOption } from "@/features/form/components/controllers/autocomplete";
2 | import { wait } from "@/utils/wait";
3 | const mockSkillCategories = [
4 | {
5 | label: "Technical Skills",
6 | value: "1",
7 | children: [
8 | {
9 | label: "Programming Languages",
10 | value: "1.1",
11 | children: [
12 | { label: "Python", value: "1.1.1" },
13 | { label: "Java", value: "1.1.2" },
14 | { label: "C++", value: "1.1.3" },
15 | { label: "JavaScript", value: "1.1.4" },
16 | ],
17 | },
18 | {
19 | label: "Software Proficiency",
20 | value: "1.2",
21 | children: [
22 | { label: "Microsoft Excel", value: "1.2.1" },
23 | { label: "Adobe Creative Suite", value: "1.2.2" },
24 | { label: "Salesforce", value: "1.2.3" },
25 | ],
26 | },
27 | {
28 | label: "Data Analysis",
29 | value: "1.3",
30 | children: [
31 | { label: "SQL", value: "1.3.1" },
32 | { label: "R", value: "1.3.2" },
33 | { label: "Tableau", value: "1.3.3" },
34 | { label: "Google Analytics", value: "1.3.4" },
35 | ],
36 | },
37 | {
38 | label: "Web Development",
39 | value: "1.4",
40 | children: [
41 | { label: "HTML", value: "1.4.1" },
42 | { label: "CSS", value: "1.4.2" },
43 | { label: "React", value: "1.4.3" },
44 | { label: "Node.js", value: "1.4.4" },
45 | ],
46 | },
47 | {
48 | label: "Network Administration",
49 | value: "1.5",
50 | children: [
51 | { label: "Cisco networking", value: "1.5.1" },
52 | { label: "Firewall management", value: "1.5.2" },
53 | { label: "VPN configuration", value: "1.5.3" },
54 | ],
55 | },
56 | ],
57 | },
58 | {
59 | label: "Soft Skills",
60 | value: "2",
61 | children: [
62 | {
63 | label: "Communication",
64 | value: "2.1",
65 | children: [
66 | { label: "Verbal communication", value: "2.1.1" },
67 | { label: "Written communication", value: "2.1.2" },
68 | { label: "Active listening", value: "2.1.3" },
69 | ],
70 | },
71 | {
72 | label: "Teamwork",
73 | value: "2.2",
74 | children: [
75 | { label: "Collaboration", value: "2.2.1" },
76 | { label: "Conflict resolution", value: "2.2.2" },
77 | { label: "Adaptability", value: "2.2.3" },
78 | ],
79 | },
80 | {
81 | label: "Problem Solving",
82 | value: "2.3",
83 | children: [
84 | { label: "Critical thinking", value: "2.3.1" },
85 | { label: "Analytical skills", value: "2.3.2" },
86 | { label: "Creativity", value: "2.3.3" },
87 | ],
88 | },
89 | {
90 | label: "Time Management",
91 | value: "2.4",
92 | children: [
93 | { label: "Prioritization", value: "2.4.1" },
94 | { label: "Multitasking", value: "2.4.2" },
95 | { label: "Meeting deadlines", value: "2.4.3" },
96 | ],
97 | },
98 | {
99 | label: "Emotional Intelligence",
100 | value: "2.5",
101 | children: [
102 | { label: "Empathy", value: "2.5.1" },
103 | { label: "Self-regulation", value: "2.5.2" },
104 | { label: "Interpersonal skills", value: "2.5.3" },
105 | ],
106 | },
107 | ],
108 | },
109 | {
110 | label: "Leadership Skills",
111 | value: "3",
112 | children: [
113 | {
114 | label: "Project Management",
115 | value: "3.1",
116 | children: [
117 | { label: "Agile methodologies", value: "3.1.1" },
118 | { label: "Risk management", value: "3.1.2" },
119 | { label: "Resource allocation", value: "3.1.3" },
120 | ],
121 | },
122 | {
123 | label: "Decision Making",
124 | value: "3.2",
125 | children: [
126 | { label: "Strategic thinking", value: "3.2.1" },
127 | { label: "Data-driven decision-making", value: "3.2.2" },
128 | { label: "Consensus building", value: "3.2.3" },
129 | ],
130 | },
131 | {
132 | label: "Mentoring",
133 | value: "3.3",
134 | children: [
135 | { label: "Coaching", value: "3.3.1" },
136 | { label: "Providing feedback", value: "3.3.2" },
137 | { label: "Developing others", value: "3.3.3" },
138 | ],
139 | },
140 | {
141 | label: "Change Management",
142 | value: "3.4",
143 | children: [
144 | { label: "Leading through change", value: "3.4.1" },
145 | { label: "Stakeholder engagement", value: "3.4.2" },
146 | { label: "Communication strategies", value: "3.4.3" },
147 | ],
148 | },
149 | {
150 | label: "Vision and Strategy",
151 | value: "3.5",
152 | children: [
153 | { label: "Long-term planning", value: "3.5.1" },
154 | { label: "Innovation", value: "3.5.2" },
155 | { label: "Goal setting", value: "3.5.3" },
156 | ],
157 | },
158 | ],
159 | },
160 | {
161 | label: "Industry Specific Skills",
162 | value: "4",
163 | children: [
164 | {
165 | label: "Healthcare",
166 | value: "4.1",
167 | children: [
168 | { label: "Patient care", value: "4.1.1" },
169 | { label: "Medical coding", value: "4.1.2" },
170 | { label: "HIPAA compliance", value: "4.1.3" },
171 | ],
172 | },
173 | {
174 | label: "Finance",
175 | value: "4.2",
176 | children: [
177 | { label: "Financial analysis", value: "4.2.1" },
178 | { label: "Budgeting", value: "4.2.2" },
179 | { label: "Tax regulations", value: "4.2.3" },
180 | ],
181 | },
182 | {
183 | label: "Marketing",
184 | value: "4.3",
185 | children: [
186 | { label: "SEO", value: "4.3.1" },
187 | { label: "Content creation", value: "4.3.2" },
188 | { label: "Market research", value: "4.3.3" },
189 | ],
190 | },
191 | {
192 | label: "Manufacturing",
193 | value: "4.4",
194 | children: [
195 | { label: "Quality control", value: "4.4.1" },
196 | { label: "Lean manufacturing", value: "4.4.2" },
197 | { label: "Supply chain management", value: "4.4.3" },
198 | ],
199 | },
200 | {
201 | label: "Education",
202 | value: "4.5",
203 | children: [
204 | { label: "Curriculum development", value: "4.5.1" },
205 | { label: "Classroom management", value: "4.5.2" },
206 | { label: "Educational technology", value: "4.5.3" },
207 | ],
208 | },
209 | ],
210 | },
211 | {
212 | label: "Digital Skills",
213 | value: "5",
214 | children: [
215 | {
216 | label: "Social Media Management",
217 | value: "5.1",
218 | children: [
219 | { label: "Content creation", value: "5.1.1" },
220 | { label: "Analytics", value: "5.1.2" },
221 | { label: "Community engagement", value: "5.1.3" },
222 | ],
223 | },
224 | {
225 | label: "Cybersecurity",
226 | value: "5.2",
227 | children: [
228 | { label: "Threat assessment", value: "5.2.1" },
229 | { label: "Risk management", value: "5.2.2" },
230 | { label: "Incident response", value: "5.2.3" },
231 | ],
232 | },
233 | {
234 | label: "Cloud Computing",
235 | value: "5.3",
236 | children: [
237 | { label: "AWS", value: "5.3.1" },
238 | { label: "Azure", value: "5.3.2" },
239 | { label: "Google Cloud Platform", value: "5.3.3" },
240 | ],
241 | },
242 | {
243 | label: "E-commerce",
244 | value: "5.4",
245 | children: [
246 | { label: "Online sales strategies", value: "5.4.1" },
247 | { label: "Payment processing", value: "5.4.2" },
248 | { label: "Customer service", value: "5.4.3" },
249 | ],
250 | },
251 | {
252 | label: "Digital Design",
253 | value: "5.5",
254 | children: [
255 | { label: "UI/UX design", value: "5.5.1" },
256 | { label: "Graphic design", value: "5.5.2" },
257 | { label: "Video editing", value: "5.5.3" },
258 | ],
259 | },
260 | ],
261 | },
262 | {
263 | label: "Research and Analytical Skills",
264 | value: "6",
265 | children: [
266 | {
267 | label: "Market Research",
268 | value: "6.1",
269 | children: [
270 | { label: "Survey design", value: "6.1.1" },
271 | { label: "Data collection", value: "6.1.2" },
272 | { label: "Trend analysis", value: "6.1.3" },
273 | ],
274 | },
275 | {
276 | label: "Scientific Research",
277 | value: "6.2",
278 | children: [
279 | { label: "Experimental design", value: "6.2.1" },
280 | { label: "Data interpretation", value: "6.2.2" },
281 | { label: "Statistical analysis", value: "6.2.3" },
282 | ],
283 | },
284 | {
285 | label: "Financial Research",
286 | value: "6.3",
287 | children: [
288 | { label: "Investment analysis", value: "6.3.1" },
289 | { label: "Risk assessment", value: "6.3.2" },
290 | { label: "Economic forecasting", value: "6.3.3" },
291 | ],
292 | },
293 | {
294 | label: "Policy Analysis",
295 | value: "6.4",
296 | children: [
297 | { label: "Regulatory research", value: "6.4.1" },
298 | { label: "Impact assessment", value: "6.4.2" },
299 | { label: "Legislative analysis", value: "6.4.3" },
300 | ],
301 | },
302 | {
303 | label: "User Research",
304 | value: "6.5",
305 | children: [
306 | { label: "Usability testing", value: "6.5.1" },
307 | { label: "User interviews", value: "6.5.2" },
308 | { label: "Persona development", value: "6.5.3" },
309 | ],
310 | },
311 | ],
312 | },
313 | {
314 | label: "Sales and Customer Service Skills",
315 | value: "7",
316 | children: [
317 | {
318 | label: "Sales Techniques",
319 | value: "7.1",
320 | children: [
321 | { label: "Negotiation", value: "7.1.1" },
322 | { label: "Relationship building", value: "7.1.2" },
323 | { label: "Closing strategies", value: "7.1.3" },
324 | ],
325 | },
326 | {
327 | label: "Customer Support",
328 | value: "7.2",
329 | children: [
330 | { label: "Troubleshooting", value: "7.2.1" },
331 | { label: "Product knowledge", value: "7.2.2" },
332 | { label: "Customer retention", value: "7.2.3" },
333 | ],
334 | },
335 | {
336 | label: "Account Management",
337 | value: "7.3",
338 | children: [
339 | { label: "Client relationship management", value: "7.3.1" },
340 | { label: "Upselling", value: "7.3.2" },
341 | { label: "Contract negotiation", value: "7.3.3" },
342 | ],
343 | },
344 | {
345 | label: "Lead Generation",
346 | value: "7.4",
347 | children: [
348 | { label: "Networking", value: "7.4.1" },
349 | { label: "Cold calling", value: "7.4.2" },
350 | { label: "Inbound marketing", value: "7.4.3" },
351 | ],
352 | },
353 | {
354 | label: "CRM Software",
355 | value: "7.5",
356 | children: [
357 | { label: "Salesforce", value: "7.5.1" },
358 | { label: "HubSpot", value: "7.5.2" },
359 | { label: "Zoho CRM", value: "7.5.3" },
360 | ],
361 | },
362 | ],
363 | },
364 | {
365 | label: "Creative Skills",
366 | value: "8",
367 | children: [
368 | {
369 | label: "Content Creation",
370 | value: "8.1",
371 | children: [
372 | { label: "Writing", value: "8.1.1" },
373 | { label: "Blogging", value: "8.1.2" },
374 | { label: "Video production", value: "8.1.3" },
375 | ],
376 | },
377 | {
378 | label: "Graphic Design",
379 | value: "8.2",
380 | children: [
381 | { label: "Logo design", value: "8.2.1" },
382 | { label: "Branding", value: "8.2.2" },
383 | { label: "Illustration", value: "8.2.3" },
384 | ],
385 | },
386 | {
387 | label: "Artistic Skills",
388 | value: "8.3",
389 | children: [
390 | { label: "Photography", value: "8.3.1" },
391 | { label: "Painting", value: "8.3.2" },
392 | { label: "Sculpture", value: "8.3.3" },
393 | ],
394 | },
395 | {
396 | label: "Creative Writing",
397 | value: "8.4",
398 | children: [
399 | { label: "Copywriting", value: "8.4.1" },
400 | { label: "Storytelling", value: "8.4.2" },
401 | { label: "Scriptwriting", value: "8.4.3" },
402 | ],
403 | },
404 | {
405 | label: "Music and Performing Arts",
406 | value: "8.5",
407 | children: [
408 | { label: "Instrument proficiency", value: "8.5.1" },
409 | { label: "Acting", value: "8.5.2" },
410 | { label: "Stage production", value: "8.5.3" },
411 | ],
412 | },
413 | ],
414 | },
415 | ];
416 |
417 | const getCoreCompetencies = async (): Promise => {
418 | await wait();
419 |
420 | return [
421 | {
422 | label: "Project Management",
423 | value: "1",
424 | },
425 | {
426 | label: "Communication",
427 | value: "2",
428 | },
429 | {
430 | label: "Technical Skills",
431 | value: "3",
432 | },
433 | {
434 | label: "Leadership",
435 | value: "4",
436 | },
437 | {
438 | label: "Problem-Solving",
439 | value: "5",
440 | },
441 | {
442 | label: "Other",
443 | value: "6",
444 | },
445 | ];
446 | };
447 |
448 | const getProficiencyLevels = async (): Promise => {
449 | await wait();
450 |
451 | return [
452 | {
453 | label: "Beginner",
454 | value: "1",
455 | },
456 | {
457 | label: "Intermediate",
458 | value: "2",
459 | },
460 | {
461 | label: "Advanced",
462 | value: "3",
463 | },
464 | ];
465 | };
466 |
467 | const getSkillCategories = async (): Promise => {
468 | await wait();
469 | return mockSkillCategories.map((category) => ({
470 | label: category.label,
471 | value: category.value,
472 | }));
473 | };
474 |
475 | const getSkillSubcategories = async (
476 | categoryId: string
477 | ): Promise => {
478 | await wait();
479 | const category = mockSkillCategories.find((cat) => cat.value === categoryId);
480 | if (!category) return [];
481 |
482 | return category.children.map((subcategory) => ({
483 | label: subcategory.label,
484 | value: subcategory.value,
485 | }));
486 | };
487 |
488 | const getSkills = async (
489 | subcategoryId: string
490 | ): Promise => {
491 | await wait();
492 | for (const category of mockSkillCategories) {
493 | const subcategory = category.children.find(
494 | (sub) => sub.value === subcategoryId
495 | );
496 | if (subcategory) {
497 | return subcategory.children.map((skill) => ({
498 | label: skill.label,
499 | value: skill.value,
500 | }));
501 | }
502 | }
503 | return [];
504 | };
505 |
506 | const getLanguages = async (): Promise => {
507 | await wait();
508 | return [
509 | { value: "1", label: "English" },
510 | { value: "2", label: "Spanish" },
511 | { value: "3", label: "French" },
512 | { value: "4", label: "German" },
513 | { value: "5", label: "Chinese" },
514 | { value: "6", label: "Japanese" },
515 | { value: "7", label: "Korean" },
516 | { value: "8", label: "Arabic" },
517 | { value: "9", label: "Hindi" },
518 | { value: "10", label: "Portuguese" },
519 | { value: "11", label: "Russian" },
520 | { value: "12", label: "Italian" },
521 | ];
522 | };
523 |
524 | export {
525 | getCoreCompetencies,
526 | getProficiencyLevels,
527 | getSkillCategories,
528 | getSkillSubcategories,
529 | getSkills,
530 | getLanguages,
531 | };
532 |
--------------------------------------------------------------------------------
/src/features/employee/wrapper/components/stepper.tsx:
--------------------------------------------------------------------------------
1 | import { useEmployeeAdditionalInfoStore } from "@/features/employee/additional-info/hooks/useStore";
2 | import { employeeAdditionalInfoSchema } from "@/features/employee/additional-info/types/schema";
3 | import { useEmployeeHistoryStore } from "@/features/employee/history/hooks/useStore";
4 | import { employeeHistorySchema } from "@/features/employee/history/types/schema";
5 | import { useEmployeePersonalInfoStore } from "@/features/employee/personal-info/hooks/useStore";
6 | import { employeePersonalInfoSchema } from "@/features/employee/personal-info/types/schema";
7 | import { useEmployeeReviewStore } from "@/features/employee/review/hooks/useStore";
8 | import { employeeReviewSchema } from "@/features/employee/review/types/schema";
9 | import { useEmployeeSkillsStore } from "@/features/employee/skills/hooks/useStore";
10 | import { employeeSkillsSchema } from "@/features/employee/skills/types/schema";
11 | import { d } from "@/utils/dictionary";
12 | import {
13 | Stepper as MuiStepper,
14 | Step,
15 | StepButton,
16 | Typography,
17 | } from "@mui/material";
18 | import { useLocation } from "react-router";
19 |
20 | const Stepper = () => {
21 | const { pathname } = useLocation();
22 |
23 | const { formData: employeePersonalInfoFormData } =
24 | useEmployeePersonalInfoStore();
25 | const { formData: employeeHistoryFormData } = useEmployeeHistoryStore();
26 | const { formData: employeeSkillsFormData } = useEmployeeSkillsStore();
27 | const { formData: employeeAdditionalInfoFormData } =
28 | useEmployeeAdditionalInfoStore();
29 | const {
30 | formData: employeeReviewFormData,
31 | isSubmitted: isEmployeeReviewSubmitted,
32 | } = useEmployeeReviewStore();
33 |
34 | const { success: employeePersonaInfoSuccess } =
35 | employeePersonalInfoSchema.safeParse(employeePersonalInfoFormData);
36 |
37 | const { success: employeeHistorySuccess } = employeeHistorySchema.safeParse(
38 | employeeHistoryFormData
39 | );
40 |
41 | const { success: employeeSkillsSuccess } = employeeSkillsSchema.safeParse(
42 | employeeSkillsFormData
43 | );
44 |
45 | const { success: employeeAdditionalInfoSuccess } =
46 | employeeAdditionalInfoSchema.safeParse(employeeAdditionalInfoFormData);
47 |
48 | const { success: employeeReviewSuccess } = employeeReviewSchema.safeParse(
49 | employeeReviewFormData
50 | );
51 |
52 | const steps = [
53 | {
54 | href: "/employee/personal-info",
55 | label: d.personalInfo,
56 | success: employeePersonaInfoSuccess,
57 | },
58 | {
59 | href: "/employee/history",
60 | label: d.history,
61 | success: employeeHistorySuccess,
62 | },
63 | {
64 | href: "/employee/skills",
65 | label: d.skills,
66 | success: employeeSkillsSuccess,
67 | },
68 | {
69 | href: "/employee/additional-info",
70 | label: d.additionalInfo,
71 | success: employeeAdditionalInfoSuccess,
72 | },
73 | {
74 | href: "/employee/review",
75 | label: d.review,
76 | success: employeeReviewSuccess,
77 | },
78 | ];
79 |
80 | const activeStep = steps.findIndex((item) => item.href === pathname);
81 |
82 | return (
83 |
84 | {steps.map((step) => (
85 |
86 |
93 | {d.invalidFormData}
94 |
95 | )
96 | }
97 | >
98 | {step.label}
99 |
100 |
101 | ))}
102 |
103 | );
104 | };
105 |
106 | export { Stepper };
107 |
--------------------------------------------------------------------------------
/src/features/employee/wrapper/components/summary-dialog.tsx:
--------------------------------------------------------------------------------
1 | import { useEmployeeAdditionalInfoStore } from "@/features/employee/additional-info/hooks/useStore";
2 |
3 | import SendOutlinedIcon from "@mui/icons-material/SendOutlined";
4 | import { EmployeeAdditionalInfo } from "@/features/employee/additional-info/page";
5 | import { useEmployeeHistoryStore } from "@/features/employee/history/hooks/useStore";
6 | import { EmployeeHistory } from "@/features/employee/history/page";
7 | import { useEmployeePersonalInfoStore } from "@/features/employee/personal-info/hooks/useStore";
8 | import { EmployeePersonalInfo } from "@/features/employee/personal-info/page";
9 | import { useEmployeeReviewStore } from "@/features/employee/review/hooks/useStore";
10 | import { EmployeeReview } from "@/features/employee/review/page";
11 | import { useEmployeeSkillsStore } from "@/features/employee/skills/hooks/useStore";
12 | import { EmployeeSkills } from "@/features/employee/skills/page";
13 | import { useCreate } from "@/features/employee/wrapper/hooks/useMutations";
14 | import { useStore } from "@/features/employee/wrapper/hooks/useStore";
15 | import { schema } from "@/features/employee/wrapper/types/schema";
16 | import { getErrorMessage } from "@/utils/getErrorMessage";
17 | import { showSnack } from "@/utils/showSnack";
18 | import { LoadingButton } from "@mui/lab";
19 | import {
20 | Button,
21 | Dialog,
22 | DialogActions,
23 | DialogContent,
24 | DialogTitle,
25 | Divider,
26 | } from "@mui/material";
27 | import { FormEvent } from "react";
28 | import { d } from "@/utils/dictionary";
29 |
30 | const SummaryDialog = () => {
31 | const { summaryDialogOpen, updateSummaryDialogOpen } = useStore();
32 | const createMutation = useCreate();
33 |
34 | const { formData: employeePersonalInfoFormData } =
35 | useEmployeePersonalInfoStore();
36 | const { formData: employeeHistoryFormData } = useEmployeeHistoryStore();
37 | const { formData: employeeSkillsFormData } = useEmployeeSkillsStore();
38 | const { formData: employeeAdditionalInfoFormData } =
39 | useEmployeeAdditionalInfoStore();
40 | const { formData: employeeReviewFormData } = useEmployeeReviewStore();
41 |
42 | const allFormData = {
43 | ...employeePersonalInfoFormData,
44 | ...employeeHistoryFormData,
45 | ...employeeSkillsFormData,
46 | ...employeeAdditionalInfoFormData,
47 | ...employeeReviewFormData,
48 | };
49 |
50 | const handleClose = () => {
51 | if (!createMutation.isPending) {
52 | updateSummaryDialogOpen(false);
53 | }
54 | };
55 |
56 | const onSubmit = (e: FormEvent) => {
57 | e.preventDefault();
58 | try {
59 | schema.parse(allFormData);
60 | createMutation.mutate(undefined, { onSuccess: handleClose });
61 | } catch (error) {
62 | showSnack(getErrorMessage(error), { variant: "error" });
63 | }
64 | };
65 |
66 | return (
67 |
102 | );
103 | };
104 |
105 | export { SummaryDialog };
106 |
--------------------------------------------------------------------------------
/src/features/employee/wrapper/hooks/useMutations.ts:
--------------------------------------------------------------------------------
1 | import { useEmployeeAdditionalInfoStore } from "@/features/employee/additional-info/hooks/useStore";
2 | import { useEmployeeHistoryStore } from "@/features/employee/history/hooks/useStore";
3 | import { useEmployeePersonalInfoStore } from "@/features/employee/personal-info/hooks/useStore";
4 | import { useEmployeeReviewStore } from "@/features/employee/review/hooks/useStore";
5 | import { useEmployeeSkillsStore } from "@/features/employee/skills/hooks/useStore";
6 | import { create } from "@/features/employee/wrapper/utils/api";
7 | import { getErrorMessage } from "@/utils/getErrorMessage";
8 | import { showSnack } from "@/utils/showSnack";
9 | import { useMutation } from "@tanstack/react-query";
10 |
11 | const useCreate = () => {
12 | const { formData: employeePersonalInfoFormData } =
13 | useEmployeePersonalInfoStore();
14 | const { formData: employeeHistoryFormData } = useEmployeeHistoryStore();
15 | const { formData: employeeSkillsFormData } = useEmployeeSkillsStore();
16 | const { formData: employeeAdditionalInfoFormData } =
17 | useEmployeeAdditionalInfoStore();
18 | const { formData: employeeReviewFormData } = useEmployeeReviewStore();
19 |
20 | return useMutation({
21 | mutationFn: () =>
22 | create({
23 | ...employeePersonalInfoFormData,
24 | ...employeeHistoryFormData,
25 | ...employeeSkillsFormData,
26 | ...employeeAdditionalInfoFormData,
27 | ...employeeReviewFormData,
28 | }),
29 |
30 | onSuccess: async () => {
31 | showSnack("Successful");
32 | },
33 | onError: (error) => {
34 | showSnack(getErrorMessage(error), { variant: "error" });
35 | },
36 | });
37 | };
38 |
39 | export { useCreate };
40 |
--------------------------------------------------------------------------------
/src/features/employee/wrapper/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from "@/utils/createStore";
2 |
3 | type State = {
4 | summaryDialogOpen: boolean;
5 | };
6 |
7 | type Actions = {
8 | updateSummaryDialogOpen: (is: State["summaryDialogOpen"]) => void;
9 | };
10 |
11 | type Store = State & Actions;
12 |
13 | const useStore = createStore(
14 | (set) => ({
15 | summaryDialogOpen: false,
16 | updateSummaryDialogOpen: (is) =>
17 | set((state) => {
18 | state.summaryDialogOpen = is;
19 | }),
20 | }),
21 | {
22 | name: "employee-wrapper-store",
23 | }
24 | );
25 |
26 | export { useStore, useStore as useEmployeeWrapperStore };
27 |
--------------------------------------------------------------------------------
/src/features/employee/wrapper/page.tsx:
--------------------------------------------------------------------------------
1 | import { Stepper } from "@/features/employee/wrapper/components/stepper";
2 | import { SummaryDialog } from "@/features/employee/wrapper/components/summary-dialog";
3 | import { Divider } from "@mui/material";
4 | import { Outlet } from "react-router";
5 |
6 | const Page = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 | >
14 | );
15 | };
16 |
17 | export { Page as EmployeeWrapper };
18 |
--------------------------------------------------------------------------------
/src/features/employee/wrapper/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { employeeAdditionalInfoSchema } from "@/features/employee/additional-info/types/schema";
2 | import { employeeHistorySchema } from "@/features/employee/history/types/schema";
3 | import { employeePersonalInfoSchema } from "@/features/employee/personal-info/types/schema";
4 | import { employeeReviewSchema } from "@/features/employee/review/types/schema";
5 | import { employeeSkillsSchema } from "@/features/employee/skills/types/schema";
6 | import { z } from "zod";
7 |
8 | const schema = employeePersonalInfoSchema
9 | .and(employeeSkillsSchema)
10 | .and(employeeHistorySchema)
11 | .and(employeeReviewSchema)
12 | .and(employeeAdditionalInfoSchema);
13 |
14 | type Schema = z.infer;
15 |
16 | export { schema, type Schema };
17 |
--------------------------------------------------------------------------------
/src/features/employee/wrapper/utils/api.ts:
--------------------------------------------------------------------------------
1 | import { Schema } from "@/features/employee/wrapper/types/schema";
2 | import { wait } from "@/utils/wait";
3 |
4 | const create = async (data: Schema) => {
5 | await wait();
6 | console.log(data);
7 | };
8 |
9 | export { create };
10 |
--------------------------------------------------------------------------------
/src/features/form/components/controllers/autocomplete.tsx:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "@/features/form/hooks/useFormContext";
2 | import { d } from "@/utils/dictionary";
3 | import {
4 | AutocompleteValue,
5 | Autocomplete as MuiAutocomplete,
6 | AutocompleteProps as MuiAutocompleteProps,
7 | TextField as MuiTextField,
8 | } from "@mui/material";
9 | import { forwardRef, ReactElement, Ref } from "react";
10 | import { Controller, FieldValues, Path } from "react-hook-form";
11 |
12 | type AutocompleteOption = {
13 | label: string;
14 | value: string | number;
15 | };
16 |
17 | type AutocompleteProps<
18 | T extends FieldValues,
19 | Multiple extends boolean = false
20 | > = Omit<
21 | MuiAutocompleteProps,
22 | "renderInput" | "onChange" | "options" | "multiple"
23 | > & {
24 | name: Path;
25 | textFieldProps?: Omit<
26 | React.ComponentProps,
27 | "name" | "error" | "helperText"
28 | >;
29 | options: AutocompleteOption[] | undefined;
30 | multiple?: Multiple;
31 | onOptionSelect?: Multiple extends true
32 | ? (options: AutocompleteOption[]) => void
33 | : (option: AutocompleteOption | null) => void;
34 | };
35 |
36 | const Autocomplete = forwardRef(
37 | (
38 | {
39 | name,
40 | options,
41 | textFieldProps,
42 | onOptionSelect,
43 | multiple = false as Multiple,
44 | ...autocompleteProps
45 | }: AutocompleteProps,
46 | ref: Ref
47 | ) => {
48 | const { control, readOnly } = useFormContext();
49 |
50 | return (
51 | {
58 | const getValue = (): AutocompleteValue<
59 | AutocompleteOption,
60 | Multiple,
61 | false,
62 | false
63 | > => {
64 | if (multiple) {
65 | return (options ?? []).filter((option) =>
66 | Array.isArray(value) ? value.includes(option.value) : false
67 | ) as AutocompleteValue<
68 | AutocompleteOption,
69 | Multiple,
70 | false,
71 | false
72 | >;
73 | }
74 | return ((options ?? []).find((option) => option.value === value) ||
75 | null) as AutocompleteValue<
76 | AutocompleteOption,
77 | Multiple,
78 | false,
79 | false
80 | >;
81 | };
82 |
83 | return (
84 |
85 | {...autocompleteProps}
86 | {...field}
87 | multiple={multiple}
88 | options={options ?? []}
89 | value={getValue()}
90 | readOnly={readOnly}
91 | id={name}
92 | onChange={(_, newValue) => {
93 | if (multiple) {
94 | const values = (newValue as AutocompleteOption[]).map(
95 | (option) => option.value
96 | );
97 | onChange(values);
98 | if (onOptionSelect) {
99 | (onOptionSelect as (options: AutocompleteOption[]) => void)(
100 | newValue as AutocompleteOption[]
101 | );
102 | }
103 | } else {
104 | const singleValue = newValue as AutocompleteOption | null;
105 | onChange(singleValue?.value ?? "");
106 | if (onOptionSelect) {
107 | (
108 | onOptionSelect as (
109 | option: AutocompleteOption | null
110 | ) => void
111 | )(singleValue);
112 | }
113 | }
114 | }}
115 | renderInput={(params) => (
116 |
133 | )}
134 | />
135 | );
136 | }}
137 | />
138 | );
139 | }
140 | ) as (
141 | props: AutocompleteProps & { ref?: Ref }
142 | ) => ReactElement;
143 |
144 | export { Autocomplete, type AutocompleteOption };
145 |
--------------------------------------------------------------------------------
/src/features/form/components/controllers/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorMessage } from "@/features/form/components/error-message";
2 | import { useFormContext } from "@/features/form/hooks/useFormContext";
3 | import {
4 | FormControl,
5 | FormControlLabel,
6 | FormControlLabelProps,
7 | Checkbox as MuiCheckbox,
8 | CheckboxProps as MuiCheckboxProps,
9 | useTheme,
10 | } from "@mui/material";
11 | import { forwardRef, ReactElement, Ref } from "react";
12 | import { Controller, FieldValues, Path } from "react-hook-form";
13 |
14 | type CheckboxProps = Omit<
15 | MuiCheckboxProps,
16 | "name" | "checked" | "defaultChecked"
17 | > & {
18 | name: Path;
19 | label?: string;
20 | labelPlacement?: FormControlLabelProps["labelPlacement"];
21 | helperText?: string;
22 | };
23 |
24 | const Checkbox = forwardRef(
25 | (
26 | {
27 | name,
28 | label,
29 | labelPlacement = "end",
30 | helperText,
31 | ...checkboxProps
32 | }: CheckboxProps,
33 | ref: Ref
34 | ) => {
35 | const { control, readOnly } = useFormContext();
36 | const theme = useTheme();
37 |
38 | return (
39 | {
46 | const checkbox = (
47 | onChange(event.target.checked)}
53 | sx={{
54 | color: error ? "error.main" : undefined,
55 | "&.Mui-checked": {
56 | color: error ? "error.main" : undefined,
57 | },
58 | }}
59 | />
60 | );
61 |
62 | return (
63 |
64 | {label ? (
65 |
76 | ) : (
77 | checkbox
78 | )}
79 |
80 | {(error || helperText) && name={name} />}
81 |
82 | );
83 | }}
84 | />
85 | );
86 | }
87 | ) as (
88 | props: CheckboxProps & { ref?: Ref }
89 | ) => ReactElement;
90 |
91 | export { Checkbox };
92 |
--------------------------------------------------------------------------------
/src/features/form/components/controllers/date-picker.tsx:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "@/features/form/hooks/useFormContext";
2 | import { SxProps, Theme } from "@mui/material";
3 | import {
4 | DatePicker as MuiDatePicker,
5 | DatePickerProps as MuiDatePickerProps,
6 | } from "@mui/x-date-pickers";
7 | import { forwardRef, ReactElement, Ref } from "react";
8 | import { Controller, FieldValues, Path } from "react-hook-form";
9 |
10 | type DatePickerProps = Omit<
11 | MuiDatePickerProps,
12 | "name" | "value" | "onChange"
13 | > & {
14 | name: Path;
15 | };
16 |
17 | const DatePicker = forwardRef(
18 | (
19 | { name, sx, ...datePickerProps }: DatePickerProps,
20 | ref: Ref
21 | ) => {
22 | const { control, readOnly } = useFormContext();
23 |
24 | const defaultSx: SxProps = {
25 | width: 1,
26 | ...sx,
27 | };
28 |
29 | return (
30 | {
34 | const isValidDate = (val: unknown): val is Date => {
35 | return val instanceof Date && !isNaN(val.getTime());
36 | };
37 |
38 | const dateValue = isValidDate(value)
39 | ? value
40 | : value
41 | ? new Date(value as string)
42 | : null;
43 |
44 | return (
45 | {
49 | const finalValue = isValidDate(newValue)
50 | ? newValue.toISOString()
51 | : newValue;
52 | onChange(finalValue);
53 | }}
54 | ref={ref}
55 | disableOpenPicker={readOnly}
56 | sx={defaultSx}
57 | readOnly={readOnly}
58 | slotProps={{
59 | ...datePickerProps.slotProps,
60 | textField: {
61 | ...datePickerProps.slotProps?.textField,
62 | error: !!error,
63 | helperText: error?.message,
64 | name,
65 | },
66 | }}
67 | />
68 | );
69 | }}
70 | />
71 | );
72 | }
73 | ) as (
74 | props: DatePickerProps,
75 | ref: Ref
76 | ) => ReactElement;
77 |
78 | export { DatePicker };
79 |
--------------------------------------------------------------------------------
/src/features/form/components/controllers/menu.tsx:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "@/features/form/hooks/useFormContext";
2 | import { Box, IconButton, Menu as MuiMenu } from "@mui/material";
3 | import MenuItem, {
4 | MenuItemProps as MuiMenuItemProps,
5 | } from "@mui/material/MenuItem";
6 | import Typography from "@mui/material/Typography";
7 | import { SxProps } from "@mui/material/styles";
8 | import {
9 | bindPopover,
10 | usePopupState,
11 | bindTrigger,
12 | } from "material-ui-popup-state/hooks";
13 | import { forwardRef, ReactElement, ReactNode, Ref } from "react";
14 | import { Controller, FieldValues, Path } from "react-hook-form";
15 |
16 | type Option = {
17 | value: string | number;
18 | label: string;
19 | leftIcon?: ReactNode;
20 | rightIcon?: ReactNode;
21 | disabled?: boolean;
22 | };
23 |
24 | type MenuProps = Omit<
25 | MuiMenuItemProps,
26 | "name" | "error" | "value"
27 | > & {
28 | name: Path;
29 | options: Option[];
30 | MenuItemProps?: MuiMenuItemProps;
31 | className?: string;
32 | renderLabel?: (option: Option) => ReactNode;
33 | sx?: SxProps;
34 | };
35 |
36 | const Menu = forwardRef(
37 | (
38 | {
39 | name,
40 | options,
41 | MenuItemProps,
42 | className,
43 | renderLabel,
44 | sx,
45 | onChange,
46 | ...props
47 | }: MenuProps,
48 | ref: Ref
49 | ) => {
50 | const state = usePopupState({ variant: "popover" });
51 |
52 | const { control } = useFormContext();
53 |
54 | return (
55 | (
59 | <>
60 |
61 | {options.map((option) => (
62 |
98 | ))}
99 |
100 |
101 | {options?.find((item) => item.value === field.value)?.leftIcon}
102 |
103 | >
104 | )}
105 | />
106 | );
107 | }
108 | ) as (
109 | props: MenuProps & { ref?: Ref }
110 | ) => ReactElement;
111 |
112 | export { Menu };
113 |
--------------------------------------------------------------------------------
/src/features/form/components/controllers/slider.tsx:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "@/features/form/hooks/useFormContext";
2 | import {
3 | Slider as MuiSlider,
4 | SliderProps as MuiSliderProps,
5 | SxProps,
6 | Theme,
7 | } from "@mui/material";
8 | import Typography from "@mui/material/Typography";
9 | import { forwardRef, ReactElement, Ref } from "react";
10 | import { Controller, FieldValues, Path } from "react-hook-form";
11 |
12 | type SliderProps = Omit<
13 | MuiSliderProps,
14 | "name" | "value" | "onChange"
15 | > & {
16 | name: Path;
17 | label?: string;
18 | unit?: string;
19 | };
20 |
21 | const formatNumber = (value: number): string => {
22 | if (value >= 1000000) {
23 | return `${(value / 1000000).toFixed(1)}M`;
24 | }
25 | if (value >= 1000) {
26 | return `${(value / 1000).toFixed(1)}k`;
27 | }
28 | return value.toString();
29 | };
30 |
31 | const Slider = forwardRef(
32 | (
33 | {
34 | name,
35 | label,
36 | min = 0,
37 | max = 100,
38 | sx,
39 | unit,
40 | ...sliderProps
41 | }: SliderProps,
42 | ref: Ref
43 | ) => {
44 | const { control, readOnly } = useFormContext();
45 |
46 | const defaultSx: SxProps = {
47 | width: 1,
48 | ...sx,
49 | };
50 |
51 | return (
52 | (
56 | <>
57 | {label && {label}}
58 | `${unit}${formatNumber(value)}`}
69 | />
70 | >
71 | )}
72 | />
73 | );
74 | }
75 | ) as (
76 | props: SliderProps & { ref?: Ref }
77 | ) => ReactElement;
78 |
79 | export { Slider };
80 |
--------------------------------------------------------------------------------
/src/features/form/components/controllers/text-field.tsx:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "@/features/form/hooks/useFormContext";
2 | import {
3 | InputBaseComponentProps,
4 | TextField as MuiTextField,
5 | TextFieldProps as MuiTextFieldProps,
6 | SxProps,
7 | Theme,
8 | } from "@mui/material";
9 | import { forwardRef, ReactElement, Ref } from "react";
10 | import { Controller, FieldValues, Path } from "react-hook-form";
11 | import { NumericFormat, PatternFormat } from "react-number-format";
12 |
13 | type FormatType =
14 | | "number"
15 | | "phoneNumber"
16 | | "currency"
17 | | "socialSecurity"
18 | | undefined;
19 |
20 | type CustomNumberFormatProps = InputBaseComponentProps & {
21 | onChange: (event: { target: { name: string; value: string } }) => void;
22 | name: string;
23 | };
24 |
25 | type TextFieldProps = Omit<
26 | MuiTextFieldProps,
27 | "name" | "error" | "helperText"
28 | > & {
29 | name: Path;
30 | format?: FormatType;
31 | };
32 |
33 | const createNumberFormat = (
34 | formatConfig: {
35 | format?: string;
36 | mask?: string;
37 | thousandSeparator?: boolean;
38 | allowEmptyFormatting?: boolean;
39 | } = {}
40 | ) => {
41 | return forwardRef(
42 | function NumberFormat(props, ref) {
43 | const { onChange, name, ...other } = props;
44 |
45 | const handleValueChange = (values: { value: string }) => {
46 | onChange({
47 | target: {
48 | name,
49 | value: values.value,
50 | },
51 | });
52 | };
53 |
54 | const Component = formatConfig.format ? PatternFormat : NumericFormat;
55 |
56 | return (
57 |
64 | );
65 | }
66 | );
67 | };
68 |
69 | const formatComponents = {
70 | number: createNumberFormat({
71 | thousandSeparator: true,
72 | }),
73 | phoneNumber: createNumberFormat({
74 | format: "(###) ###-####",
75 | allowEmptyFormatting: true,
76 | mask: "_",
77 | }),
78 | socialSecurity: createNumberFormat({
79 | format: "### ## ####",
80 | allowEmptyFormatting: true,
81 | mask: "_",
82 | }),
83 | currency: createNumberFormat({
84 | thousandSeparator: true,
85 | }),
86 | };
87 |
88 | const TextField = forwardRef(
89 | (
90 | { name, format, sx, ...textFieldProps }: TextFieldProps,
91 | ref: Ref
92 | ) => {
93 | const { control, readOnly } = useFormContext();
94 |
95 | const getInputComponent = (format?: FormatType) => {
96 | return format ? formatComponents[format] : undefined;
97 | };
98 |
99 | const defaultSx: SxProps = {
100 | width: 1,
101 | ...sx,
102 | };
103 |
104 | return (
105 | (
109 |
126 | )}
127 | />
128 | );
129 | }
130 | ) as (
131 | props: TextFieldProps & { ref?: Ref }
132 | ) => ReactElement;
133 |
134 | export { TextField };
135 |
--------------------------------------------------------------------------------
/src/features/form/components/error-message.tsx:
--------------------------------------------------------------------------------
1 | import { FormHelperText } from "@mui/material";
2 |
3 | import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message";
4 | import { ArrayPath, FieldValues, Path } from "react-hook-form";
5 |
6 | type ErrorMessageProps = {
7 | name: Path | ArrayPath;
8 | };
9 |
10 | const ErrorMessage = ({
11 | name,
12 | }: ErrorMessageProps) => {
13 | return (
14 | <>
15 | } />
16 | } />
17 | >
18 | );
19 | };
20 |
21 | export { ErrorMessage };
22 |
--------------------------------------------------------------------------------
/src/features/form/components/form-error-summary.tsx:
--------------------------------------------------------------------------------
1 | import { d } from "@/utils/dictionary";
2 | import { formatErrors, ErrorMessage } from "@/utils/formatErrors";
3 | import { Alert, AlertTitle, List, ListItem } from "@mui/material";
4 | import { useFormState } from "react-hook-form";
5 | import { humanizeFieldName } from "@/utils/humanizeFieldName";
6 |
7 | const FormErrorSummary = () => {
8 | const { errors, isSubmitted } = useFormState();
9 |
10 | if (!isSubmitted || !errors || Object.keys(errors).length === 0) return null;
11 |
12 | const formattedErrors = formatErrors(errors);
13 |
14 | const handleErrorClick = (field: string) => {
15 | const cleanField = field.endsWith(".root") ? field.slice(0, -5) : field;
16 | const formFieldName = cleanField.replace(/\[(\d+)\]/g, ".$1");
17 |
18 | try {
19 | const elementById = document.getElementById(formFieldName);
20 | if (elementById) {
21 | elementById.scrollIntoView({ behavior: "smooth", block: "center" });
22 | elementById.focus();
23 | return;
24 | }
25 | const elementByName = document.getElementsByName(formFieldName)[0];
26 | if (elementByName) {
27 | elementByName.scrollIntoView({ behavior: "smooth", block: "center" });
28 | elementByName.focus();
29 | return;
30 | }
31 | console.warn(`No element found for field: ${formFieldName}`);
32 | } catch (error) {
33 | console.error("Error focusing field:", formFieldName, error);
34 | }
35 | };
36 |
37 | const getDisplayLabel = (label: string, field: string) => {
38 | if (label.toLowerCase() === "root") {
39 | const parentKey = field.split(".root")[0].split(".").pop() || field;
40 | return d[parentKey as keyof typeof d] || humanizeFieldName(parentKey);
41 | }
42 | return label;
43 | };
44 |
45 | const groupedErrors = formattedErrors.reduce(
46 | (acc: Record, error) => {
47 | if (error.category) {
48 | if (!acc[error.category]) {
49 | acc[error.category] = [];
50 | }
51 | acc[error.category].push(error);
52 | } else {
53 | if (!acc["general"]) {
54 | acc["general"] = [];
55 | }
56 | acc["general"].push(error);
57 | }
58 | return acc;
59 | },
60 | {}
61 | );
62 |
63 | return (
64 |
71 | {d.errorValidationTitle}
72 |
73 | {groupedErrors["general"]?.map(({ label, message, field }, index) => (
74 | handleErrorClick(field)}
82 | >
83 | • {getDisplayLabel(label, field)}: {message}
84 |
85 | ))}
86 |
87 | {Object.entries(groupedErrors).map(([category, errors]) => {
88 | if (category === "general") return null;
89 |
90 | return (
91 |
92 |
93 | {category}:
94 |
95 | {errors.map(({ label, message, field, index }, i) => (
96 | handleErrorClick(field)}
104 | >
105 | • {index !== undefined ? `#${index + 1} - ` : ""}
106 | {getDisplayLabel(label, field)}: {message}
107 |
108 | ))}
109 |
110 | );
111 | })}
112 |
113 |
114 | );
115 | };
116 |
117 | export { FormErrorSummary };
118 |
--------------------------------------------------------------------------------
/src/features/form/components/form.tsx:
--------------------------------------------------------------------------------
1 | import Grid from "@mui/material/Grid2";
2 |
3 | import { FormErrorSummary } from "@/features/form/components/form-error-summary";
4 | import { d } from "@/utils/dictionary";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import RestartAltOutlinedIcon from "@mui/icons-material/RestartAltOutlined";
7 | import {
8 | Button,
9 | ButtonProps,
10 | IconButton,
11 | IconButtonProps,
12 | Typography,
13 | } from "@mui/material";
14 | import { ReactNode } from "react";
15 | import {
16 | DefaultValues,
17 | FieldValues,
18 | SubmitErrorHandler,
19 | SubmitHandler,
20 | useForm,
21 | UseFormProps,
22 | } from "react-hook-form";
23 | import { ZodSchema } from "zod";
24 |
25 | import { FormContext } from "@/features/form/types/formContext";
26 | import { FormProvider } from "react-hook-form";
27 | import { useConfirm } from "@/features/confirm/hooks/useContext";
28 |
29 | type FormProps = {
30 | children: ReactNode;
31 | schema: ZodSchema;
32 | title?: string;
33 | onSubmit: SubmitHandler;
34 | onError?: SubmitErrorHandler;
35 | slotProps?: {
36 | submitButtonProps?: ButtonProps;
37 | resetButtonProps?: Partial;
38 | formContainerProps?: Partial;
39 | };
40 | showResetButton?: boolean;
41 | mode?: UseFormProps["mode"];
42 | submitButtonText?: string;
43 | values?: UseFormProps["values"];
44 | defaultValues?: DefaultValues;
45 | readOnly?: boolean;
46 | };
47 |
48 | const Form = ({
49 | children,
50 | schema,
51 | title,
52 | onSubmit,
53 | onError,
54 | slotProps,
55 | showResetButton = true,
56 | mode = "all",
57 | values,
58 | defaultValues,
59 | submitButtonText,
60 | readOnly = false,
61 | }: FormProps) => {
62 | const confirm = useConfirm();
63 |
64 | const form = useForm({
65 | mode,
66 | values,
67 | defaultValues,
68 | resolver: zodResolver(schema),
69 | });
70 |
71 | const handleConfirm = () => {
72 | form.reset(defaultValues);
73 | };
74 |
75 | const handleResetFormClick = async () => {
76 | await confirm({
77 | onConfirm: handleConfirm,
78 | });
79 | };
80 |
81 | const extendedForm: FormContext = {
82 | ...form,
83 | readOnly,
84 | };
85 |
86 | return (
87 |
88 |
95 | {title && (
96 |
104 | {title}
105 | {showResetButton && !readOnly && (
106 |
111 |
112 |
113 | )}
114 |
115 | )}
116 |
117 |
118 |
119 |
120 |
121 | {children}
122 |
123 | {!readOnly && (
124 |
125 |
134 |
135 | )}
136 |
137 |
138 | );
139 | };
140 |
141 | export { Form };
142 |
--------------------------------------------------------------------------------
/src/features/form/hooks/useFormContext.ts:
--------------------------------------------------------------------------------
1 | import { FormContext } from "@/features/form/types/formContext";
2 | import { useFormContext as useRHFFormContext } from "react-hook-form";
3 |
4 | const useFormContext = >() => {
5 | const context = useRHFFormContext() as FormContext;
6 |
7 | if (context === undefined) {
8 | throw new Error("useFormContext must be used within a FormProvider");
9 | }
10 |
11 | return context;
12 | };
13 |
14 | export { useFormContext };
15 |
--------------------------------------------------------------------------------
/src/features/form/hooks/useFormLogger.ts:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "@/features/form/hooks/useFormContext";
2 | import { useEffect } from "react";
3 |
4 | const useFormLogger = () => {
5 | const { watch } = useFormContext();
6 |
7 | useEffect(() => {
8 | const { unsubscribe } = watch((value) => console.log(value));
9 | return unsubscribe;
10 | }, [watch]);
11 | };
12 |
13 | export { useFormLogger };
14 |
--------------------------------------------------------------------------------
/src/features/form/types/formContext.ts:
--------------------------------------------------------------------------------
1 | import { UseFormReturn } from "react-hook-form";
2 |
3 | type FormContext> = UseFormReturn & {
4 | readOnly: boolean;
5 | };
6 |
7 | export { type FormContext };
8 |
--------------------------------------------------------------------------------
/src/features/layout/components/dashboard-layout.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeToggle } from "@/features/layout/components/theme-toggle";
2 | import { useStore } from "@/features/layout/hooks/useStore";
3 | import { DRAWER_WIDTH } from "@/features/layout/utils/constants";
4 | import { d } from "@/utils/dictionary";
5 | import BadgeOutlinedIcon from "@mui/icons-material/BadgeOutlined";
6 | import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
7 | import MenuIcon from "@mui/icons-material/Menu";
8 | import { Container, Paper, Stack } from "@mui/material";
9 | import MuiAppBar from "@mui/material/AppBar";
10 | import Box from "@mui/material/Box";
11 | import Divider from "@mui/material/Divider";
12 | import Drawer from "@mui/material/Drawer";
13 | import IconButton from "@mui/material/IconButton";
14 | import List from "@mui/material/List";
15 | import ListItem from "@mui/material/ListItem";
16 | import ListItemButton from "@mui/material/ListItemButton";
17 | import ListItemIcon from "@mui/material/ListItemIcon";
18 | import ListItemText from "@mui/material/ListItemText";
19 | import { useTheme } from "@mui/material/styles";
20 | import Toolbar from "@mui/material/Toolbar";
21 | import Typography from "@mui/material/Typography";
22 | import { Outlet } from "react-router";
23 |
24 | const DashboardLayout = () => {
25 | const theme = useTheme();
26 |
27 | const { drawerOpen, updateDrawerOpen } = useStore();
28 |
29 | const handleDrawerOpen = () => {
30 | updateDrawerOpen(true);
31 | };
32 |
33 | const handleDrawerClose = () => {
34 | updateDrawerOpen(false);
35 | };
36 |
37 | return (
38 |
39 |
56 |
57 |
66 |
67 |
68 |
69 |
77 |
78 | {d.dashboard}
79 |
80 |
81 |
82 |
83 |
84 |
97 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | );
149 | };
150 |
151 | export { DashboardLayout };
152 |
--------------------------------------------------------------------------------
/src/features/layout/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Menu } from "@/features/form/components/controllers/menu";
2 | import { d } from "@/utils/dictionary";
3 | import ContrastOutlinedIcon from "@mui/icons-material/ContrastOutlined";
4 | import DarkModeOutlinedIcon from "@mui/icons-material/DarkModeOutlined";
5 | import WbSunnyOutlinedIcon from "@mui/icons-material/WbSunnyOutlined";
6 | import Typography from "@mui/material/Typography";
7 | import React from "react";
8 | import { FormProvider, useForm } from "react-hook-form";
9 |
10 | export interface Option {
11 | value: string | number;
12 | label: string;
13 | leftIcon?: React.ReactNode;
14 | rightIcon?: React.ReactNode;
15 | disabled?: boolean;
16 | }
17 |
18 | interface FormValues {
19 | selectedOption: string;
20 | }
21 |
22 | const menuOptions: Option[] = [
23 | {
24 | value: "system",
25 | label: d.system,
26 | leftIcon: ,
27 | },
28 | {
29 | value: "light",
30 | label: d.light,
31 | leftIcon: ,
32 | },
33 | {
34 | value: "dark",
35 | label: d.dark,
36 | leftIcon: ,
37 | },
38 | ];
39 |
40 | const ThemeToggle = () => {
41 | const methods = useForm({
42 | defaultValues: {
43 | selectedOption: "system",
44 | },
45 | });
46 |
47 | return (
48 |
49 |
58 |
59 | );
60 | };
61 |
62 | export { ThemeToggle };
63 |
--------------------------------------------------------------------------------
/src/features/layout/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from "@/utils/createStore";
2 |
3 | type State = {
4 | drawerOpen: boolean;
5 | };
6 |
7 | type Actions = {
8 | updateDrawerOpen: (data: State["drawerOpen"]) => void;
9 | };
10 |
11 | type Store = State & Actions;
12 |
13 | const useStore = createStore(
14 | (set) => ({
15 | drawerOpen: true,
16 | updateDrawerOpen: (is) =>
17 | set((state) => {
18 | state.drawerOpen = is;
19 | }),
20 | }),
21 | {
22 | name: "layout",
23 | }
24 | );
25 |
26 | export { useStore };
27 |
--------------------------------------------------------------------------------
/src/features/layout/utils/constants.ts:
--------------------------------------------------------------------------------
1 | const DRAWER_WIDTH = 240;
2 |
3 | export { DRAWER_WIDTH };
4 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { ConfirmProvider } from "@/features/confirm/components/provider";
2 | import { RoutesWrapper } from "@/routes";
3 | import { theme } from "@/utils/theme";
4 | import { setupZodErrors } from "@/utils/zodConfig";
5 | import { CssBaseline, ThemeProvider } from "@mui/material";
6 | import { LocalizationProvider } from "@mui/x-date-pickers";
7 | import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
8 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9 | import { SnackbarProvider } from "notistack";
10 | import { StrictMode } from "react";
11 | import { createRoot } from "react-dom/client";
12 |
13 | const queryClient = new QueryClient();
14 | setupZodErrors();
15 |
16 | createRoot(document.getElementById("root")!).render(
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import { EmployeeAdditionalInfo } from "@/features/employee/additional-info/page";
2 | import { EmployeeHistory } from "@/features/employee/history/page";
3 | import { EmployeePersonalInfo } from "@/features/employee/personal-info/page";
4 | import { EmployeeReview } from "@/features/employee/review/page";
5 | import { EmployeeSkills } from "@/features/employee/skills/page";
6 | import { EmployeeWrapper } from "@/features/employee/wrapper/page";
7 | import { DashboardLayout } from "@/features/layout/components/dashboard-layout";
8 | import { BrowserRouter, Route, Routes } from "react-router";
9 |
10 | const RoutesWrapper = () => {
11 | return (
12 |
13 |
14 | }>
15 | }>
16 | }
19 | />
20 | } />
21 | } />
22 | }
25 | />
26 | } />
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export { RoutesWrapper };
35 |
--------------------------------------------------------------------------------
/src/utils/calculatePastDate.ts:
--------------------------------------------------------------------------------
1 | const calculatePastDate = (years: number): Date => {
2 | const today = new Date();
3 | return new Date(
4 | today.getFullYear() - years,
5 | today.getMonth(),
6 | today.getDate()
7 | );
8 | };
9 |
10 | export { calculatePastDate };
11 |
--------------------------------------------------------------------------------
/src/utils/createStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { persist, createJSONStorage } from "zustand/middleware";
3 | import { immer } from "zustand/middleware/immer";
4 | import { StateCreator } from "zustand/vanilla";
5 |
6 | type ConfigType = {
7 | name?: string;
8 | storage?: Storage;
9 | skipPersist?: boolean;
10 | };
11 |
12 | const createStore = (
13 | storeCreator: StateCreator,
14 | config?: ConfigType
15 | ) => {
16 | const { name, storage, skipPersist = false } = config || {};
17 |
18 | const immerStore = immer(storeCreator);
19 |
20 | if (skipPersist) {
21 | return create()(immerStore);
22 | }
23 |
24 | return create()(
25 | persist(immerStore, {
26 | name: name || "zustand-store",
27 | storage: createJSONStorage(() => storage || localStorage),
28 | })
29 | );
30 | };
31 |
32 | export { createStore };
33 |
--------------------------------------------------------------------------------
/src/utils/dictionary.ts:
--------------------------------------------------------------------------------
1 | const d = {
2 | firstName: "First Name",
3 | lastName: "Last Name",
4 | dateOfBirth: "Date of Birth",
5 | email: "Email",
6 | phoneNumber: "Phone Number",
7 | socialSecurityNumber: "Social Security Number",
8 | state: "State",
9 | city: "City",
10 | streetAddress: "Street Address",
11 | nextStep: "Next Step",
12 | errorValidationTitle: "Please Check Your Entries",
13 | currentEmploymentStatus: "Current Employment Status",
14 | reasonsForLeavingPreviousJobs: "Reasons for Leaving Previous Jobs",
15 | otherReasonsForLeaving: "Other Reasons for Leaving",
16 | highestDegreeObtained: "Highest Degree Obtained",
17 | previousEmployers: "Previous Employers",
18 | employer: "Employer",
19 | responsibilities: "Responsibilities",
20 | name: "Name",
21 | jobTitle: "Job Title",
22 | educationalInstitutions: "Educational Institutions",
23 | institution: "Institution",
24 | degree: "Degree",
25 | fieldOfStudy: "Field of Study",
26 | institutionName: "Institution Name",
27 | employerName: "Employer Name",
28 | coreCompetencies: "Core Competencies",
29 | otherCoreCompetencies: "Other Core Competencies",
30 | languagesSpoken: "Languages Spoken",
31 | projectManagement: "Project Management",
32 | communication: "Communication",
33 | technicalSkills: "Technical Skills",
34 | leadership: "Leadership",
35 | problemSolving: "Problem Solving",
36 | proficiencyLevels: "Proficiency Levels",
37 | skillSets: "Skill Sets",
38 | skill: "Skill",
39 | category: "Category",
40 | subCategory: "Sub Category",
41 | skills: "Skills",
42 | yearsOfExperience: "Years of Experience",
43 | description: "Description",
44 | portfolioLink: "Portfolio Link",
45 | availabilityToStart: "Availability to Start",
46 | salaryExpectations: "Salary Expectations",
47 | references: "References",
48 | reference: "Reference",
49 | relationship: "Relationship",
50 | contactInformation: "Contact Information",
51 | submit: "Submit",
52 | form: "Form",
53 | personalInfo: "Personal Info",
54 | history: "History",
55 | additionalInfo: "Additional Info",
56 | review: "Review",
57 | graduationYear: "Graduation Year",
58 | resetForm: "Reset Form",
59 | iAcceptTermsAndConditions: "I accept the terms and conditions",
60 | uploadPortfolioFiles: "Upload Portfolio Files",
61 | uploadResume: "Upload Resume",
62 | confirmInformation: "Confirm Information",
63 | close: "Close",
64 | youMustAcceptTermsAndConditions: "You must accept terms and conditions",
65 | invalidFormData: "Invalid Form Data",
66 | defaultLoading: "Loading...",
67 | saveAndContinue: "Save and Continue",
68 | confirmAction: "Confirm Action",
69 | pleaseConfirmYouWouldLikeToProcess:
70 | "Please confirm you would like to proceed",
71 | ok: "Ok",
72 | cancel: "Cancel",
73 | uploadPDF: "Upload PDF",
74 | uploadZIP: "Upload ZIP",
75 | portfolio: "Portfolio",
76 | resume: "Resume",
77 | newEmployee: "New Employee",
78 | dashboard: "Dashboard",
79 | system: "System",
80 | dark: "Dark",
81 | light: "Light",
82 | } as const;
83 |
84 | export { d };
85 |
--------------------------------------------------------------------------------
/src/utils/formatErrors.ts:
--------------------------------------------------------------------------------
1 | import { d } from "@/utils/dictionary";
2 | import { FieldErrors } from "react-hook-form";
3 | import { humanizeFieldName } from "@/utils/humanizeFieldName";
4 |
5 | export type ErrorMessage = {
6 | field: string;
7 | label: string;
8 | message: string | undefined;
9 | category?: string;
10 | index?: number;
11 | };
12 |
13 | type ErrorValue = {
14 | message?: string;
15 | type?: string;
16 | ref?: unknown;
17 | } & Record;
18 |
19 | export const formatErrors = >(
20 | errors: FieldErrors
21 | ): ErrorMessage[] => {
22 | const formattedErrors: ErrorMessage[] = [];
23 |
24 | const processErrors = (
25 | obj: FieldErrors | ErrorValue | Array,
26 | parentField = "",
27 | parentLabel = ""
28 | ): void => {
29 | if (!obj || typeof obj !== "object") {
30 | return;
31 | }
32 |
33 | Object.entries(obj).forEach(([key, value]) => {
34 | if (!key || value === undefined) {
35 | return;
36 | }
37 |
38 | const currentField = parentField ? `${parentField}.${key}` : key;
39 | const isArrayField = currentField.includes("[");
40 | const arrayMatch = currentField.match(/\[(\d+)\]/);
41 | const arrayIndex = arrayMatch ? parseInt(arrayMatch[1]) : undefined;
42 |
43 | const categoryName = currentField.split("[")[0];
44 | const categoryLabel =
45 | d[categoryName as keyof typeof d] || humanizeFieldName(categoryName);
46 |
47 | if (Array.isArray(value)) {
48 | value.forEach((item, index) => {
49 | if (item) {
50 | processErrors(item, `${currentField}[${index}]`, categoryLabel);
51 | }
52 | });
53 | } else if (value && typeof value === "object") {
54 | const errorValue = value as ErrorValue;
55 | if (errorValue.message) {
56 | formattedErrors.push({
57 | field: currentField,
58 | label: d[key as keyof typeof d] || humanizeFieldName(key),
59 | message: errorValue.message,
60 | category: isArrayField ? categoryLabel : undefined,
61 | index: arrayIndex,
62 | });
63 | } else {
64 | processErrors(errorValue, currentField, parentLabel);
65 | }
66 | }
67 | });
68 | };
69 |
70 | try {
71 | processErrors(errors);
72 | } catch (error) {
73 | console.error("Error processing form errors:", error);
74 | }
75 |
76 | return formattedErrors.sort((a, b) => {
77 | if (a.category && b.category) {
78 | if (a.category !== b.category) {
79 | return a.category.localeCompare(b.category);
80 | }
81 | return (a.index || 0) - (b.index || 0);
82 | }
83 | if (a.category) return 1;
84 | if (b.category) return -1;
85 | return 0;
86 | });
87 | };
88 |
--------------------------------------------------------------------------------
/src/utils/getErrorMessage.ts:
--------------------------------------------------------------------------------
1 | import { humanizeFieldName } from "@/utils/humanizeFieldName";
2 | import { ZodError } from "zod";
3 |
4 | const getErrorMessage = (error: unknown): string => {
5 | let message: string;
6 |
7 | if (error instanceof ZodError) {
8 | message = error.errors
9 | .map((item) => `${humanizeFieldName(item.path[0])}: ${item.message}`)
10 | .join(", ");
11 | } else if (error instanceof Error) {
12 | message = error.message;
13 | } else if (error && typeof error === "object" && "message" in error) {
14 | message = String(error.message);
15 | } else if (typeof error === "string") {
16 | message = error;
17 | } else {
18 | message = "Unknown error";
19 | }
20 |
21 | return message;
22 | };
23 |
24 | export { getErrorMessage };
25 |
--------------------------------------------------------------------------------
/src/utils/humanizeFieldName.ts:
--------------------------------------------------------------------------------
1 | const humanizeFieldName = (field: string | number): string => {
2 | const str = field.toString();
3 |
4 | return str
5 | .split(/(?=[A-Z])/)
6 | .join(" ")
7 | .replace(/^./, (item) => item.toUpperCase());
8 | };
9 |
10 | export { humanizeFieldName };
11 |
--------------------------------------------------------------------------------
/src/utils/regex.ts:
--------------------------------------------------------------------------------
1 | const regex = {
2 | socialSecurityNumber: /^(?!000|666|9\d{2})\d{3}(?!00)\d{2}(?!0000)\d{4}$/,
3 | link: /^(https?:\/\/)?[\w-]+(\.[\w-]+)+[/#?]?.*$/,
4 | };
5 |
6 | export { regex };
7 |
--------------------------------------------------------------------------------
/src/utils/showSnack.tsx:
--------------------------------------------------------------------------------
1 | import { enqueueSnackbar, VariantType, OptionsObject } from "notistack";
2 |
3 | type ShowSnackOptions = Partial & {
4 | variant?: VariantType;
5 | duration?: number;
6 | };
7 |
8 | const showSnack = (message: string, options: ShowSnackOptions = {}) => {
9 | const defaultOptions: ShowSnackOptions = {
10 | variant: "success",
11 | duration: 3000,
12 | };
13 |
14 | return enqueueSnackbar(message, {
15 | ...defaultOptions,
16 | ...options,
17 | });
18 | };
19 |
20 | export { showSnack };
21 |
--------------------------------------------------------------------------------
/src/utils/theme.ts:
--------------------------------------------------------------------------------
1 | import { Link } from "@/components/link";
2 | import { createTheme } from "@mui/material";
3 |
4 | const theme = createTheme({
5 | colorSchemes: {
6 | dark: true,
7 | },
8 | components: {
9 | MuiLink: {
10 | defaultProps: {
11 | component: Link,
12 | },
13 | },
14 | MuiTextField: {
15 | defaultProps: {
16 | slotProps: {
17 | inputLabel: {
18 | shrink: true,
19 | },
20 | },
21 | },
22 | },
23 | MuiButtonBase: {
24 | defaultProps: {
25 | LinkComponent: Link,
26 | },
27 | },
28 | },
29 | });
30 |
31 | export { theme };
32 |
--------------------------------------------------------------------------------
/src/utils/wait.ts:
--------------------------------------------------------------------------------
1 | const wait = async () =>
2 | await new Promise((resolve) =>
3 | setTimeout(resolve, Math.floor(Math.random() * (2000 - 500 + 1)) + 500)
4 | );
5 |
6 | export { wait };
7 |
--------------------------------------------------------------------------------
/src/utils/zodConfig.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const formatDate = (date: Date): string => {
4 | return date.toLocaleDateString(undefined, {
5 | year: "numeric",
6 | month: "long",
7 | day: "numeric",
8 | });
9 | };
10 |
11 | const setupZodErrors = () => {
12 | z.setErrorMap((issue, ctx) => {
13 | let message: string;
14 |
15 | switch (issue.code) {
16 | case "invalid_type":
17 | if (issue.received === "undefined" || issue.received === "null") {
18 | message = "Required";
19 | } else if (issue.expected === "date") {
20 | message = "Please enter a valid date";
21 | } else {
22 | message = "Invalid input";
23 | }
24 | break;
25 |
26 | case "too_small":
27 | if (issue.type === "date") {
28 | const minDate = new Date(issue.minimum as number);
29 | message = `Date must be after ${formatDate(minDate)}`;
30 | } else if (issue.minimum === 1) {
31 | message = "Required";
32 | } else if (issue.type === "array") {
33 | message = "At least one item is required";
34 | } else {
35 | message = `Minimum ${issue.minimum} characters`;
36 | }
37 | break;
38 |
39 | case "too_big":
40 | if (issue.type === "date") {
41 | if (
42 | issue.maximum &&
43 | typeof issue.maximum === "object" &&
44 | (issue.maximum as Date).getTime() ===
45 | new Date().setHours(0, 0, 0, 0)
46 | ) {
47 | message = "Date cannot be in the future";
48 | } else {
49 | const maxDate = new Date(issue.maximum as number);
50 | message = `Date must be before ${formatDate(maxDate)}`;
51 | }
52 | } else if (issue.type === "string") {
53 | message = `Maximum ${issue.maximum} characters allowed`;
54 | } else {
55 | message = ctx.defaultError;
56 | }
57 | break;
58 |
59 | case "invalid_date":
60 | message = "Please enter a valid date";
61 | break;
62 |
63 | case "invalid_string":
64 | if (issue.validation === "email") {
65 | message = ctx.data === "" ? "Required" : "Invalid email";
66 | } else {
67 | message = "Invalid input";
68 | }
69 | break;
70 |
71 | default:
72 | message = ctx.defaultError;
73 | }
74 |
75 | return { message };
76 | });
77 | };
78 |
79 | export { setupZodErrors };
80 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 | "paths": {
18 | "@/*": ["./src/*"]
19 | },
20 | "noErrorTruncation": true,
21 |
22 | /* Linting */
23 | "strict": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": true,
26 | "noFallthroughCasesInSwitch": true,
27 | "noUncheckedSideEffectImports": true
28 | },
29 | "include": ["src"]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react-swc";
3 | import path from "path";
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | "@": path.resolve("./src"),
10 | $: path.resolve("."),
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------