├── .gitignore ├── client ├── debug.log ├── public │ ├── bg.jpg │ ├── favicon.ico │ ├── webfonts │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.ttf │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.ttf │ │ ├── fa-solid-900.woff │ │ ├── fa-solid-900.woff2 │ │ ├── fa-brands-400.woff2 │ │ ├── fa-brands-400d41d.eot │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900d41d.eot │ │ └── fa-regular-400d41d.eot │ ├── fonts │ │ ├── nunito-v9-latin-600.eot │ │ ├── nunito-v9-latin-600.ttf │ │ ├── nunito-v9-latin-600.woff │ │ ├── nunito-v9-latin-700.eot │ │ ├── nunito-v9-latin-700.ttf │ │ ├── nunito-v9-latin-700.woff │ │ ├── nunito-v9-latin-800.eot │ │ ├── nunito-v9-latin-800.ttf │ │ ├── nunito-v9-latin-800.woff │ │ ├── nunito-v9-latin-600.woff2 │ │ ├── nunito-v9-latin-700.woff2 │ │ ├── nunito-v9-latin-800.woff2 │ │ ├── nunito-v9-latin-600d41d.eot │ │ ├── nunito-v9-latin-700d41d.eot │ │ ├── nunito-v9-latin-800d41d.eot │ │ ├── nunito-v9-latin-regular.eot │ │ ├── nunito-v9-latin-regular.ttf │ │ ├── nunito-v9-latin-regular.woff │ │ ├── nunito-v9-latin-regular.woff2 │ │ └── nunito-v9-latin-regulard41d.eot │ ├── manifest.json │ ├── index.html │ └── js │ │ └── tooltip.js ├── src │ ├── components │ │ ├── dashboard │ │ │ ├── avatar.png │ │ │ ├── settings.jpg │ │ │ ├── Footer.js │ │ │ ├── AdminCard.js │ │ │ ├── OneoffCard.js │ │ │ ├── LevelCard.js │ │ │ ├── EmployeeCard.js │ │ │ ├── ExceptionCard.js │ │ │ ├── PayslipCard.js │ │ │ ├── DeletedEmployeeCard.js │ │ │ ├── OtherPays.js │ │ │ ├── SalaryPay.js │ │ │ ├── EmployeeRow.js │ │ │ ├── Netpay.js │ │ │ ├── SideBar.js │ │ │ ├── Dashboard.js │ │ │ └── SearchBar.js │ │ ├── common │ │ │ ├── Spinner.js │ │ │ ├── Button.js │ │ │ ├── PrivateRoute.js │ │ │ ├── TextFieldGroup.js │ │ │ ├── SelectListGroup.js │ │ │ ├── Pagination.js │ │ │ ├── Utilities.js │ │ │ └── Spinner.svg │ │ ├── payroll │ │ │ ├── all │ │ │ │ ├── links │ │ │ │ │ ├── Year.js │ │ │ │ │ ├── MonthlyTax.js │ │ │ │ │ ├── MonthYear.js │ │ │ │ │ ├── MonthlyPension.js │ │ │ │ │ ├── MonthlyPayroll.js │ │ │ │ │ ├── YearlyAllEmployee.js │ │ │ │ │ ├── YearlyMonthlyslip.js │ │ │ │ │ ├── YearlySingleEmployee.js │ │ │ │ │ └── EmployeeMonthYear.js │ │ │ │ ├── records │ │ │ │ │ ├── AllEmployee.js │ │ │ │ │ └── AllTimeYear.js │ │ │ │ ├── MonthlyDashboard.js │ │ │ │ └── Contribution.js │ │ │ ├── MonthlySalary.js │ │ │ └── MonthlySalaryTable.js │ │ ├── employee │ │ │ └── ViewEmployee.js │ │ ├── exception │ │ │ ├── ViewException.js │ │ │ ├── ExceptionTab.js │ │ │ ├── ViewOtherException.js │ │ │ ├── Viewoneofpayment.js │ │ │ ├── AddExceptionForm.js │ │ │ ├── AddOtherExceptionForm.js │ │ │ └── Addoneoffpayment.js │ │ ├── level │ │ │ ├── LevelTab.js │ │ │ ├── ViewBonusTable.js │ │ │ ├── ViewDeductableTable.js │ │ │ ├── AddLevelForm.js │ │ │ ├── AddBonusForm.js │ │ │ ├── Level.js │ │ │ └── AddDeductableForm.js │ │ └── auth │ │ │ ├── Forgot.js │ │ │ └── Login.js │ ├── validation │ │ └── is-empty.js │ ├── utils │ │ └── setAuthToken.js │ ├── store.js │ ├── reducers │ │ ├── errorReducer.js │ │ ├── index.js │ │ ├── dashReducer.js │ │ ├── levelReducer.js │ │ ├── employeeReducer.js │ │ ├── authReducer.js │ │ ├── payrollReducer.js │ │ └── exceptionReducer.js │ ├── index.css │ ├── index.js │ └── actions │ │ ├── dashActions.js │ │ ├── types.js │ │ ├── employeeActions.js │ │ ├── payrollActions.js │ │ └── levelActions.js ├── .gitignore ├── package.json └── README.md ├── fonts ├── Roboto-Italic.ttf ├── Roboto-Medium.ttf ├── Roboto-Regular.ttf └── Roboto-MediumItalic.ttf ├── docs └── Employee_Record_Template.xlsx ├── config ├── keys.js ├── keys_prod.js └── passport.js ├── validation ├── is-empty.js ├── reset.js ├── monthyear.js ├── role.js ├── monthlyrecord.js ├── login.js ├── bonus.js ├── deductable.js ├── employeemonthyear.js ├── level.js ├── newpassword.js ├── individualcost.js ├── oneoffpayment.js ├── register.js └── employee.js ├── models ├── Exception.js ├── Individualcost.js ├── User.js ├── Oneoffpayment.js ├── Level.js ├── Employee.js └── Payslip.js ├── package.json ├── utils └── createSuperAdmin.js ├── server.js ├── routes └── api │ ├── individualcost.js │ ├── oneoffpayment.js │ └── record.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /config/keys_dev.js 3 | /docs/payroll.pdf -------------------------------------------------------------------------------- /client/debug.log: -------------------------------------------------------------------------------- 1 | [0401/144806.291:ERROR:file_io.cc(89)] ReadExactly: expected 36, observed 0 2 | -------------------------------------------------------------------------------- /client/public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/bg.jpg -------------------------------------------------------------------------------- /fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /docs/Employee_Record_Template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/docs/Employee_Record_Template.xlsx -------------------------------------------------------------------------------- /client/public/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /client/public/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /client/public/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /client/public/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /client/public/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /client/public/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /client/public/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /client/public/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /client/public/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-600.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-600.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-600.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-600.ttf -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-600.woff -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-700.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-700.ttf -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-700.woff -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-800.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-800.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-800.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-800.ttf -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-800.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-800.woff -------------------------------------------------------------------------------- /client/public/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /client/public/webfonts/fa-brands-400d41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-brands-400d41d.eot -------------------------------------------------------------------------------- /client/public/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /client/public/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /client/public/webfonts/fa-solid-900d41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-solid-900d41d.eot -------------------------------------------------------------------------------- /client/src/components/dashboard/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/src/components/dashboard/avatar.png -------------------------------------------------------------------------------- /client/src/components/dashboard/settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/src/components/dashboard/settings.jpg -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-600.woff2 -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-700.woff2 -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-800.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-800.woff2 -------------------------------------------------------------------------------- /client/public/webfonts/fa-regular-400d41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/webfonts/fa-regular-400d41d.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-600d41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-600d41d.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-700d41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-700d41d.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-800d41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-800d41d.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-regular.eot -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-regular.ttf -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-regular.woff -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-regular.woff2 -------------------------------------------------------------------------------- /client/public/fonts/nunito-v9-latin-regulard41d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akinlekan28/payroll/HEAD/client/public/fonts/nunito-v9-latin-regulard41d.eot -------------------------------------------------------------------------------- /config/keys.js: -------------------------------------------------------------------------------- 1 | if(process.env.NODE_ENV === 'production'){ 2 | module.exports = require('./keys_prod'); 3 | } else{ 4 | module.exports = require('./keys_dev'); 5 | } -------------------------------------------------------------------------------- /client/src/validation/is-empty.js: -------------------------------------------------------------------------------- 1 | const isEmpty = value => 2 | value === undefined || 3 | value === null || 4 | (typeof value === 'object' && Object.keys(value).length === 0) || 5 | (typeof value === 'string' && value.trim().length === 0); 6 | 7 | export default isEmpty; 8 | -------------------------------------------------------------------------------- /validation/is-empty.js: -------------------------------------------------------------------------------- 1 | const isEmpty = value => 2 | value === undefined || 3 | value === null || 4 | (typeof value === 'object' && Object.keys(value).length === 0) || 5 | (typeof value === 'string' && value.trim().length === 0); 6 | 7 | module.exports = isEmpty; 8 | -------------------------------------------------------------------------------- /client/src/components/common/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import spinner from './Spinner.svg'; 3 | 4 | export default () => { 5 | return ( 6 |
7 | loading 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /client/src/components/dashboard/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = () => ( 4 | 10 | ); 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /client/src/utils/setAuthToken.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const setAuthToken = token => { 4 | if (token) { 5 | //apply to every request 6 | axios.defaults.headers.common['Authorization'] = token; 7 | } else { 8 | //Delete auth header 9 | delete axios.defaults.headers.common['Authorization']; 10 | } 11 | } 12 | 13 | export default setAuthToken; -------------------------------------------------------------------------------- /client/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import rootReducer from "./reducers"; 4 | 5 | const initialState = {}; 6 | 7 | const middleware = [thunk]; 8 | 9 | const store = createStore( 10 | rootReducer, 11 | initialState, 12 | compose(applyMiddleware(...middleware)) 13 | ); 14 | 15 | export default store; -------------------------------------------------------------------------------- /config/keys_prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoURI: process.env.MONGO_URI, //Mongodb uri link 3 | secretKey: process.env.SECRET_OR_KEY, //Secret key for passportjs auth 4 | sendGridKey: process.env.SENDGRID_KEY, //Sendgrid key for password reset email 5 | name: process.env.NAME, //Full name for super user registration 6 | email: process.env.EMAIL //Email for super user registration 7 | }; 8 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/reducers/errorReducer.js: -------------------------------------------------------------------------------- 1 | import { GET_ERRORS, CLEAR_ERRORS } from '../actions/types'; 2 | 3 | const initialState = {}; 4 | 5 | export default function (state = initialState, action) { 6 | switch (action.type) { 7 | 8 | case GET_ERRORS: 9 | return action.payload; 10 | 11 | case CLEAR_ERRORS: 12 | return {}; 13 | 14 | default: 15 | return state; 16 | } 17 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/Year.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const Year = () => ( 5 |
6 |
7 | 8 | Yearly Employee Records 9 | 10 |
11 |
12 | ); 13 | 14 | export default Year; 15 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/MonthlyTax.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const MonthlyTax = () => ( 5 |
6 |
7 | 8 | Employees Tax 9 | 10 |
11 |
12 | ); 13 | 14 | export default MonthlyTax; 15 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/MonthYear.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const MonthYear = () => ( 5 |
6 |
7 | 8 | Monthly/Yearly Employee Records 9 | 10 |
11 |
12 | ); 13 | 14 | export default MonthYear; 15 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/MonthlyPension.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const MonthlyPension = () => ( 5 |
6 |
7 | 8 | Employees pension 9 | 10 |
11 |
12 | ); 13 | 14 | export default MonthlyPension; 15 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/MonthlyPayroll.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const MonthlyPayroll = () => ( 5 |
6 |
7 | 8 | Payroll with pension 9 | 10 |
11 |
12 | ); 13 | 14 | export default MonthlyPayroll; 15 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/YearlyAllEmployee.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const YearlyAllEmployee = () => ( 5 |
6 |
7 | 8 | All employee Records 9 | 10 |
11 |
12 | ); 13 | 14 | export default YearlyAllEmployee; 15 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/YearlyMonthlyslip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const YearlyMonthlyslip = () => ( 5 |
6 |
7 | 8 | Monthly employee Records 9 | 10 |
11 |
12 | ); 13 | 14 | export default YearlyMonthlyslip; 15 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/YearlySingleEmployee.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const YearlySingleEmployee = () => ( 5 |
6 |
7 | 8 | Single employee Records 9 | 10 |
11 |
12 | ); 13 | 14 | export default YearlySingleEmployee; 15 | -------------------------------------------------------------------------------- /models/Exception.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const ExceptionSchema = new Schema({ 5 | amount: { 6 | type: Number, 7 | required: true 8 | }, 9 | employee: { 10 | type: Schema.Types.ObjectId, 11 | ref: "employee" 12 | }, 13 | name: { 14 | type: String 15 | }, 16 | is_delete: { 17 | type: Number, 18 | default: 0 19 | }, 20 | }); 21 | 22 | module.exports = Exception = mongoose.model("exception", ExceptionSchema); 23 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/links/EmployeeMonthYear.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const EmployeeMonthYear = () => ( 5 |
6 |
7 | 11 | Monthly Employee Records 12 | 13 |
14 |
15 | ); 16 | 17 | export default EmployeeMonthYear; 18 | -------------------------------------------------------------------------------- /validation/reset.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function validateResetInput(data) { 5 | let errors = {}; 6 | 7 | data.email = !isEmpty(data.email) ? data.email : ''; 8 | 9 | if (validator.isEmpty(data.email)) { 10 | errors.email = 'Email field is required' 11 | } 12 | 13 | if (!validator.isEmail(data.email)) { 14 | errors.email = 'Email is invalid' 15 | } 16 | 17 | return { 18 | errors, 19 | isValid: isEmpty(errors) 20 | }; 21 | }; -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render(); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: http://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /client/src/components/common/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classnames from "classnames"; 4 | 5 | const Button = ({ type, classnameItems, btnName, onClick }) => ( 6 | 14 | ); 15 | 16 | Button.propTypes = { 17 | btnName: PropTypes.string.isRequired 18 | }; 19 | 20 | Button.defaultProps = { 21 | type: "button" 22 | }; 23 | 24 | export default Button; 25 | -------------------------------------------------------------------------------- /validation/monthyear.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function monthYear(data) { 5 | let errors = {}; 6 | 7 | data.month = !isEmpty(data.month) ? data.month : ''; 8 | data.year = !isEmpty(data.year) ? data.year : ''; 9 | 10 | if (validator.isEmpty(data.month)) { 11 | errors.month = 'Month field is required'; 12 | } 13 | 14 | if (validator.isEmpty(data.year)) { 15 | errors.year = 'Year field is required'; 16 | } 17 | 18 | return { 19 | errors, 20 | isValid: isEmpty(errors) 21 | }; 22 | }; -------------------------------------------------------------------------------- /validation/role.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function validateRoleInput(data) { 5 | let errors = {}; 6 | 7 | data.user = !isEmpty(data.user) ? data.user : ''; 8 | data.role = !isEmpty(data.role) ? data.role : ''; 9 | 10 | if (validator.isEmpty(data.user)) { 11 | errors.user = 'Administrator field is required' 12 | } 13 | 14 | if (validator.isEmpty(data.role)) { 15 | errors.role = 'Role field is required' 16 | } 17 | 18 | return { 19 | errors, 20 | isValid: isEmpty(errors) 21 | }; 22 | }; -------------------------------------------------------------------------------- /client/src/components/dashboard/AdminCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AdminCard = ({ dashboard }) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

Active Admins

12 |
13 |
{dashboard.adminCount}
14 |
15 |
16 |
17 | ); 18 | 19 | export default AdminCard; 20 | -------------------------------------------------------------------------------- /validation/monthlyrecord.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function monthlyRecord(data) { 5 | let errors = {}; 6 | 7 | data.employee = !isEmpty(data.employee) ? data.employee : ''; 8 | data.month = !isEmpty(data.month) ? data.month : ''; 9 | 10 | if(validator.isEmpty(data.employee)){ 11 | errors.employee = 'Employee field is required'; 12 | } 13 | 14 | if (validator.isEmpty(data.month)) { 15 | errors.month = 'Month field is required'; 16 | } 17 | 18 | return { 19 | errors, 20 | isValid: isEmpty(errors) 21 | }; 22 | }; -------------------------------------------------------------------------------- /client/src/components/dashboard/OneoffCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const OneoffCard = ({ dashboard }) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

One-off Payments

12 |
13 |
{dashboard.oneOff}
14 |
15 |
16 |
17 | ); 18 | 19 | export default OneoffCard; 20 | -------------------------------------------------------------------------------- /client/src/components/dashboard/LevelCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LevelCard = ({ dashboard }) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

Employee Levels

12 |
13 |
{dashboard.levelCount}
14 |
15 |
16 |
17 | ); 18 | 19 | export default LevelCard; 20 | -------------------------------------------------------------------------------- /client/src/components/dashboard/EmployeeCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const EmployeeCard = ({ dashboard }) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

Active Employees

12 |
13 |
{dashboard.employeeCount}
14 |
15 |
16 |
17 | ); 18 | 19 | export default EmployeeCard; 20 | -------------------------------------------------------------------------------- /client/src/components/dashboard/ExceptionCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ExceptionCard = ({ dashboard }) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

Salary Exceptions

12 |
13 |
{dashboard.exceptionCount}
14 |
15 |
16 |
17 | ); 18 | 19 | export default ExceptionCard; 20 | -------------------------------------------------------------------------------- /client/src/components/dashboard/PayslipCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PayslipCard = ({ dashboard }) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

Monthly Generated Payslip

12 |
13 |
{dashboard.payslipCount}
14 |
15 |
16 |
17 | ); 18 | 19 | export default PayslipCard; 20 | -------------------------------------------------------------------------------- /client/src/components/dashboard/DeletedEmployeeCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DeletedEmployeeCard = ({ dashboard }) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

Deleted Employees

12 |
13 |
{dashboard.deletedEmployees}
14 |
15 |
16 |
17 | ); 18 | 19 | export default DeletedEmployeeCard; 20 | -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import authReducer from './authReducer'; 3 | import errorReducer from './errorReducer'; 4 | import dashReducer from './dashReducer'; 5 | import levelReducer from './levelReducer'; 6 | import employeeReducer from './employeeReducer'; 7 | import exceptionReducer from './exceptionReducer'; 8 | import payrollReducer from './payrollReducer'; 9 | 10 | export default combineReducers({ 11 | auth: authReducer, 12 | errors: errorReducer, 13 | dashboard: dashReducer, 14 | levels: levelReducer, 15 | employees: employeeReducer, 16 | exceptions: exceptionReducer, 17 | payroll: payrollReducer 18 | }); -------------------------------------------------------------------------------- /models/Individualcost.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const IndividualcostSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true 8 | }, 9 | costType: { 10 | type: String, 11 | required: true 12 | }, 13 | amount: { 14 | type: Number, 15 | required: true 16 | }, 17 | employee: { 18 | type: Schema.Types.ObjectId, 19 | ref: "employee" 20 | }, 21 | employeeName: { 22 | type: String 23 | }, 24 | is_delete: { 25 | type: Number, 26 | default: 0 27 | } 28 | }); 29 | 30 | module.exports = Individualcost = mongoose.model( 31 | "individualcost", 32 | IndividualcostSchema 33 | ); 34 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const UserSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true 8 | }, 9 | email: { 10 | type: String, 11 | required: true 12 | }, 13 | password: { 14 | type: String, 15 | required: true 16 | }, 17 | date: { 18 | type: Date, 19 | default: Date.now() 20 | }, 21 | token: { 22 | type: String 23 | }, 24 | expiry: { 25 | type: Date 26 | }, 27 | is_admin: { 28 | type: Number, 29 | default: 0 30 | }, 31 | is_delete: { 32 | type: Number, 33 | default: 0 34 | } 35 | }); 36 | 37 | module.exports = User = mongoose.model('users', UserSchema); -------------------------------------------------------------------------------- /validation/login.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function validateLoginInput(data) { 5 | let errors = {}; 6 | 7 | data.email = !isEmpty(data.email) ? data.email : ''; 8 | data.password = !isEmpty(data.password) ? data.password : ''; 9 | 10 | if (!validator.isEmail(data.email)) { 11 | errors.email = 'Email is invalid' 12 | } 13 | 14 | if (validator.isEmpty(data.email)) { 15 | errors.email = 'Email field is required' 16 | } 17 | 18 | if (validator.isEmpty(data.password)) { 19 | errors.password = 'Password field is required' 20 | } 21 | 22 | return { 23 | errors, 24 | isValid: isEmpty(errors) 25 | }; 26 | }; -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const JwtStrategy = require('passport-jwt').Strategy; 2 | const ExtractJwt = require('passport-jwt').ExtractJwt; 3 | const mongoose = require('mongoose'); 4 | const User = mongoose.model('users'); 5 | const keys = require('./keys') 6 | 7 | const opts = {}; 8 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); 9 | opts.secretOrKey = keys.secretKey; 10 | 11 | module.exports = passport => { 12 | passport.use( 13 | new JwtStrategy(opts, (jwt_payload, done) => { 14 | User.findById(jwt_payload.id) 15 | .then(user => { 16 | if(user){ 17 | return done(null, user); 18 | } 19 | return(null, false) 20 | }) 21 | .catch(err => console.log(err)); 22 | })) 23 | }; -------------------------------------------------------------------------------- /client/src/components/common/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | // import PropTypes from 'prop-types'; 5 | 6 | const PrivateRoute = ({ component: Component, auth, ...rest }) => ( 7 | 10 | auth.isAuthenticated === true ? ( 11 | 12 | ) : ( 13 | 14 | ) 15 | } 16 | /> 17 | ); 18 | 19 | // PrivateRoute.proptypes = { 20 | // auth: PropTypes.object.isRequired 21 | // } 22 | 23 | const mapStateToProps = (state) => ({ 24 | auth: state.auth, 25 | }); 26 | 27 | export default connect(mapStateToProps)(PrivateRoute); 28 | -------------------------------------------------------------------------------- /validation/bonus.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function bonusInput(data) { 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ''; 8 | data.amount = !isEmpty(data.amount) ? data.amount : ''; 9 | data.level = !isEmpty(data.level) ? data.level : ''; 10 | 11 | if(validator.isEmpty(data.name)){ 12 | errors.name = 'Bonus name is required'; 13 | } 14 | 15 | if (validator.isEmpty(data.amount)) { 16 | errors.amount = 'Amount field is required'; 17 | } 18 | 19 | if (validator.isEmpty(data.level)) { 20 | errors.level = 'Level field is required'; 21 | } 22 | 23 | return { 24 | errors, 25 | isValid: isEmpty(errors) 26 | }; 27 | }; -------------------------------------------------------------------------------- /validation/deductable.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function deductableInput(data) { 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ''; 8 | data.amount = !isEmpty(data.amount) ? data.amount : ''; 9 | data.level = !isEmpty(data.level) ? data.level : ''; 10 | 11 | if(validator.isEmpty(data.name)){ 12 | errors.name = 'Deductable name is required'; 13 | } 14 | 15 | if (validator.isEmpty(data.amount)) { 16 | errors.amount = 'Amount field is required'; 17 | } 18 | 19 | if (validator.isEmpty(data.level)) { 20 | errors.level = 'Level field is required'; 21 | } 22 | 23 | return { 24 | errors, 25 | isValid: isEmpty(errors) 26 | }; 27 | }; -------------------------------------------------------------------------------- /client/src/reducers/dashReducer.js: -------------------------------------------------------------------------------- 1 | import { VIEW_ANALYTICS, ANALYTICS_LOADING, VIEW_NET } from "../actions/types"; 2 | 3 | const initialState = { 4 | dashboard: {}, 5 | net: {}, 6 | loading: false 7 | }; 8 | 9 | export default function(state = initialState, action) { 10 | switch (action.type) { 11 | case VIEW_ANALYTICS: 12 | return { 13 | ...state, 14 | dashboard: action.payload, 15 | loading: false 16 | }; 17 | 18 | case VIEW_NET: 19 | return { 20 | ...state, 21 | net: action.payload, 22 | loading: false 23 | }; 24 | 25 | case ANALYTICS_LOADING: 26 | return { 27 | ...state, 28 | loading: true 29 | }; 30 | 31 | default: 32 | return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /validation/employeemonthyear.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function employeeMonthYear(data) { 5 | let errors = {}; 6 | 7 | data.employee = !isEmpty(data.employee) ? data.employee : ''; 8 | data.month = !isEmpty(data.month) ? data.month : ''; 9 | data.year = !isEmpty(data.year) ? data.year : ''; 10 | 11 | if(validator.isEmpty(data.employee)){ 12 | errors.employee = 'Employee field is required'; 13 | } 14 | 15 | if (validator.isEmpty(data.month)) { 16 | errors.month = 'Month field is required'; 17 | } 18 | 19 | if (validator.isEmpty(data.year)) { 20 | errors.year = 'Year field is required'; 21 | } 22 | 23 | return { 24 | errors, 25 | isValid: isEmpty(errors) 26 | }; 27 | }; -------------------------------------------------------------------------------- /validation/level.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function levelInput(data) { 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ''; 8 | data.basic = !isEmpty(data.basic) ? data.basic : ''; 9 | data.description = !isEmpty(data.description) ? data.description : ''; 10 | 11 | if(validator.isEmpty(data.name)){ 12 | errors.name = 'Level name is required'; 13 | } 14 | 15 | if (validator.isEmpty(data.basic)) { 16 | errors.basic = 'Basic salary field is required'; 17 | } 18 | 19 | if (validator.isEmpty(data.description)) { 20 | errors.description = 'Level description field is required'; 21 | } 22 | 23 | return { 24 | errors, 25 | isValid: isEmpty(errors) 26 | }; 27 | }; -------------------------------------------------------------------------------- /models/Oneoffpayment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const OneoffSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true 8 | }, 9 | amount: { 10 | type: Number, 11 | required: true 12 | }, 13 | month: { 14 | type: String, 15 | required: true 16 | }, 17 | costType: { 18 | type: String, 19 | required: true 20 | }, 21 | employeeName: { 22 | type: String 23 | }, 24 | employee: { 25 | type: Schema.Types.ObjectId, 26 | ref: "employee" 27 | }, 28 | is_delete: { 29 | type: Number, 30 | default: 0 31 | }, 32 | date_added: { 33 | type: Date, 34 | default: Date.now() 35 | } 36 | }); 37 | 38 | module.exports = Oneoffpayment = mongoose.model("oneoff", OneoffSchema); 39 | -------------------------------------------------------------------------------- /validation/newpassword.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function validateNewInput(data) { 5 | let errors = {}; 6 | 7 | data.password = !isEmpty(data.password) ? data.password : ''; 8 | data.password2 = !isEmpty(data.password2) ? data.password2 : ''; 9 | 10 | if (validator.isEmpty(data.password)) { 11 | errors.password = 'Password field is required' 12 | } 13 | 14 | if (!validator.isLength(data.password, { min: 6, max: 30 })) { 15 | errors.password = 'Password must be atleast 6 characters' 16 | } 17 | 18 | if (validator.isEmpty(data.password2)) { 19 | errors.password2 = 'Confirm Password field is required' 20 | } 21 | 22 | if (!validator.equals(data.password, data.password2)) { 23 | errors.password2 = 'Passwords must match' 24 | } 25 | 26 | return { 27 | errors, 28 | isValid: isEmpty(errors) 29 | }; 30 | }; -------------------------------------------------------------------------------- /client/src/components/dashboard/OtherPays.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Pie } from "react-chartjs-2"; 3 | 4 | const OtherPays = ({ tax, pension, deduction }) => { 5 | const data = { 6 | labels: ["Tax", "Pension", "Other Deductions"], 7 | datasets: [ 8 | { 9 | data: [tax, pension, deduction], 10 | backgroundColor: ["#fc544b", "#FFA426", "#63ED7A"] 11 | } 12 | ] 13 | }; 14 | 15 | const options = { 16 | tooltips: { 17 | bodyFontSize: 21 18 | } 19 | }; 20 | 21 | return ( 22 |
23 |
24 |
25 | 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default OtherPays; 38 | -------------------------------------------------------------------------------- /models/Level.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | //Create Schema 5 | const LevelSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | description: { 11 | type: String 12 | }, 13 | basic: { 14 | type: Number, 15 | required: true 16 | }, 17 | bonuses: [ 18 | { 19 | name: { 20 | type: String, 21 | required: true 22 | }, 23 | amount: { 24 | type: Number, 25 | required: true 26 | } 27 | } 28 | ], 29 | deductables: [ 30 | { 31 | name: { 32 | type: String, 33 | required: true 34 | }, 35 | amount: { 36 | type: Number, 37 | required: true 38 | } 39 | } 40 | ], 41 | is_delete: { 42 | type: Number, 43 | default: 0 44 | } 45 | }); 46 | 47 | module.exports = Level = mongoose.model("level", LevelSchema); 48 | -------------------------------------------------------------------------------- /client/src/components/dashboard/SalaryPay.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Doughnut } from "react-chartjs-2"; 3 | 4 | const SalaryPay = ({ net, cra, bonus }) => { 5 | const data = { 6 | labels: ["Net Pay", "Consolidation Relief Allowance", "Bonuses"], 7 | datasets: [ 8 | { 9 | data: [net, cra, bonus], 10 | backgroundColor: ["#3ECACA", "#808080", "#4a4a4a"] 11 | } 12 | ] 13 | }; 14 | 15 | const options = { 16 | tooltips: { 17 | bodyFontSize: 21 18 | } 19 | }; 20 | 21 | return ( 22 |
23 |
24 |
25 | 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default SalaryPay; 38 | -------------------------------------------------------------------------------- /validation/individualcost.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function individualcostInput(data) { 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ''; 8 | data.amount = !isEmpty(data.amount) ? data.amount : ''; 9 | data.costType = !isEmpty(data.costType) ? data.costType : ''; 10 | data.employee = !isEmpty(data.employee) ? data.employee : ''; 11 | 12 | if(validator.isEmpty(data.name)){ 13 | errors.name = 'Exception name is required'; 14 | } 15 | 16 | if (validator.isEmpty(data.amount)) { 17 | errors.amount = 'Amount field is required'; 18 | } 19 | 20 | if (validator.isEmpty(data.costType)) { 21 | errors.costType = 'Exception type field is required'; 22 | } 23 | 24 | if (validator.isEmpty(data.employee)) { 25 | errors.employee = 'Employee field is required'; 26 | } 27 | 28 | return { 29 | errors, 30 | isValid: isEmpty(errors) 31 | }; 32 | }; -------------------------------------------------------------------------------- /client/src/reducers/levelReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | VIEW_LEVELS, 3 | LEVELS_LOADING, 4 | ADD_LEVEL, 5 | DELETE_LEVEL 6 | } from "../actions/types"; 7 | 8 | const initialState = { 9 | levels: [], 10 | loading: false 11 | }; 12 | 13 | export default function(state = initialState, action) { 14 | switch (action.type) { 15 | 16 | default: 17 | return state; 18 | 19 | case VIEW_LEVELS: 20 | return { 21 | ...state, 22 | levels: action.payload, 23 | loading: false 24 | }; 25 | 26 | case ADD_LEVEL: 27 | return { 28 | ...state, 29 | levels: [action.payload, ...state.levels], 30 | loading: false 31 | }; 32 | 33 | case DELETE_LEVEL: 34 | return { 35 | ...state, 36 | levels: state.levels.filter(level => level._id !== action.payload), 37 | loading: false 38 | }; 39 | 40 | case LEVELS_LOADING: 41 | return { 42 | ...state, 43 | loading: true 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/actions/dashActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | VIEW_ANALYTICS, 4 | VIEW_NET, 5 | ANALYTICS_LOADING, 6 | GET_ERRORS 7 | } from "./types"; 8 | 9 | //Get analytics 10 | export const getAnalytics = () => dispatch => { 11 | dispatch(setAnalyticsLoading()); 12 | axios 13 | .get("/api/dashboard/analytics") 14 | .then(res => 15 | dispatch({ 16 | type: VIEW_ANALYTICS, 17 | payload: res.data 18 | }) 19 | ) 20 | .catch(err => 21 | dispatch({ 22 | type: GET_ERRORS, 23 | payload: null 24 | }) 25 | ); 26 | }; 27 | 28 | export const getNet = () => dispatch => { 29 | dispatch(setAnalyticsLoading); 30 | axios 31 | .get("/api/dashboard/payoverview") 32 | .then(res => 33 | dispatch({ 34 | type: VIEW_NET, 35 | payload: res.data 36 | }) 37 | ) 38 | .catch(err => 39 | dispatch({ 40 | type: GET_ERRORS, 41 | payload: null 42 | }) 43 | ); 44 | }; 45 | 46 | //Set loding state 47 | export const setAnalyticsLoading = () => { 48 | return { 49 | type: ANALYTICS_LOADING 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /validation/oneoffpayment.js: -------------------------------------------------------------------------------- 1 | const validator = require("validator"); 2 | const isEmpty = require("./is-empty"); 3 | 4 | module.exports = function oneoffpaymentInput(data) { 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ""; 8 | data.amount = !isEmpty(data.amount) ? data.amount : ""; 9 | data.month = !isEmpty(data.month) ? data.month : ""; 10 | data.costType = !isEmpty(data.costType) ? data.costType : ""; 11 | data.employee = !isEmpty(data.employee) ? data.employee : ""; 12 | 13 | if (validator.isEmpty(data.name)) { 14 | errors.name = "Exception name is required"; 15 | } 16 | 17 | if (validator.isEmpty(data.amount)) { 18 | errors.amount = "Amount field is required"; 19 | } 20 | 21 | if (validator.isEmpty(data.month)) { 22 | errors.month = "Month field is required"; 23 | } 24 | 25 | if (validator.isEmpty(data.costType)) { 26 | errors.costType = "Exception type field is required"; 27 | } 28 | 29 | if (validator.isEmpty(data.employee)) { 30 | errors.employee = "Employee field is required"; 31 | } 32 | 33 | return { 34 | errors, 35 | isValid: isEmpty(errors) 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /client/src/reducers/employeeReducer.js: -------------------------------------------------------------------------------- 1 | import { VIEW_EMPLOYEES, EMPLOYEE_LOADING, ADD_EMPLOYEE, GET_EMPLOYEE, DELETE_EMPLOYEE } from "../actions/types"; 2 | 3 | const initialState = { 4 | employees: [], 5 | employee: {}, 6 | loading: false, 7 | }; 8 | 9 | export default function(state = initialState, action) { 10 | switch (action.type) { 11 | case VIEW_EMPLOYEES: 12 | return { 13 | ...state, 14 | employees: action.payload, 15 | loading: false 16 | }; 17 | 18 | case ADD_EMPLOYEE: 19 | return { 20 | ...state, 21 | employees: [action.payload, ...state.employees], 22 | loading: false 23 | }; 24 | 25 | case DELETE_EMPLOYEE: 26 | return { 27 | ...state, 28 | employees: state.employees.filter(employee => employee._id !== action.payload) 29 | } 30 | 31 | case GET_EMPLOYEE: 32 | return { 33 | ...state, 34 | employee: action.payload, 35 | loading: false 36 | } 37 | 38 | case EMPLOYEE_LOADING: 39 | return { 40 | ...state, 41 | loading: true 42 | }; 43 | 44 | default: 45 | return state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /models/Employee.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const EmployeeSchema = new Schema({ 5 | tag: { 6 | type: String, 7 | required: true 8 | }, 9 | name: { 10 | type: String, 11 | required: true 12 | }, 13 | email: { 14 | type: String, 15 | required: true 16 | }, 17 | designation: { 18 | type: String, 19 | required: true 20 | }, 21 | department: { 22 | type: String, 23 | required: true 24 | }, 25 | stateResidence: { 26 | type: String, 27 | required: true 28 | }, 29 | bankName: { 30 | type: String, 31 | required: true 32 | }, 33 | accountNumber: { 34 | type: String, 35 | required: true 36 | }, 37 | pfaName: { 38 | type: String, 39 | required: true 40 | }, 41 | pensionAccountNumber: { 42 | type: String, 43 | required: true 44 | }, 45 | level: { 46 | type: Schema.Types.ObjectId, 47 | ref: "level" 48 | }, 49 | levelName: { 50 | type: String 51 | }, 52 | date_added: { 53 | type: Date, 54 | default: Date.now() 55 | }, 56 | is_delete: { 57 | type: Number, 58 | default: 0 59 | } 60 | }); 61 | 62 | module.exports = Employee = mongoose.model("employee", EmployeeSchema); 63 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@progress/kendo-drawing": "^1.5.7", 7 | "@progress/kendo-react-pdf": "^2.5.0", 8 | "@trendmicro/react-sidenav": "^0.4.5", 9 | "axios": "^0.18.0", 10 | "chart.js": "^2.7.3", 11 | "classnames": "^2.2.6", 12 | "jwt-decode": "^2.2.0", 13 | "moment": "^2.23.0", 14 | "react": "^18.2.0", 15 | "react-chartjs-2": "^2.7.4", 16 | "react-confirm-alert": "^2.0.7", 17 | "react-dom": "^18.2.0", 18 | "react-html-table-to-excel": "^2.0.0", 19 | "react-redux": "^6.0.0", 20 | "react-router-dom": "^4.3.1", 21 | "react-scripts": "^5.0.1", 22 | "react-to-print": "^2.0.0-alpha-6", 23 | "react-toastify": "^4.5.0", 24 | "redux": "^4.0.1", 25 | "redux-thunk": "^2.3.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "proxy": "http://localhost:6000", 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "browserslist": [ 38 | ">0.2%", 39 | "not dead", 40 | "not ie <= 11", 41 | "not op_mini all" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /client/src/reducers/authReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CURRENT_USER, 3 | ADD_USER, 4 | GET_SUCCESS, 5 | VIEW_USERS, 6 | EMPLOYEE_LOADING, 7 | ASSIGN_ROLE 8 | } from "../actions/types"; 9 | import isEmpty from "../validation/is-empty"; 10 | 11 | const initialState = { 12 | isAuthenticated: false, 13 | loading: false, 14 | user: {}, 15 | users: [], 16 | notification: {} 17 | }; 18 | 19 | export default function(state = initialState, action) { 20 | switch (action.type) { 21 | case SET_CURRENT_USER: 22 | return { 23 | ...state, 24 | isAuthenticated: !isEmpty(action.payload), 25 | user: action.payload 26 | }; 27 | 28 | case ADD_USER: 29 | return { 30 | ...state, 31 | user: action.payload 32 | }; 33 | 34 | case VIEW_USERS: 35 | return { 36 | ...state, 37 | users: action.payload, 38 | loading: false 39 | }; 40 | 41 | case ASSIGN_ROLE: 42 | return { 43 | ...state, 44 | notification: action.payload, 45 | loading: false 46 | }; 47 | 48 | case GET_SUCCESS: 49 | return action.payload; 50 | 51 | case EMPLOYEE_LOADING: 52 | return { 53 | ...state, 54 | loading: true 55 | } 56 | 57 | default: 58 | return state; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | Payeroll 16 | 17 | 18 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /client/src/components/common/TextFieldGroup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | import PropTypes from "prop-types"; 4 | 5 | const TextFieldGroup = ({ 6 | name, 7 | placeholder, 8 | value, 9 | label, 10 | error, 11 | info, 12 | type, 13 | onChange, 14 | disabled, 15 | tabindex 16 | }) => ( 17 |
18 |
19 | 20 | 30 | {info && {info}} 31 | {error &&
{error}
} 32 |
33 |
34 | ); 35 | 36 | TextFieldGroup.propTypes = { 37 | label: PropTypes.string, 38 | name: PropTypes.string.isRequired, 39 | placeholder: PropTypes.string, 40 | value: PropTypes.string.isRequired, 41 | type: PropTypes.string.isRequired, 42 | onChange: PropTypes.func.isRequired, 43 | info: PropTypes.string, 44 | error: PropTypes.string, 45 | disabled: PropTypes.string, 46 | tabindex: PropTypes.string 47 | }; 48 | 49 | TextFieldGroup.defaultProps = { 50 | type: "text" 51 | }; 52 | 53 | export default TextFieldGroup; 54 | -------------------------------------------------------------------------------- /client/src/components/common/SelectListGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classnames from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const SelectListGroup = ({ 6 | name, 7 | value, 8 | error, 9 | info, 10 | onChange, 11 | options, 12 | label 13 | }) => { 14 | 15 | const selectOptions = options.map(option => ( 16 | 19 | )) 20 | 21 | return ( 22 |
23 |
24 | 25 | 37 | {info && {info}} 38 | {error &&
{error}
} 39 |
40 |
41 | ) 42 | } 43 | 44 | SelectListGroup.propTypes = { 45 | name: PropTypes.string.isRequired, 46 | value: PropTypes.string.isRequired, 47 | info: PropTypes.string, 48 | error: PropTypes.string, 49 | label: PropTypes.string, 50 | onChange: PropTypes.func.isRequired, 51 | options: PropTypes.array.isRequired, 52 | } 53 | 54 | export default SelectListGroup; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payeroll", 3 | "version": "1.0.0", 4 | "description": "An enterprise payroll application", 5 | "scripts": { 6 | "server": "nodemon server.js", 7 | "back": "node server.js", 8 | "front": "npm start --prefix client", 9 | "client": "npm start --prefix client", 10 | "dev": "concurrently \"npm run server\" \"npm run client\"", 11 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/akinlekan28/payroll.git" 16 | }, 17 | "keywords": [ 18 | "payroll", 19 | "reactjs", 20 | "nodejs", 21 | "mongodb", 22 | "expressjs" 23 | ], 24 | "author": "akintola olalekan", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/akinlekan28/payroll/issues" 28 | }, 29 | "homepage": "https://github.com/akinlekan28/payroll#readme", 30 | "dependencies": { 31 | "@sendgrid/mail": "^6.5.5", 32 | "bcryptjs": "^2.4.3", 33 | "body-parser": "^1.18.3", 34 | "concurrently": "^4.1.0", 35 | "convert-excel-to-json": "^1.6.1", 36 | "cors": "^2.8.5", 37 | "express": "^4.16.4", 38 | "jsonwebtoken": "^9.0.0", 39 | "mongoose": "^5.13.20", 40 | "multer": "^1.4.2", 41 | "passport": "^0.4.0", 42 | "passport-jwt": "^4.0.0", 43 | "pdfmake": "^0.1.40", 44 | "redis": "^2.8.0", 45 | "validator": "^10.9.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /validation/register.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator'); 2 | const isEmpty = require('./is-empty'); 3 | 4 | module.exports = function validateRegisterInput(data){ 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ''; 8 | data.email = !isEmpty(data.email) ? data.email : ''; 9 | data.password = !isEmpty(data.password) ? data.password : ''; 10 | data.password2 = !isEmpty(data.password2) ? data.password2 : ''; 11 | 12 | if(!validator.isLength(data.name, {min: 2, max: 30})){ 13 | errors.name = 'Name must be between 2 and 30 characters'; 14 | } 15 | 16 | if(validator.isEmpty(data.name)){ 17 | errors.name = 'Name field is required' 18 | } 19 | 20 | if (validator.isEmpty(data.email)) { 21 | errors.email = 'Email field is required' 22 | } 23 | 24 | if (!validator.isEmail(data.email)) { 25 | errors.email = 'Email is invalid' 26 | } 27 | 28 | if (validator.isEmpty(data.password)) { 29 | errors.password = 'Password field is required' 30 | } 31 | 32 | if (!validator.isLength(data.password, {min: 6, max: 30})) { 33 | errors.password = 'Password must be atleast 6 characters' 34 | } 35 | 36 | if (validator.isEmpty(data.password2)) { 37 | errors.password2 = 'Confirm Password field is required' 38 | } 39 | 40 | if (!validator.equals(data.password, data.password2)) { 41 | errors.password2 = 'Passwords must match' 42 | } 43 | 44 | return { 45 | errors, 46 | isValid: isEmpty(errors) 47 | }; 48 | }; -------------------------------------------------------------------------------- /client/src/components/common/Pagination.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Pagination = ({ employeePerPage, totalEmployees, paginate, currentPage, currentLevel }) => { 4 | const pageNumbers = []; 5 | employeePerPage = parseInt(employeePerPage); 6 | for (let i = 1; i <= Math.ceil(totalEmployees / employeePerPage); i++) { 7 | pageNumbers.push(i) 8 | } 9 | return ( 10 |
11 | 29 |
30 | ) 31 | } 32 | 33 | export default Pagination 34 | -------------------------------------------------------------------------------- /client/src/components/dashboard/EmployeeRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class EmployeeRow extends Component { 4 | 5 | render() { 6 | 7 | const employees = this.props.employeeDetails.map(employee => ( 8 | 9 | {employee.tag} 10 | {employee.name} 11 | {employee.email} 12 | {employee.designation} 13 | {employee.department} 14 | 15 | )) 16 | 17 | return ( 18 | 19 |
20 |
21 |
22 |
23 |

Last Five Entry of Employees

24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {employees} 39 | 40 |
TagNameEmailDesignationDepartment
41 |
42 |
43 |
44 |
45 |
46 | ) 47 | } 48 | } 49 | 50 | export default EmployeeRow 51 | -------------------------------------------------------------------------------- /client/src/components/employee/ViewEmployee.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import Spinner from "../common/Spinner"; 5 | import { getEmployees } from "../../actions/employeeActions"; 6 | import SearchBar from "../dashboard/SearchBar"; 7 | import SideBar from "../dashboard/SideBar"; 8 | import Footer from "../dashboard/Footer"; 9 | import EmployeeTable from "./EmployeeTable"; 10 | 11 | class ViewEmployee extends Component { 12 | componentDidMount() { 13 | this.props.getEmployees(); 14 | } 15 | 16 | render() { 17 | const { employees, loading } = this.props.employees; 18 | 19 | let employeeTable; 20 | 21 | if (employees === null || loading) { 22 | employeeTable = ; 23 | } else { 24 | if (Object.keys(employees).length > 0) { 25 | employeeTable = ; 26 | } else { 27 | employeeTable =

No previous employee entries!

; 28 | } 29 | } 30 | 31 | return ( 32 |
33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |

View Employees

41 |
42 | {employeeTable} 43 |
44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | ViewEmployee.propTypes = { 53 | getEmployees: PropTypes.func.isRequired 54 | }; 55 | 56 | const mapStateToProps = state => ({ 57 | employees: state.employees 58 | }); 59 | 60 | export default connect( 61 | mapStateToProps, 62 | { getEmployees } 63 | )(ViewEmployee); 64 | -------------------------------------------------------------------------------- /client/src/reducers/payrollReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | VIEW_PAYROLL, 3 | VIEW_MONTHLY_PAYROLL, 4 | VIEW_PAYROLL_RECORDS, 5 | VIEW_PAYROLL_RECORDS_MONTHLY, 6 | VIEW_PAYROLL_RECORDS_YEARLY, 7 | VIEW_EMPLOYEE_MONTH_YEAR, 8 | VIEW_MONTH_YEAR, 9 | PAYROLL_LOADING 10 | } from "../actions/types"; 11 | 12 | const initialState = { 13 | payroll: [], 14 | payrolls: [], 15 | payrollRecords: [], 16 | payrollRecordsMonthly: [], 17 | payrollRecordsYearly: [], 18 | employeeMonthYear: [], 19 | monthYear: [], 20 | loading: false 21 | }; 22 | 23 | export default function(state = initialState, action) { 24 | switch (action.type) { 25 | default: 26 | return state; 27 | 28 | case VIEW_PAYROLL: 29 | return { 30 | ...state, 31 | payroll: action.payload, 32 | loading: false 33 | }; 34 | case VIEW_MONTHLY_PAYROLL: 35 | return { 36 | ...state, 37 | payrolls: action.payload, 38 | loading: false 39 | }; 40 | case VIEW_PAYROLL_RECORDS: 41 | return { 42 | ...state, 43 | payrollRecords: action.payload, 44 | loading: false 45 | }; 46 | case VIEW_PAYROLL_RECORDS_MONTHLY: 47 | return { 48 | ...state, 49 | payrollRecordsMonthly: action.payload, 50 | loading: false 51 | }; 52 | case VIEW_PAYROLL_RECORDS_YEARLY: 53 | return { 54 | ...state, 55 | payrollRecordsYearly: action.payload, 56 | loading: false 57 | }; 58 | case VIEW_EMPLOYEE_MONTH_YEAR: 59 | return { 60 | ...state, 61 | employeeMonthYear: action.payload, 62 | loading: false 63 | }; 64 | case VIEW_MONTH_YEAR: 65 | return { 66 | ...state, 67 | monthYear: action.payload, 68 | loading: false 69 | }; 70 | case PAYROLL_LOADING: 71 | return { 72 | ...state, 73 | loading: true 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const GET_ERRORS = 'GET_ERRORS'; 2 | export const GET_SUCCESS = 'GET_SUCCESS'; 3 | export const CLEAR_ERRORS = 'CLEAR_ERRORS'; 4 | export const SET_CURRENT_USER = 'SET_CURRENT_USER'; 5 | export const ADD_USER = 'ADD_USER'; 6 | export const VIEW_USERS = 'VIEW_USERS'; 7 | export const VIEW_ANALYTICS = 'VIEW_ANALYTICS'; 8 | export const VIEW_NET = 'VIEW_NET'; 9 | export const VIEW_EMPLOYEES = 'VIEW_EMPLOYEES'; 10 | export const ANALYTICS_LOADING = 'ANALYTICS_LOADING'; 11 | export const VIEW_LEVELS = 'VIEW_LEVELS'; 12 | export const DELETE_LEVEL = 'DELETE_LEVEL'; 13 | export const LEVELS_LOADING = 'LEVELS_LOADING'; 14 | export const ADD_EMPLOYEE = 'ADD_EMPLOYEE'; 15 | export const EMPLOYEE_LOADING = 'EMPLOYEE_LOADING'; 16 | export const GET_EMPLOYEE = 'GET_EMPLOYEE'; 17 | export const DELETE_EMPLOYEE = 'DELETE_EMPLOYEE'; 18 | export const ADD_LEVEL = 'ADD_LEVEL'; 19 | export const ADD_EXCEPTION = 'ADD_EXCEPTION'; 20 | export const ADD_OTHER_EXCEPTION = 'ADD_OTHER_EXCEPTION'; 21 | export const ADD_ONE_OFF_PAYMENT = 'ADD_ONE_OFF_PAYMENT'; 22 | export const VIEW_EXCEPTIONS = 'VIEW_EXCEPTIONS'; 23 | export const VIEW_OTHER_EXCEPTION = 'VIEW_OTHER_EXCEPTION'; 24 | export const VIEW_ONE_OFF_PAYMENT = 'VIEW_ONE_OFF_PAYMENT'; 25 | export const DELETE_OTHER_EXCEPTION = 'DELETE_OTHER_EXCEPTION'; 26 | export const DELETE_EXCEPTION = 'DELETE_EXCEPTION'; 27 | export const DELETE_ONE_OFF_PAYMENT = 'DELETE_ONE_OFF_PAYMENT'; 28 | export const EXCEPTION_LOADING = 'EXCEPTION_LOADING'; 29 | export const VIEW_PAYROLL = 'VIEW_PAYROLL'; 30 | export const VIEW_MONTHLY_PAYROLL = 'VIEW_MONTHLY_PAYROLL'; 31 | export const VIEW_PAYROLL_RECORDS = 'VIEW_PAYROLL_RECORDS'; 32 | export const VIEW_PAYROLL_RECORDS_MONTHLY = 'VIEW_PAYROLL_RECORDS_MONTHLY'; 33 | export const VIEW_PAYROLL_RECORDS_YEARLY = 'VIEW_PAYROLL_RECORDS_YEARLY'; 34 | export const VIEW_EMPLOYEE_MONTH_YEAR = 'VIEW_EMPLOYEE_MONTH_YEAR'; 35 | export const VIEW_MONTH_YEAR = 'VIEW_MONTH_YEAR'; 36 | export const PAYROLL_LOADING = 'PAYROLL_LOADING'; 37 | export const ASSIGN_ROLE = 'ASSIGN_ROLE'; -------------------------------------------------------------------------------- /client/src/components/dashboard/Netpay.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Line } from "react-chartjs-2"; 4 | 5 | const Netpay = ({ net }) => { 6 | const data = { 7 | labels: [ 8 | "January", 9 | "February", 10 | "March", 11 | "April", 12 | "May", 13 | "June", 14 | "July", 15 | "August", 16 | "September", 17 | "Öctober", 18 | "November", 19 | "December" 20 | ], 21 | datasets: [ 22 | { 23 | label: "Monthly Net Pay", 24 | data: [ 25 | net.jan, 26 | net.feb, 27 | net.mar, 28 | net.apr, 29 | net.may, 30 | net.jun, 31 | net.jul, 32 | net.aug, 33 | net.sep, 34 | net.oct, 35 | net.nov, 36 | net.dec 37 | ], 38 | backgroundColor: "rgb(62, 202, 202)", 39 | pointRadius: 4, 40 | fill: false, 41 | borderColor: "rgb(62, 202, 202)" 42 | }, 43 | { 44 | label: "Gross Earnings", 45 | data: [ 46 | net.grossJan, 47 | net.grossFeb, 48 | net.grossMar, 49 | net.grossApr, 50 | net.grossMay, 51 | net.grossJun, 52 | net.grossJul, 53 | net.grossAug, 54 | net.grossSep, 55 | net.grossOct, 56 | net.grossNov, 57 | net.grossDec 58 | ], 59 | backgroundColor: "#C0998A", 60 | pointRadius: 4, 61 | fill: false, 62 | borderColor: "#C0998A" 63 | } 64 | ] 65 | }; 66 | 67 | const options = { 68 | tooltips: { 69 | titleFontSize: 15, 70 | bodyFontSize: 19 71 | } 72 | }; 73 | 74 | return ( 75 |
76 |
77 |
78 | 79 |
80 |
81 |
82 | ); 83 | }; 84 | 85 | Netpay.propTypes = { 86 | net: PropTypes.object.isRequired 87 | }; 88 | 89 | export default Netpay; 90 | -------------------------------------------------------------------------------- /utils/createSuperAdmin.js: -------------------------------------------------------------------------------- 1 | const keys = require('../config/keys'); 2 | const mailer = require('@sendgrid/mail'); 3 | const bcrypt = require('bcryptjs'); 4 | mailer.setApiKey(keys.sendGridKey); 5 | 6 | const User = require('../models/User'); 7 | 8 | const createSuperAdmin = () => { 9 | const password = _generatePassword(); 10 | const name = keys.name; 11 | const email = keys.email; 12 | const is_admin = 1; 13 | 14 | User.findOne({ email }) 15 | .then((user) => { 16 | if (!user) { 17 | const newUser = new User({ 18 | name, 19 | email, 20 | password, 21 | is_admin, 22 | }); 23 | 24 | bcrypt.genSalt(10, (err, salt) => { 25 | bcrypt.hash(newUser.password, salt, (err, hash) => { 26 | if (err) throw err; 27 | newUser.password = hash; 28 | newUser 29 | .save() 30 | .then((registered) => { 31 | if (registered) { 32 | _sendEmail(registered) 33 | .then((res) => 34 | console.log('Message successfully sent!') 35 | ) 36 | .catch((err) => console.log(err)); 37 | } 38 | }) 39 | .catch((err) => console.log(err)); 40 | }); 41 | }); 42 | } else { 43 | console.log('User email already exist'); 44 | } 45 | }) 46 | .catch((err) => condsole.log(err)); 47 | }; 48 | 49 | const _generatePassword = () => { 50 | return Array(30) 51 | .fill( 52 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 53 | ) 54 | .map(function (x) { 55 | return x[Math.floor(Math.random() * x.length)]; 56 | }) 57 | .join(''); 58 | }; 59 | 60 | const _sendEmail = (payload) => { 61 | const data = { 62 | from: 'Payroll Admin ', 63 | to: 'akinlekan@gmail.com', 64 | subject: 'Super Admin Details', 65 | text: `Hello, Here are your login details\nEmail: ${payload.email}\nPassword: ${payload.password}`, 66 | }; 67 | return mailer.send(data); 68 | }; 69 | 70 | module.exports = Object.assign({}, { createSuperAdmin }); 71 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/records/AllEmployee.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { getAllYearlyPayslip } from "../../../../actions/payrollActions"; 5 | import SearchBar from "../../../dashboard/SearchBar"; 6 | import SideBar from "../../../dashboard/SideBar"; 7 | import Spinner from "../../../common/Spinner"; 8 | import AllEmployeeTable from "./AllEmployeeTable"; 9 | 10 | class AllEmployee extends Component { 11 | componentDidMount = () => { 12 | this.props.getAllYearlyPayslip(); 13 | }; 14 | 15 | render() { 16 | let date = new Date(); 17 | const year = date.getFullYear(); 18 | 19 | const { payrollRecordsYearly, loading } = this.props.payrollRecordsYearly; 20 | let AllEmployeeSlipContainer; 21 | 22 | if (payrollRecordsYearly === null || loading) { 23 | AllEmployeeSlipContainer = ; 24 | } else { 25 | if (Object.keys(payrollRecordsYearly).length > 0) { 26 | AllEmployeeSlipContainer = ( 27 | 28 | ); 29 | } else { 30 | AllEmployeeSlipContainer = ( 31 |

No previously generated payslips

32 | ); 33 | } 34 | } 35 | 36 | return ( 37 |
38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 |

Payroll report

46 |
47 | 48 |

49 | All employee payslips generated for the year {year} 50 |

51 | {AllEmployeeSlipContainer} 52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | } 59 | 60 | AllEmployee.propTypes = { 61 | getAllYearlyPayslip: PropTypes.func.isRequired 62 | }; 63 | 64 | const mapStateToProps = state => ({ 65 | payrollRecordsYearly: state.payroll 66 | }); 67 | 68 | export default connect( 69 | mapStateToProps, 70 | { getAllYearlyPayslip } 71 | )(AllEmployee); 72 | -------------------------------------------------------------------------------- /models/Payslip.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const PayslipSchema = new Schema({ 5 | name: { 6 | type: String 7 | }, 8 | tag: { 9 | type: String 10 | }, 11 | department: { 12 | type: String 13 | }, 14 | basic: { 15 | type: Number 16 | }, 17 | grossEarning: { 18 | type: Number 19 | }, 20 | tax: { 21 | type: Number 22 | }, 23 | pension: { 24 | type: Number 25 | }, 26 | consolidationRelief: { 27 | type: Number 28 | }, 29 | totalDeductions: { 30 | type: Number 31 | }, 32 | netPay: { 33 | type: Number 34 | }, 35 | email: { 36 | type: String 37 | }, 38 | designation: { 39 | type: String 40 | }, 41 | bonuses: [ 42 | { 43 | name: { 44 | type: String 45 | }, 46 | amount: { 47 | type: Number 48 | } 49 | } 50 | ], 51 | deductables: [ 52 | { 53 | name: { 54 | type: String 55 | }, 56 | amount: { 57 | type: Number 58 | } 59 | } 60 | ], 61 | individualcost: [ 62 | { 63 | name: { 64 | type: String 65 | }, 66 | amount: { 67 | type: Number 68 | }, 69 | costType: { 70 | type: String 71 | } 72 | } 73 | ], 74 | oneOffPaymentArray: [ 75 | { 76 | name: { 77 | type: String 78 | }, 79 | amount: { 80 | type: Number 81 | }, 82 | costType: { 83 | type: String 84 | } 85 | } 86 | ], 87 | employee: { 88 | type: Schema.Types.ObjectId, 89 | ref: "employee" 90 | }, 91 | taxableIncome: { 92 | type: Number 93 | }, 94 | bankName: { 95 | type: String, 96 | required: true 97 | }, 98 | accountNumber: { 99 | type: String, 100 | required: true 101 | }, 102 | pfaName: { 103 | type: String, 104 | required: true 105 | }, 106 | pensionAccountNumber: { 107 | type: String, 108 | required: true 109 | }, 110 | presentMonth: { 111 | type: String 112 | }, 113 | presentYear: { 114 | type: Number 115 | }, 116 | is_delete: { 117 | type: Number, 118 | default: 0 119 | } 120 | }); 121 | 122 | module.exports = Payslip = mongoose.model("payslip", PayslipSchema); 123 | -------------------------------------------------------------------------------- /validation/employee.js: -------------------------------------------------------------------------------- 1 | const validator = require("validator"); 2 | const isEmpty = require("./is-empty"); 3 | 4 | module.exports = function EmployeeInput(data) { 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ""; 8 | data.email = !isEmpty(data.email) ? data.email : ""; 9 | data.designation = !isEmpty(data.designation) ? data.designation : ""; 10 | data.department = !isEmpty(data.department) ? data.department : ""; 11 | data.level = !isEmpty(data.level) ? data.level : ""; 12 | data.stateResidence = !isEmpty(data.stateResidence) 13 | ? data.stateResidence 14 | : ""; 15 | data.bankName = !isEmpty(data.bankName) ? data.bankName : ""; 16 | data.accountNumber = !isEmpty(data.accountNumber) ? data.accountNumber : ""; 17 | data.pfaName = !isEmpty(data.pfaName) ? data.pfaName : ""; 18 | data.pensionAccountNumber = !isEmpty(data.pensionAccountNumber) 19 | ? data.pensionAccountNumber 20 | : ""; 21 | 22 | if (!validator.isLength(data.name, { min: 2, max: 30 })) { 23 | errors.name = "Name must be between 2 and 30 characters"; 24 | } 25 | 26 | if (validator.isEmpty(data.name)) { 27 | errors.name = "Name field is required"; 28 | } 29 | 30 | if (validator.isEmpty(data.email)) { 31 | errors.email = "Email field is required"; 32 | } 33 | 34 | if (!validator.isEmail(data.email)) { 35 | errors.email = "Email is invalid"; 36 | } 37 | 38 | if (validator.isEmpty(data.designation)) { 39 | errors.designation = "Designation field is required"; 40 | } 41 | 42 | if (validator.isEmpty(data.department)) { 43 | errors.department = "Department field is required"; 44 | } 45 | 46 | if (validator.isEmpty(data.level)) { 47 | errors.level = "Level field is required"; 48 | } 49 | if (validator.isEmpty(data.stateResidence)) { 50 | errors.stateResidence = "State of residence field is required"; 51 | } 52 | if (validator.isEmpty(data.bankName)) { 53 | errors.bankName = "Bank name field is required"; 54 | } 55 | if (validator.isEmpty(data.accountNumber)) { 56 | errors.accountNumber = "Account number field is required"; 57 | } 58 | if (validator.isEmpty(data.pfaName)) { 59 | errors.pfaName = "PFA name field is required"; 60 | } 61 | if (validator.isEmpty(data.pensionAccountNumber)) { 62 | errors.pensionAccountNumber = "Pension account number field is required"; 63 | } 64 | 65 | return { 66 | errors, 67 | isValid: isEmpty(errors) 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/MonthlyDashboard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SearchBar from "../../dashboard/SearchBar"; 3 | import SideBar from "../../dashboard/SideBar"; 4 | import Footer from "../../dashboard/Footer"; 5 | import MonthlyPayroll from "./links/MonthlyPayroll"; 6 | import MonthlyPension from "./links/MonthlyPension"; 7 | import MonthlyTax from "./links/MonthlyTax"; 8 | import YearlySingleEmployee from "./links/YearlySingleEmployee"; 9 | import YearlyAllEmployee from "./links/YearlyAllEmployee"; 10 | import YearlyMonthlyslip from "./links/YearlyMonthlyslip"; 11 | import EmployeeMonthYear from "./links/EmployeeMonthYear"; 12 | import MonthYear from "./links/MonthYear"; 13 | import Year from "./links/Year"; 14 | 15 | const MonthlyDashboard = () => { 16 | let date = new Date(); 17 | const presentMonth = date.toLocaleString("en-us", { month: "long" }); 18 | const year = date.getFullYear(); 19 | 20 | return ( 21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |

Payroll report

31 |
32 |

33 | Payroll Report for the month of {presentMonth} 34 |

35 |
36 | 37 | 38 | 39 |
40 | 41 |

42 | Payroll Report for the year {year} 43 |

44 |
45 | 46 | 47 | 48 |
49 | 50 |

51 | All Time Payroll Report 52 |

53 |
54 | 55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default MonthlyDashboard; 68 | -------------------------------------------------------------------------------- /client/src/reducers/exceptionReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_EXCEPTION, 3 | ADD_OTHER_EXCEPTION, 4 | ADD_ONE_OFF_PAYMENT, 5 | VIEW_EXCEPTIONS, 6 | VIEW_OTHER_EXCEPTION, 7 | VIEW_ONE_OFF_PAYMENT, 8 | DELETE_EXCEPTION, 9 | DELETE_OTHER_EXCEPTION, 10 | DELETE_ONE_OFF_PAYMENT, 11 | EXCEPTION_LOADING 12 | } from "../actions/types"; 13 | 14 | const initialState = { 15 | exceptions: [], 16 | otherexception: [], 17 | oneoffpayment: [], 18 | loading: false 19 | }; 20 | 21 | export default function(state = initialState, action) { 22 | switch (action.type) { 23 | default: 24 | return state; 25 | 26 | case ADD_EXCEPTION: 27 | return { 28 | ...state, 29 | exceptions: [action.payload, ...state.exceptions], 30 | loading: false 31 | }; 32 | case ADD_OTHER_EXCEPTION: 33 | return { 34 | ...state, 35 | otherexception: [action.payload, ...state.otherexception], 36 | loading: false 37 | }; 38 | case ADD_ONE_OFF_PAYMENT: 39 | return { 40 | ...state, 41 | oneoffpayment: [action.payload, ...state.oneoffpayment], 42 | loading: false 43 | }; 44 | case VIEW_EXCEPTIONS: 45 | return { 46 | ...state, 47 | exceptions: action.payload, 48 | loading: false 49 | }; 50 | case VIEW_OTHER_EXCEPTION: 51 | return { 52 | ...state, 53 | otherexception: action.payload, 54 | loading: false 55 | }; 56 | case VIEW_ONE_OFF_PAYMENT: 57 | return { 58 | ...state, 59 | oneoffpayment: action.payload, 60 | loading: false 61 | }; 62 | case DELETE_EXCEPTION: 63 | return { 64 | ...state, 65 | exceptions: state.exceptions.filter( 66 | exception => exception._id !== action.payload 67 | ), 68 | loading: false 69 | }; 70 | case DELETE_OTHER_EXCEPTION: 71 | return { 72 | ...state, 73 | otherexception: state.otherexception.filter( 74 | otherexceptionItem => otherexceptionItem._id !== action.payload 75 | ), 76 | loading: false 77 | }; 78 | case DELETE_ONE_OFF_PAYMENT: 79 | return { 80 | ...state, 81 | oneoffpayment: state.oneoffpayment.filter( 82 | oneoffpaymentItem => oneoffpaymentItem._id !== action.payload 83 | ), 84 | loading: false 85 | }; 86 | case EXCEPTION_LOADING: 87 | return { 88 | ...state, 89 | loading: true 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyPaser = require('body-parser'); 3 | const mongoose = require('mongoose'); 4 | const passport = require('passport'); 5 | const path = require('path'); 6 | const cors = require('cors'); 7 | // const redis = require('redis'); 8 | 9 | const users = require('./routes/api/users'); 10 | const level = require('./routes/api/levels'); 11 | const employee = require('./routes/api/employees'); 12 | const exception = require('./routes/api/exception'); 13 | const payslip = require('./routes/api/payslip'); 14 | const dashboard = require('./routes/api/dashboard'); 15 | const individualcost = require('./routes/api/individualcost'); 16 | const oneoffpayment = require('./routes/api/oneoffpayment'); 17 | const record = require('./routes/api/record'); 18 | const { createSuperAdmin } = require('./utils/createSuperAdmin'); 19 | 20 | const app = express(); 21 | 22 | //Body parser middleware 23 | app.use(bodyPaser.urlencoded({ extended: false })); 24 | app.use(bodyPaser.json()); 25 | app.use(cors()); 26 | 27 | //Db 28 | const db = require('./config/keys').mongoURI; 29 | 30 | //MongoDB connection 31 | mongoose 32 | .connect(db, { useNewUrlParser: true }) 33 | .then(() => console.log('MongoDB connected')) 34 | .catch((err) => console.log(err)); 35 | 36 | //Passport Middleware 37 | app.use(passport.initialize()); 38 | 39 | //Passport config 40 | require('./config/passport')(passport); 41 | 42 | //Use routes 43 | app.use('/api/users', users); 44 | app.use('/api/level', level); 45 | app.use('/api/employee', employee); 46 | app.use('/api/exception', exception); 47 | app.use('/api/payslip', payslip); 48 | app.use('/api/dashboard', dashboard); 49 | app.use('/api/individualcost', individualcost); 50 | app.use('/api/oneoffpayment', oneoffpayment); 51 | app.use('/api/record', record); 52 | 53 | // Server static assets if in production 54 | if (process.env.NODE_ENV === 'production') { 55 | // Set static folder 56 | app.use(express.static('client/build')); 57 | 58 | app.get('*', (req, res) => { 59 | res.sendFile( 60 | path.resolve(__dirname, 'client', 'build', 'index.html') 61 | ); 62 | }); 63 | } 64 | 65 | const PORT = process.env.PORT || 6000; 66 | // const REDIS_PORT = process.env.REDIS_PORT || 6379; 67 | 68 | // const client = redis.createClient(REDIS_PORT); 69 | 70 | // client.on('connect', () => { 71 | // console.log(`Redis coonected on port ${REDIS_PORT}`); 72 | // }); 73 | 74 | app.listen(PORT, () => { 75 | console.log(`App is running on port ${PORT}`); 76 | setTimeout(() => { 77 | createSuperAdmin(); 78 | }, 20000); 79 | }); 80 | -------------------------------------------------------------------------------- /client/src/components/exception/ViewException.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { deleteException } from "../../actions/exceptionActions"; 5 | import { toast } from "react-toastify"; 6 | import { confirmAlert } from "react-confirm-alert"; 7 | 8 | class ViewException extends Component { 9 | onDelete(id) { 10 | confirmAlert({ 11 | title: "Delete this exception ?", 12 | message: "Are you sure to do this", 13 | buttons: [ 14 | { 15 | label: "Yes delete exception!", 16 | onClick: () => { 17 | this.props 18 | .deleteException(id) 19 | .then(res => { 20 | if (res.type === "DELETE_EXCEPTION") { 21 | toast.success("Exception Deleted!"); 22 | } 23 | }) 24 | .catch(err => console.log(err)); 25 | } 26 | }, 27 | { 28 | label: "No cancel delete!", 29 | onClick: () => {} 30 | } 31 | ] 32 | }); 33 | } 34 | 35 | render() { 36 | const { exceptions } = this.props; 37 | 38 | const formatMoney = money => { 39 | let formatedValue = money 40 | .toString() 41 | .replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"); 42 | return formatedValue; 43 | }; 44 | 45 | let exceptionContainer; 46 | 47 | if (Object.keys(exceptions).length === 0) 48 | return (exceptionContainer =

There are no data in the system

); 49 | 50 | exceptionContainer = exceptions.map(exceptionItem => ( 51 |
55 |

56 | Employee name: {exceptionItem.name} 57 |

58 |

59 | Amount : {" "} 60 | {formatMoney(exceptionItem.amount)} 61 |

62 |
63 | 69 |
70 |
71 |
72 | )); 73 | 74 | return
{exceptionContainer}
; 75 | } 76 | } 77 | 78 | ViewException.propTypes = { 79 | exceptions: PropTypes.array.isRequired, 80 | deleteException: PropTypes.func.isRequired 81 | }; 82 | 83 | export default connect( 84 | null, 85 | { deleteException } 86 | )(ViewException); 87 | -------------------------------------------------------------------------------- /client/src/components/level/LevelTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LevelTab = () => { 4 | return ( 5 |
6 | 88 |
89 | ); 90 | }; 91 | 92 | export default LevelTab; 93 | -------------------------------------------------------------------------------- /client/src/components/level/ViewBonusTable.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { formatMoney } from "../common/Utilities"; 5 | import { deleteBonus } from "../../actions/levelActions"; 6 | import { toast } from "react-toastify"; 7 | import { confirmAlert } from "react-confirm-alert"; 8 | 9 | class ViewBonusTable extends Component { 10 | onDelete(levelId, bonusId) { 11 | confirmAlert({ 12 | title: "Delete this bonus ?", 13 | message: "Are you sure to do this", 14 | buttons: [ 15 | { 16 | label: "Yes delete bonus!", 17 | onClick: () => { 18 | this.props 19 | .deleteBonus(levelId, bonusId) 20 | .then(res => { 21 | if (res.type === "VIEW_LEVELS") { 22 | toast.success("Bonus Deleted!"); 23 | } 24 | }) 25 | .catch(err => console.log(err)); 26 | } 27 | }, 28 | { 29 | label: "No cancel delete!", 30 | onClick: () => {} 31 | } 32 | ] 33 | }); 34 | } 35 | 36 | render() { 37 | const { levels } = this.props; 38 | 39 | const bonusTableContainer = levels.map(level => ( 40 |
44 |

45 | Level Name : {level.name} 46 |

47 |

48 | Level Salary : {" "} 49 | {formatMoney(level.basic)} 50 |

51 | {level.bonuses.length > 0 ? ( 52 |
53 |
Bonus
54 | {level.bonuses.map(bonus => ( 55 |
56 |

Bonus name: {bonus.name}

57 |

58 | Amount: {formatMoney(bonus.amount)} 59 |

60 |
61 | 67 |
68 |
69 |
70 | ))} 71 |
72 | ) : null} 73 |
74 | )); 75 | 76 | return
{bonusTableContainer}
; 77 | } 78 | } 79 | 80 | ViewBonusTable.propTypes = { 81 | levels: PropTypes.array.isRequired, 82 | deleteBonus: PropTypes.func.isRequired 83 | }; 84 | 85 | export default connect( 86 | null, 87 | { deleteBonus } 88 | )(ViewBonusTable); 89 | -------------------------------------------------------------------------------- /client/src/components/exception/ExceptionTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ExceptionTab = () => { 4 | return ( 5 | 89 | ); 90 | }; 91 | 92 | export default ExceptionTab; 93 | -------------------------------------------------------------------------------- /client/src/components/dashboard/SideBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import {Link} from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import {connect} from 'react-redux'; 5 | import {logoutUser} from '../../actions/authActions'; 6 | 7 | class SideBar extends Component { 8 | 9 | onHandleClick(e){ 10 | e.preventDefault(); 11 | 12 | this.props.logoutUser(); 13 | } 14 | 15 | render() { 16 | 17 | const { auth } = this.props; 18 | return ( 19 |
20 | 48 |
49 | 50 |
Documentation
51 | 52 |
53 |
54 | ) 55 | } 56 | } 57 | 58 | SideBar.propTypes = { 59 | logoutUser: PropTypes.func.isRequired 60 | } 61 | 62 | const mapStateToProps = state => ({ 63 | auth: state.auth 64 | }) 65 | 66 | export default connect(mapStateToProps, {logoutUser})(SideBar); 67 | -------------------------------------------------------------------------------- /client/src/components/level/ViewDeductableTable.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { formatMoney } from "../common/Utilities"; 5 | import { deleteDeductable } from "../../actions/levelActions"; 6 | import { toast } from "react-toastify"; 7 | import { confirmAlert } from "react-confirm-alert"; 8 | 9 | class ViewDeductableTable extends Component { 10 | onDelete(levelId, bonusId) { 11 | confirmAlert({ 12 | title: "Delete this deductable ?", 13 | message: "Are you sure to do this", 14 | buttons: [ 15 | { 16 | label: "Yes delete!", 17 | onClick: () => { 18 | this.props 19 | .deleteDeductable(levelId, bonusId) 20 | .then(res => toast.success("Deductable Deleted!")) 21 | .catch(err => console.log(err)); 22 | } 23 | }, 24 | { 25 | label: "No cancel delete!", 26 | onClick: () => {} 27 | } 28 | ] 29 | }); 30 | } 31 | 32 | render() { 33 | const { levels } = this.props; 34 | 35 | const DeductableContainer = levels.map(level => ( 36 |
40 |

41 | Level Name : {level.name} 42 |

43 |

44 | Level Salary : {" "} 45 | {formatMoney(level.basic)} 46 |

47 | {level.deductables.length > 0 ? ( 48 |
49 |
Deduction
50 | {level.deductables.map(deductable => ( 51 |
52 |

Deduction name: {deductable.name}

53 |

54 | Amount: {formatMoney(deductable.amount)} 55 |

56 |
57 | 67 |
68 |
69 |
70 | ))} 71 |
72 | ) : null} 73 |
74 | )); 75 | 76 | return
{DeductableContainer}
; 77 | } 78 | } 79 | 80 | ViewDeductableTable.propTypes = { 81 | levels: PropTypes.array.isRequired, 82 | deleteDeductable: PropTypes.func.isRequired 83 | }; 84 | 85 | export default connect( 86 | null, 87 | { deleteDeductable } 88 | )(ViewDeductableTable); 89 | -------------------------------------------------------------------------------- /client/src/components/exception/ViewOtherException.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { deleteOtherException } from "../../actions/exceptionActions"; 5 | import { toast } from "react-toastify"; 6 | import { confirmAlert } from "react-confirm-alert"; 7 | 8 | class ViewOtherException extends Component { 9 | onDelete(id) { 10 | confirmAlert({ 11 | title: "Delete this exception ?", 12 | message: "Are you sure to do this", 13 | buttons: [ 14 | { 15 | label: "Yes delete exception!", 16 | onClick: () => { 17 | this.props 18 | .deleteOtherException(id) 19 | .then(res => { 20 | if (res.type === "DELETE_OTHER_EXCEPTION") { 21 | toast.success("Exception Deleted!"); 22 | } 23 | }) 24 | .catch(err => console.log(err)); 25 | } 26 | }, 27 | { 28 | label: "No cancel delete!", 29 | onClick: () => {} 30 | } 31 | ] 32 | }); 33 | } 34 | 35 | render() { 36 | const { otherexception } = this.props; 37 | 38 | const formatMoney = money => { 39 | let formatedValue = money 40 | .toString() 41 | .replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"); 42 | return formatedValue; 43 | }; 44 | 45 | let exceptionContainer; 46 | 47 | if (Object.keys(otherexception).length === 0) 48 | return (exceptionContainer =

There's no data in the system

); 49 | 50 | exceptionContainer = otherexception.map(exceptionItem => ( 51 |
55 |

56 | Exception name: {exceptionItem.name} 57 |

58 |

59 | Employee name: {exceptionItem.employeeName} 60 |

61 |

62 | Exception type:{" "} 63 | {exceptionItem.costType === "income" ? "Income" : "Deduction"} 64 |

65 |

66 | Amount : {" "} 67 | {formatMoney(exceptionItem.amount)} 68 |

69 |
70 | 76 |
77 |
78 | )); 79 | 80 | return
{exceptionContainer}
; 81 | } 82 | } 83 | 84 | ViewOtherException.propTypes = { 85 | otherexception: PropTypes.array.isRequired, 86 | deleteOtherException: PropTypes.func.isRequired 87 | }; 88 | 89 | export default connect( 90 | null, 91 | { deleteOtherException } 92 | )(ViewOtherException); 93 | -------------------------------------------------------------------------------- /client/src/actions/employeeActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | VIEW_EMPLOYEES, 4 | EMPLOYEE_LOADING, 5 | GET_ERRORS, 6 | ADD_EMPLOYEE, 7 | GET_EMPLOYEE, 8 | CLEAR_ERRORS, 9 | DELETE_EMPLOYEE 10 | } from "./types"; 11 | 12 | //Post employeee 13 | export const registerEmployee = employeeData => dispatch => { 14 | dispatch(clearErrors()); 15 | return axios 16 | .post("/api/employee", employeeData) 17 | .then(res => 18 | dispatch({ 19 | type: ADD_EMPLOYEE, 20 | payload: res.data 21 | }) 22 | ) 23 | .catch( 24 | err => 25 | dispatch({ 26 | type: GET_ERRORS, 27 | payload: err.response.data 28 | }), 29 | dispatch(() => { 30 | setTimeout(function() { 31 | dispatch(clearErrors()); 32 | }, 5000); 33 | }) 34 | ); 35 | }; 36 | 37 | //Get all employees 38 | export const getEmployees = () => dispatch => { 39 | dispatch(setEmployeeLoading()); 40 | axios 41 | .get("/api/employee") 42 | .then(res => 43 | dispatch({ 44 | type: VIEW_EMPLOYEES, 45 | payload: res.data 46 | }) 47 | ) 48 | .catch(err => 49 | dispatch({ 50 | type: GET_ERRORS, 51 | payload: err.response.data 52 | }) 53 | ); 54 | }; 55 | 56 | //Delete employee 57 | export const deleteEmployee = employeeId => dispatch => { 58 | return axios 59 | .post(`/api/employee/${employeeId}`) 60 | .then(res => 61 | dispatch({ 62 | type: DELETE_EMPLOYEE, 63 | payload: employeeId 64 | }) 65 | ) 66 | .catch(err => 67 | dispatch({ 68 | type: GET_ERRORS, 69 | payload: err.response.data 70 | }) 71 | ); 72 | }; 73 | 74 | //Get single employee 75 | export const getEmployee = employeeId => dispatch => { 76 | dispatch(setEmployeeLoading()); 77 | axios 78 | .get(`/api/employee/single/${employeeId}`) 79 | .then(res => 80 | dispatch({ 81 | type: GET_EMPLOYEE, 82 | payload: res.data 83 | }) 84 | ) 85 | .catch(err => 86 | dispatch({ 87 | type: GET_ERRORS, 88 | payload: err.response.data 89 | }) 90 | ); 91 | }; 92 | 93 | export const editEmployee = (employeeId, employeeData) => dispatch => { 94 | return axios 95 | .put(`/api/employee/${employeeId}`, employeeData) 96 | .then(res => 97 | dispatch({ 98 | type: ADD_EMPLOYEE, 99 | payload: res.data 100 | }) 101 | ) 102 | .catch( 103 | err => 104 | dispatch({ 105 | type: GET_ERRORS, 106 | payload: err.response.data 107 | }), 108 | dispatch(() => { 109 | setTimeout(function() { 110 | dispatch(clearErrors()); 111 | }, 5000); 112 | }) 113 | ); 114 | }; 115 | 116 | //Set loading state 117 | export const setEmployeeLoading = () => { 118 | return { 119 | type: EMPLOYEE_LOADING 120 | }; 121 | }; 122 | 123 | //Clear errors 124 | const clearErrors = () => { 125 | return { 126 | type: CLEAR_ERRORS 127 | }; 128 | }; 129 | -------------------------------------------------------------------------------- /client/src/components/exception/Viewoneofpayment.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { deleteOneOffPayment } from "../../actions/exceptionActions"; 5 | import { toast } from "react-toastify"; 6 | import { confirmAlert } from "react-confirm-alert"; 7 | 8 | class ViewOtherException extends Component { 9 | onDelete(id) { 10 | confirmAlert({ 11 | title: "Delete this oneoff payment ?", 12 | message: "Are you sure to do this", 13 | buttons: [ 14 | { 15 | label: "Yes delete!", 16 | onClick: () => { 17 | this.props 18 | .deleteOneOffPayment(id) 19 | .then(res => { 20 | if (res.type === "DELETE_ONE_OFF_PAYMENT") { 21 | toast.success("Oneoff payment exception Deleted!"); 22 | } 23 | }) 24 | .catch(err => console.log(err)); 25 | } 26 | }, 27 | { 28 | label: "No cancel delete!", 29 | onClick: () => {} 30 | } 31 | ] 32 | }); 33 | } 34 | 35 | render() { 36 | const { oneoffpayment } = this.props; 37 | 38 | const formatMoney = money => { 39 | let formatedValue = money 40 | .toString() 41 | .replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"); 42 | return formatedValue; 43 | }; 44 | 45 | let exceptionContainer; 46 | 47 | if (Object.keys(oneoffpayment).length === 0) 48 | return (exceptionContainer =

There are no data in the system

); 49 | 50 | exceptionContainer = oneoffpayment.map(oneoffpaymmentItem => ( 51 |
55 |

56 | One off payment name: {oneoffpaymmentItem.name} 57 |

58 |

59 | Employee name: {oneoffpaymmentItem.employeeName} 60 |

61 |

62 | Exception type:{" "} 63 | {oneoffpaymmentItem.costType === "income" ? "Income" : "Deduction"} 64 |

65 |

66 | Amount : {" "} 67 | {formatMoney(oneoffpaymmentItem.amount)} 68 |

69 |

70 | Month of payment : {oneoffpaymmentItem.month} 71 |

72 |
73 | 79 |
80 |
81 | )); 82 | 83 | return
{exceptionContainer}
; 84 | } 85 | } 86 | 87 | ViewOtherException.propTypes = { 88 | oneoffpayment: PropTypes.array.isRequired, 89 | deleteOneOffPayment: PropTypes.func.isRequired 90 | }; 91 | 92 | export default connect( 93 | null, 94 | { deleteOneOffPayment } 95 | )(ViewOtherException); 96 | -------------------------------------------------------------------------------- /client/src/components/common/Utilities.js: -------------------------------------------------------------------------------- 1 | export const months = [ 2 | { name: "January", value: "January", _id: "January" }, 3 | { name: "February", value: "February", _id: "February" }, 4 | { name: "March", value: "March", _id: "March" }, 5 | { name: "April", value: "April", _id: "April" }, 6 | { name: "May", value: "May", _id: "May" }, 7 | { name: "June", value: "June", _id: "June" }, 8 | { name: "July", value: "July", _id: "July" }, 9 | { name: "August", value: "August", _id: "August" }, 10 | { name: "September", value: "September", _id: "September" }, 11 | { name: "October", value: "October", _id: "October" }, 12 | { name: "November", value: "November", _id: "November" }, 13 | { name: "December", value: "December", _id: "December" } 14 | ]; 15 | 16 | export const banks = [ 17 | { name: "Access Bank PLC", value: "Access Bank PLC", _id: "Access Bank PLC" }, 18 | { name: "CitiBank Nigeria Ltd", value: "CitiBank Nigeria Ltd", _id: "CitiBank Nigeria Ltd" }, 19 | { name: "Diamond Bank Plc", value: "Diamond Bank Plc", _id: "Diamond Bank Plc" }, 20 | { name: "EcoBank Nigeria Plc", value: "EcoBank Nigeria Plc", _id: "EcoBank Nigeria Plc" }, 21 | { name: "Fidelity Bank Plc", value: "Fidelity Bank Plc", _id: "Fidelity Bank Plc" }, 22 | { name: "First Bank Nigeria Ltd", value: "First Bank Nigeria Ltd", _id: "First Bank Nigeria Ltd" }, 23 | { name: "First City Monument Bank Plc", value: "First City Monument Bank Plc", _id: "First City Monument Bank Plc" }, 24 | { name: "Guarantee Trust Bank Plc", value: "Guarantee Trust Bank Plc", _id: "Guarantee Trust Bank Plc" }, 25 | { name: "Heritage Banking Company Ltd", value: "Heritage Banking Company Ltd", _id: "Heritage Banking Company Ltd" }, 26 | { name: "Key Stone Bank", value: "Key Stone Bank", _id: "Key Stone Bank" }, 27 | { name: "Polaris Bank", value: "Polaris Bank", _id: "Polaris Bank" }, 28 | { name: "Providus Bank", value: "Providus Bank", _id: "Providus Bank" }, 29 | { name: "Stanbic IBTC Bank Ltd", value: "Stanbic IBTC Bank Ltd", _id: "Stanbic IBTC Bank Ltd" }, 30 | { name: "Standard Chartered Bank Nigeria Ltd", value: "Standard Chartered Bank Nigeria Ltd", _id: "Standard Chartered Bank Nigeria Ltd" }, 31 | { name: "Sterling Bank Plc", value: "Sterling Bank Plc", _id: "Sterling Bank Plc" }, 32 | { name: "SunTrust Bank Nigeria Limited", value: "SunTrust Bank Nigeria Limited", _id: "SunTrust Bank Nigeria Limited" }, 33 | { name: "Union Bank of Nigeria Plc", value: "Union Bank of Nigeria Plc", _id: "Union Bank of Nigeria Plc" }, 34 | { name: "United Bank For Africa Plc", value: "United Bank For Africa Plc", _id: "United Bank For Africa Plc" }, 35 | { name: "Unity Bank Plc", value: "Unity Bank Plc", _id: "Unity Bank Plc" }, 36 | { name: "Wema Bank Plc", value: "Wema Bank Plc", _id: "Wema Bank Plc" }, 37 | { name: "Zenith Bank Plc", value: "Zenith Bank Plc", _id: "Zenith Bank Plc" } 38 | ] 39 | 40 | export const roles = [ 41 | {name: "Administrator", value: 0, _id: 0}, 42 | {name: "Super Administrator", value: 1, _id: 1} 43 | ] 44 | 45 | export const formatMoney = money => { 46 | let formatedValue = money 47 | .toString() 48 | .replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"); 49 | return formatedValue; 50 | }; 51 | -------------------------------------------------------------------------------- /client/src/components/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import SearchBar from "./SearchBar"; 3 | import SideBar from "./SideBar"; 4 | import Footer from "./Footer"; 5 | import { connect } from "react-redux"; 6 | import PropTypes from "prop-types"; 7 | import { getAnalytics, getNet } from "../../actions/dashActions"; 8 | import Spinner from "../common/Spinner"; 9 | import EmployeeRow from "./EmployeeRow"; 10 | import EmployeeCard from "./EmployeeCard"; 11 | import LevelCard from "./LevelCard"; 12 | import ExceptionCard from "./ExceptionCard"; 13 | import DeletedEmployeeCard from "./DeletedEmployeeCard"; 14 | import Netpay from "./Netpay"; 15 | import OtherPays from "./OtherPays"; 16 | import SalaryPay from "./SalaryPay"; 17 | 18 | class Dashboard extends Component { 19 | componentDidMount() { 20 | this.props.getAnalytics(); 21 | this.props.getNet(); 22 | } 23 | 24 | render() { 25 | const { dashboard, net, loading } = this.props.dashboard; 26 | 27 | let dashboardContent; 28 | 29 | if (dashboard === undefined || loading) { 30 | dashboardContent = ; 31 | } else { 32 | if (Object.keys(dashboard).length > 0 && Object.keys(net).length > 0) { 33 | dashboardContent = ( 34 | 35 |
36 | 37 | 38 | 39 | 40 |
41 |

Monthly Analysis

42 |
43 | 44 |
45 |

Yearly Analysis

46 |
47 | 52 | 57 |
58 | 59 |
60 | ); 61 | } else { 62 | dashboardContent =

No employees in system...

; 63 | } 64 | } 65 | 66 | return ( 67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 |

Dashboard

76 |
77 | {dashboardContent} 78 |
79 |
80 |
81 |
82 |
83 | ); 84 | } 85 | } 86 | 87 | Dashboard.propTypes = { 88 | getAnalytics: PropTypes.func.isRequired, 89 | getNet: PropTypes.func.isRequired, 90 | dashboard: PropTypes.object.isRequired 91 | }; 92 | 93 | const mapStateToProps = state => ({ 94 | dashboard: state.dashboard 95 | }); 96 | 97 | export default connect( 98 | mapStateToProps, 99 | { getAnalytics, getNet } 100 | )(Dashboard); 101 | -------------------------------------------------------------------------------- /client/src/components/exception/AddExceptionForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import TextFieldGroup from "../common/TextFieldGroup"; 5 | import SelectListGroup from "../common/SelectListGroup"; 6 | import Button from "../common/Button"; 7 | import { toast } from "react-toastify"; 8 | import { addException } from "../../actions/exceptionActions"; 9 | 10 | class AddExceptionForm extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | amount: "", 16 | employee: "", 17 | errors: {} 18 | }; 19 | 20 | this.onSubmit = this.onSubmit.bind(this); 21 | this.onChange = this.onChange.bind(this); 22 | } 23 | 24 | static getDerivedStateFromProps(nextProps, prevState) { 25 | if (nextProps.errors) { 26 | return { 27 | errors: nextProps.errors 28 | }; 29 | } 30 | } 31 | 32 | onSubmit(e) { 33 | e.preventDefault(); 34 | 35 | const exceptionDetails = { 36 | amount: this.state.amount, 37 | employee: this.state.employee 38 | }; 39 | 40 | this.props 41 | .addException(exceptionDetails) 42 | .then(res => { 43 | if (res.type === "ADD_EXCEPTION") { 44 | toast.success("Salary exception successfully added!"); 45 | this.setState({ 46 | amount: "", 47 | employee: "" 48 | }); 49 | } 50 | }) 51 | .catch(err => console.log(err)); 52 | } 53 | 54 | onChange(e) { 55 | this.setState({ [e.target.name]: e.target.value }); 56 | } 57 | 58 | render() { 59 | const { errors } = this.state; 60 | 61 | const { employees } = this.props; 62 | 63 | const exceptionFormContainer = ( 64 |
65 |

{errors.exception}

66 | 77 | 78 | 87 | 88 |
89 |
95 | 96 | ); 97 | 98 | return ( 99 |
100 |
101 |
102 |

103 | *All fields are required 104 |

105 |
106 |
{exceptionFormContainer}
107 |
108 |
109 | ); 110 | } 111 | } 112 | 113 | AddExceptionForm.propTypes = { 114 | employees: PropTypes.array.isRequired, 115 | addException: PropTypes.func.isRequired 116 | }; 117 | 118 | const mapStateToProps = state => ({ 119 | errors: state.errors 120 | }); 121 | 122 | export default connect( 123 | mapStateToProps, 124 | { addException } 125 | )(AddExceptionForm); 126 | -------------------------------------------------------------------------------- /client/src/components/payroll/MonthlySalary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import Spinner from "../common/Spinner"; 5 | import { getEmployees } from "../../actions/employeeActions"; 6 | import SearchBar from "../dashboard/SearchBar"; 7 | import SideBar from "../dashboard/SideBar"; 8 | import Footer from "../dashboard/Footer"; 9 | import MonthlySalaryTable from './MonthlySalaryTable'; 10 | import axios from 'axios'; 11 | import { toast } from "react-toastify"; 12 | import Button from '../common/Button'; 13 | 14 | class MonthlySalary extends Component { 15 | 16 | componentDidMount() { 17 | this.props.getEmployees(); 18 | } 19 | 20 | generateAll(employees){ 21 | let totalEmployees = employees.length 22 | let completeGeneration = 0; 23 | let failedGeneration = 0; 24 | 25 | let loadingBtn = document.querySelector('.loading'); 26 | let loadingComp = document.createElement("i") 27 | loadingComp.classList = "fas fa-circle-notch fa-spin" 28 | loadingBtn.innerHTML = "Generating " 29 | loadingBtn.appendChild(loadingComp) 30 | 31 | employees.forEach(employee => { 32 | axios.get(`/api/payslip/${employee._id}`) 33 | .then(res => { 34 | if(res.status === 200){ 35 | completeGeneration++; 36 | } else { 37 | failedGeneration++; 38 | } 39 | if(completeGeneration === totalEmployees){ 40 | toast.success('All payslip generated successfully!') 41 | loadingBtn.innerHTML = "Generate All Payslips" 42 | } 43 | if(failedGeneration !== 0){ 44 | toast.warn(`Could not generate ${failedGeneration} payroll of ${totalEmployees}`) 45 | loadingBtn.innerHTML = "Generate All Payslips" 46 | } 47 | }) 48 | .catch(err => console.log(err)) 49 | }) 50 | } 51 | 52 | render() { 53 | 54 | const { employees, loading } = this.props.employees; 55 | 56 | let employeeTable, generateBtn; 57 | let date = new Date(); 58 | let salaryDay = date.getDate(); 59 | 60 | if (employees === null || loading) { 61 | employeeTable = ; 62 | } else { 63 | if (Object.keys(employees).length > 0) { 64 | 65 | employeeTable = ; 66 | 67 | if(salaryDay >= 21){ 68 | generateBtn =
102 | 103 |
104 |
105 |
106 | 107 | {payslipTableContainer} 108 | 109 | 110 | 111 | 112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /client/src/components/payroll/all/Contribution.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import {connect} from 'react-redux'; 3 | import {getMonthly} from '../../../actions/payrollActions'; 4 | import {Link} from 'react-router-dom'; 5 | import PropTypes from 'prop-types'; 6 | import SearchBar from '../../dashboard/SearchBar'; 7 | import SideBar from '../../dashboard/SideBar'; 8 | import Footer from '../../dashboard/Footer'; 9 | import Spinner from '../../common/Spinner'; 10 | import ReactHTMLTableToExcel from 'react-html-table-to-excel'; 11 | 12 | class Contribution extends PureComponent { 13 | 14 | componentDidMount = () => { 15 | this.props.getMonthly() 16 | } 17 | 18 | render() { 19 | 20 | const {payrolls, loading} = this.props.payroll; 21 | 22 | const formatMoney = money => { 23 | let formatedValue = money 24 | .toString() 25 | .replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"); 26 | return formatedValue; 27 | }; 28 | 29 | let payrollContainer; 30 | 31 | if(payrolls === null || loading){ 32 | payrollContainer = 33 | } else { 34 | if(Object.keys(payrolls).length > 0){ 35 | 36 | payrollContainer = ( 37 |
38 | Back 39 | 46 | 47 |

Employees Pension

48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {payrolls.payslip.map(payrollItem => ( 59 | 60 | 61 | 62 | 63 | 64 | ))} 65 | 66 | 67 | 68 | 69 | 70 | 71 |
NameDesignationPension Contribution
{payrollItem.name}{payrollItem.designation}{formatMoney(payrollItem.pension.toFixed(2))}
Total Sum---{formatMoney(payrolls.pensionSum.toFixed(2))}
72 |
73 |
74 | ) 75 | }else { 76 | payrollContainer =

Employee monthly payslips haven't been generated!

77 | } 78 | } 79 | 80 | return ( 81 |
82 |
83 |
84 | 85 | 86 |
87 |
88 |
89 |

Payroll report

90 |
91 | {payrollContainer} 92 |
93 |
94 |
95 |
96 |
97 | ) 98 | } 99 | } 100 | 101 | Contribution.propTypes = { 102 | getMonthly: PropTypes.func.isRequired 103 | } 104 | 105 | const mapStateToProps = state => ({ 106 | payroll: state.payroll 107 | }) 108 | 109 | export default connect(mapStateToProps, {getMonthly})(Contribution); 110 | -------------------------------------------------------------------------------- /routes/api/individualcost.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const passport = require("passport"); 4 | const protect = passport.authenticate("jwt", { session: false }); 5 | 6 | //Validation rules 7 | const individualcostInput = require("../../validation/individualcost"); 8 | 9 | //load models 10 | const Individualcost = require("../../models/Individualcost"); 11 | const Employee = require("../../models/Employee"); 12 | 13 | //@route Post api/individualcost 14 | //@desc Create/Edit Employee individual exception route 15 | //@access Private 16 | router.post("/", protect, (req, res) => { 17 | const { errors, isValid } = individualcostInput(req.body); 18 | 19 | if (!isValid) { 20 | return res.status(400).json(errors); 21 | } 22 | 23 | const individualcostFields = {}; 24 | 25 | individualcostFields._id = req.body.id; 26 | 27 | if (req.body.name) individualcostFields.name = req.body.name; 28 | if (req.body.amount) individualcostFields.amount = req.body.amount; 29 | if (req.body.costType) individualcostFields.costType = req.body.costType; 30 | if (req.body.employee) individualcostFields.employee = req.body.employee; 31 | individualcostFields.employeeName = ""; 32 | 33 | Employee.findOne({ _id: req.body.employee }) 34 | .then(employee => { 35 | individualcostFields.employeeName = employee.name; 36 | 37 | Individualcost.findOne({ _id: req.body.id }) 38 | .then(individualCost => { 39 | if (individualCost) { 40 | //Update 41 | Individualcost.findOneAndUpdate( 42 | { _id: req.body.id }, 43 | { $set: individualcostFields }, 44 | { new: true } 45 | ) 46 | .then(updatedIndividualCost => res.json(updatedIndividualCost)) 47 | .catch(err => 48 | res 49 | .status(400) 50 | .json({ message: "Error updating this information" }) 51 | ); 52 | } else { 53 | //Create 54 | new Individualcost(individualcostFields) 55 | .save() 56 | .then(newIndividualCost => res.json(newIndividualCost)) 57 | .catch(err => 58 | res 59 | .status(400) 60 | .json({ message: "Error saving new employee exception" }) 61 | ); 62 | } 63 | }) 64 | .catch(err => console.log(err)); 65 | }) 66 | .catch(err => console.log(err)); 67 | }); 68 | 69 | //@route Get api/individualcost 70 | //@desc View Employee individual exception route 71 | //@access Private 72 | router.get("/", protect, (req, res) => { 73 | const errors = {}; 74 | Individualcost.find({ is_delete: 0 }) 75 | .then(individualCost => { 76 | if (!individualCost) { 77 | errors.individualCost = "There are no levels"; 78 | return res.status(404).json(errors); 79 | } 80 | res.json(individualCost); 81 | }) 82 | .catch(err => console.log(err)); 83 | }); 84 | 85 | //@route Post api/individualcost 86 | //@desc Move Employee individual exception to trash route 87 | //@access Private 88 | router.post('/:id', protect, (req, res) => { 89 | 90 | const individualCostFields = { 91 | is_delete: 1 92 | }; 93 | 94 | Individualcost.findOne({ _id: req.params.id }) 95 | .then(individualCostItem => { 96 | //Update 97 | Individualcost.findOneAndUpdate( 98 | { _id: req.params.id }, 99 | { $set: individualCostFields }, 100 | { new: true } 101 | ) 102 | .then(() => res.json({ success: true })) 103 | .catch(err => console.log(err)) 104 | }) 105 | .catch(err => res.status(404).json({message: "Error fetching individual exception record"})) 106 | }) 107 | 108 | //@route Delete api/individualcost 109 | //@desc Delete Employee individual exception route 110 | //@access Private 111 | router.delete("/:id", protect, (req, res) => { 112 | Individualcost.findOneAndRemove({ _id: req.params.id }) 113 | .then(() => res.json({ success: true })) 114 | .catch(err => console.log(err)); 115 | }); 116 | 117 | module.exports = router; 118 | -------------------------------------------------------------------------------- /routes/api/oneoffpayment.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const passport = require("passport"); 4 | const protect = passport.authenticate("jwt", { session: false }); 5 | 6 | //Load models 7 | const Oneoffpayment = require("../../models/Oneoffpayment"); 8 | const Employee = require("../../models/Employee"); 9 | 10 | //validation rules 11 | const oneoffpaymentInput = require("../../validation/oneoffpayment"); 12 | 13 | //@route Post api/oneoffpayment 14 | //@desc Create/Edit Employee oneoffpayment exception route 15 | //@access Private 16 | router.post("/", protect, (req, res) => { 17 | const { errors, isValid } = oneoffpaymentInput(req.body); 18 | 19 | if (!isValid) { 20 | return res.status(400).json(errors); 21 | } 22 | 23 | const oneoffPaymentFields = {}; 24 | 25 | oneoffPaymentFields._id = req.body.id; 26 | 27 | if (req.body.name) oneoffPaymentFields.name = req.body.name; 28 | if (req.body.amount) oneoffPaymentFields.amount = req.body.amount; 29 | if (req.body.month) oneoffPaymentFields.month = req.body.month; 30 | if (req.body.costType) oneoffPaymentFields.costType = req.body.costType; 31 | if (req.body.employee) oneoffPaymentFields.employee = req.body.employee; 32 | oneoffPaymentFields.employeeName = ""; 33 | 34 | Employee.findOne({ _id: req.body.employee }) 35 | .then(employee => { 36 | oneoffPaymentFields.employeeName = employee.name; 37 | 38 | Oneoffpayment.findOne({ _id: req.body.id }) 39 | .then(oneOffPaymentItem => { 40 | if (oneOffPaymentItem) { 41 | //Update 42 | Oneoffpayment.findOneAndUpdate( 43 | { _id: req.body.id }, 44 | { $set: oneoffPaymentFields }, 45 | { new: true } 46 | ) 47 | .then(updatedoneOffPayment => res.json(updatedoneOffPayment)) 48 | .catch(err => 49 | res 50 | .status(400) 51 | .json({ message: "Error updating this information" }) 52 | ); 53 | } else { 54 | //Create 55 | new Oneoffpayment(oneoffPaymentFields) 56 | .save() 57 | .then(newOneOffPayment => res.json(newOneOffPayment)) 58 | .catch(err => 59 | res 60 | .status(400) 61 | .json({ message: "Error saving new employee exception" }) 62 | ); 63 | } 64 | }) 65 | .catch(err => console.log(err)); 66 | }) 67 | .catch(err => console.log(err)); 68 | }); 69 | 70 | //@route Get api/oneoffpayment 71 | //@desc View Employee oneoffpayment exception route 72 | //@access Private 73 | router.get("/", protect, (req, res) => { 74 | const errors = {}; 75 | Oneoffpayment.find({ is_delete: 0 }) 76 | .then(oneOffPaymentItems => { 77 | if (!oneOffPaymentItems) { 78 | errors.oneoffpayment = "There are no one off payment records"; 79 | return res.status(404).json(errors); 80 | } 81 | res.json(oneOffPaymentItems); 82 | }) 83 | .catch(err => console.log(err)); 84 | }); 85 | 86 | //@route Post api/oneoffpayment 87 | //@desc Move Employee oneoffpayment exception to trash route 88 | //@access Private 89 | router.post("/:id", protect, (req, res) => { 90 | const oneoffPaymentFields = { 91 | is_delete: 1 92 | }; 93 | 94 | Oneoffpayment.findOne({ _id: req.params.id }) 95 | .then(oneOffPaymentItem => { 96 | //Update 97 | Oneoffpayment.findOneAndUpdate( 98 | { _id: req.params.id }, 99 | { $set: oneoffPaymentFields }, 100 | { new: true } 101 | ) 102 | .then(() => res.json({ success: true })) 103 | .catch(err => console.log(err)); 104 | }) 105 | .catch(err => 106 | res.status(404).json({ message: "Error fetching oneoff payment record" }) 107 | ); 108 | }); 109 | 110 | //@route Delete api/oneoffpayment 111 | //@desc Delete Employee oneoffpayment exception route 112 | //@access Private 113 | router.delete("/:id", protect, (req, res) => { 114 | Oneoffpayment.findOneAndDelete({ _id: req.params.id }) 115 | .then(() => res.json({ success: true })) 116 | .catch(err => console.log(err)); 117 | }); 118 | 119 | module.exports = router; 120 | -------------------------------------------------------------------------------- /client/src/components/level/AddLevelForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { toast } from "react-toastify"; 5 | import TextFieldGroup from "../common/TextFieldGroup"; 6 | import { addLevel } from "../../actions/levelActions"; 7 | import Button from "../common/Button"; 8 | 9 | class AddLevelForm extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | name: "", 15 | basic: "", 16 | description: "", 17 | errors: {} 18 | }; 19 | 20 | this.onChange = this.onChange.bind(this); 21 | this.onSubmit = this.onSubmit.bind(this); 22 | } 23 | 24 | static getDerivedStateFromProps(nextProps, prevState) { 25 | if (nextProps.errors) { 26 | return { 27 | errors: nextProps.errors 28 | }; 29 | } 30 | } 31 | 32 | onChange(e) { 33 | this.setState({ [e.target.name]: e.target.value }); 34 | } 35 | 36 | onSubmit(e) { 37 | e.preventDefault(); 38 | 39 | let loadingBtn = document.querySelector('.loading'); 40 | let loadingComp = document.createElement("i") 41 | loadingComp.classList = "fas fa-circle-notch fa-spin" 42 | loadingBtn.innerHTML = "Adding " 43 | loadingBtn.appendChild(loadingComp) 44 | 45 | const levelDetails = { 46 | name: this.state.name, 47 | basic: this.state.basic, 48 | description: this.state.description 49 | }; 50 | 51 | this.props 52 | .addLevel(levelDetails) 53 | .then(res => { 54 | if (res.type === "ADD_LEVEL") { 55 | toast.success("Level information successfully added!"); 56 | this.setState({ 57 | name: "", 58 | basic: "", 59 | description: "" 60 | }); 61 | 62 | loadingBtn.innerHTML = "Add Level" 63 | } 64 | }) 65 | .catch(err => console.log(err)); 66 | } 67 | 68 | render() { 69 | const { errors } = this.state; 70 | return ( 71 |
72 |
73 |
74 |

75 | *All fields are required 76 |

77 |
78 |
79 |
80 | 90 | 91 | 102 | 103 | 113 | 114 |
115 |
117 | 118 |
119 |
120 |
121 | ); 122 | } 123 | } 124 | 125 | AddLevelForm.propTypes = { 126 | addLevel: PropTypes.func.isRequired, 127 | errors: PropTypes.object.isRequired 128 | }; 129 | 130 | const mapStateToProps = state => ({ 131 | errors: state.errors 132 | }); 133 | 134 | export default connect( 135 | mapStateToProps, 136 | { addLevel } 137 | )(AddLevelForm); 138 | -------------------------------------------------------------------------------- /client/src/components/common/Spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | manager 3 | manager,officer,worker,admin,organizer,leader,pm,customer service,tie 4 | cc-by 5 | bhsz96 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 📜 Employee Payroll Management System Documentation 2 | 3 | ## 🔢 1.0 Level System 4 | 5 | This software uses a level system to calculate employee salaries. Before adding an employee to the system, you must add a level. 6 | 7 | **Important**: Enter the basic salary without commas. 8 | 9 | From the employee level page, you can edit and delete previously entered level information. An employee level cannot be deleted once an employee has been registered under that level. 10 | 11 | ### 💰 1.1 Level Bonuses 12 | 13 | From the employee level tabs, you can add level-wide bonuses to a specific level. These bonuses are added to the employee's salary monthly. All employees belonging to a level receive the bonuses attached to that level. 14 | 15 | **Action**: From the bonus tab, you can delete previously entered bonuses. 16 | 17 | ### ➖ 1.2 Level Deductions 18 | 19 | From the employee level tabs, you can add level-wide deductions to a specific level. These deductions are subtracted from the employee's salary monthly. All employees belonging to a level will have the deductions attached to that level subtracted from their salary. 20 | 21 | **Action**: From the deduction(s) tab, you can delete previously entered deductions. 22 | 23 | ### ❌ 1.3 Fixed Deductions 24 | 25 | This system generates payroll based on the PAYE tax system in Nigeria and calculates employee pension at a rate of 8% of the taxable income. You can add other company or state-based deductions through the level deduction option. 26 | 27 | ## 🧑‍💼 2.0 Employee Profile 28 | 29 | After adding an employee level, you can create an employee profile. This requires the employee's full name, a valid email, designation, department, and the employee level. All fields are mandatory. 30 | 31 | **Action**: From the view profiles page, you can edit or delete employee profiles. 32 | 33 | ## 💹 3.0 Salary Exception 34 | 35 | This feature ensures an employee's basic salary can be independent of their level. To add a basic salary exception, select the employee's name from the dropdown list. 36 | 37 | **Action**: You can view or delete previously entered basic salary exceptions. 38 | 39 | ### ➕➖ 3.1 Other Exceptions 40 | 41 | You can add other exceptions to an employee's payroll. These can be either incomes or deductions and are specific to the employee. Depending on the type, it's added or deducted from the salary monthly. 42 | 43 | **Action**: You can view or delete previously entered exceptions. 44 | 45 | ### 📅 3.2 One-Off Exceptions 46 | 47 | This feature allows adding a one-off exception to an employee's payroll. These can also be incomes or deductions. Select the month of payment—this payment or deduction is made only in that month and removed after a year. 48 | 49 | **Action**: You can view or delete previously entered one-off exceptions. 50 | 51 | ## 📊 4.0 Dashboard 52 | 53 | The dashboard provides an analytical overview of the system, aggregating database information. It displays: 54 | 55 | - Registered admins 56 | - Active employees 57 | - Deleted employees 58 | - Levels 59 | - Salary exceptions 60 | 61 | Charts include: 62 | 63 | - Monthly net and gross income line chart 64 | - Total net pay, consolidation relief allowance, and employee bonuses doughnut chart 65 | - Total tax, pension, and deductions pie chart 66 | 67 | **Highlight**: Overview of the last five registered employees with basic information. 68 | 69 | ## 📑 5.0 Payroll 70 | 71 | ### 📅 Monthly Report 72 | 73 | This module handles payroll calculations and generation. From the monthly report page, generate an employee payslip with one click. Generate all payslips after 21 days of a month. 74 | 75 | **Note**: Employee payroll can only be generated after 21 days of the current month. On the payslip page: 76 | 77 | - Download the payslip as a PDF 78 | - Print the payslip 79 | - Send a PDF copy to the employee's email 80 | 81 | ### 📈 All Report 82 | 83 | The all report page has control buttons for: 84 | 85 | - Payroll with pension 86 | - Employee pension 87 | - Employee tax 88 | 89 | You can filter records by employee name, month, and year, and export them in Excel format. 90 | 91 | ## 👥 6.0 Role Privileges 92 | 93 | ### 🦸‍♂️ Super Administrator 94 | 95 | This user has full access, including creating, editing, and deleting records. 96 | 97 | ### 👩‍💼 Administrator 98 | 99 | New accounts are assigned the administrator role by default. They cannot delete or create employee exceptions. 100 | -------------------------------------------------------------------------------- /client/src/components/level/AddBonusForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { addBonus } from "../../actions/levelActions"; 5 | import TextFieldGroup from "../common/TextFieldGroup"; 6 | import SelectListGroup from "../common/SelectListGroup"; 7 | import { toast } from "react-toastify"; 8 | 9 | class AddBonusForm extends Component { 10 | constructor() { 11 | super(); 12 | 13 | this.state = { 14 | name: "", 15 | amount: "", 16 | level: "", 17 | errors: {} 18 | }; 19 | 20 | this.onSubmit = this.onSubmit.bind(this); 21 | this.onChange = this.onChange.bind(this); 22 | } 23 | 24 | componentDidMount = () => { 25 | this.refs.addBtn.disabled = true 26 | } 27 | 28 | 29 | static getDerivedStateFromProps(nextProps, prevState) { 30 | if (nextProps.errors) { 31 | return { 32 | errors: nextProps.errors 33 | }; 34 | } 35 | } 36 | 37 | onChange(e) { 38 | this.setState({ [e.target.name]: e.target.value }, () => { 39 | if(this.state.level !== ''){ 40 | this.refs.addBtn.disabled = false; 41 | } else { 42 | this.refs.addBtn.disabled = true; 43 | } 44 | }); 45 | } 46 | 47 | onSubmit(e) { 48 | e.preventDefault(); 49 | 50 | const bonusDetails = { 51 | name: this.state.name, 52 | amount: this.state.amount, 53 | level: this.state.level 54 | }; 55 | 56 | let loadingComp = document.createElement("i") 57 | loadingComp.classList = "fas fa-circle-notch fa-spin" 58 | this.refs.addBtn.appendChild(loadingComp) 59 | 60 | this.props 61 | .addBonus(bonusDetails, this.state.level) 62 | .then(res => { 63 | if(res.type === 'VIEW_LEVELS'){ 64 | toast.success("Bonus successfully added!"); 65 | this.setState({ 66 | name: '', 67 | amount: '', 68 | level: '' 69 | }) 70 | this.refs.addBtn.disabled = true; 71 | } 72 | this.refs.addBtn.innerHTML = "Add Bonus " 73 | }) 74 | .catch(err => console.log(err)); 75 | } 76 | 77 | render() { 78 | const { errors } = this.state; 79 | 80 | const { levels } = this.props; 81 | 82 | const bonusFormContainer = ( 83 |
84 | 94 | 95 | 106 | 107 | 116 | 117 |
118 | 121 |
122 | 123 | ); 124 | 125 | return ( 126 |
127 |
128 |
129 |

130 | *All fields are required 131 |

132 |
133 |
{bonusFormContainer}
134 |
135 |
136 | ); 137 | } 138 | } 139 | 140 | AddBonusForm.propTypes = { 141 | addBonus: PropTypes.func.isRequired, 142 | levels: PropTypes.array.isRequired, 143 | errors: PropTypes.object.isRequired 144 | }; 145 | 146 | const mapStateToProps = state => ({ 147 | errors: state.errors 148 | }); 149 | 150 | export default connect( 151 | mapStateToProps, 152 | { addBonus } 153 | )(AddBonusForm); 154 | -------------------------------------------------------------------------------- /client/src/components/level/Level.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { getLevels } from "../../actions/levelActions"; 5 | import SearchBar from "../dashboard/SearchBar"; 6 | import SideBar from "../dashboard/SideBar"; 7 | import Footer from "../dashboard/Footer"; 8 | import LevelTab from "./LevelTab"; 9 | import AddLevelForm from "./AddLevelForm"; 10 | import ViewLevelTable from "./ViewLevelTable"; 11 | import AddBonusForm from "./AddBonusForm"; 12 | import ViewBonusTable from "./ViewBonusTable"; 13 | import Spinner from "../common/Spinner"; 14 | import AddDeductableForm from "./AddDeductableForm"; 15 | import ViewDeductableTable from "./ViewDeductableTable"; 16 | 17 | class Level extends Component { 18 | _isMounted = false; 19 | 20 | componentDidMount = () => { 21 | this.props.getLevels(); 22 | }; 23 | 24 | render() { 25 | let levelContainer; 26 | 27 | const { levels, loading } = this.props.levels; 28 | 29 | if (levels === null || loading) { 30 | levelContainer = ; 31 | } else { 32 | levelContainer = ( 33 |
34 |
40 | 41 |
42 |
48 | 49 |
50 |
56 | 57 |
58 |
64 | 65 |
66 |
72 | 73 |
74 |
80 | 81 |
82 |
83 | ); 84 | } 85 | 86 | return ( 87 |
88 |
89 |
90 | 91 | 92 |
93 |
94 |
95 |

Employee Level Section

96 |
97 |
98 |
99 |
100 |

101 | From the dropdown navs, you can attach a bonus or a 102 | deductible to a level 103 |

104 |
105 |
106 |
107 | 108 |
109 | {levelContainer} 110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | ); 121 | } 122 | } 123 | 124 | Level.propTypes = { 125 | levels: PropTypes.object.isRequired, 126 | getLevels: PropTypes.func.isRequired 127 | }; 128 | 129 | const mapStateToProps = state => ({ 130 | levels: state.levels 131 | }); 132 | 133 | export default connect( 134 | mapStateToProps, 135 | { getLevels } 136 | )(Level); 137 | -------------------------------------------------------------------------------- /client/src/components/auth/Forgot.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Link } from "react-router-dom"; 4 | import PropTypes from "prop-types"; 5 | import { sendResetLink } from "../../actions/authActions"; 6 | import TextFieldGroup from "../common/TextFieldGroup"; 7 | import Button from "../common/Button"; 8 | import { toast } from "react-toastify"; 9 | 10 | class Forgot extends Component { 11 | constructor() { 12 | super(); 13 | 14 | this.state = { 15 | email: "", 16 | errors: {} 17 | }; 18 | 19 | this.onChange = this.onChange.bind(this); 20 | this.onSubmit = this.onSubmit.bind(this); 21 | } 22 | 23 | static getDerivedStateFromProps(nextProps, prevState) { 24 | if (nextProps.auth.isAuthenticated) { 25 | nextProps.history.push("/dashboard"); 26 | } 27 | if (nextProps.errors) { 28 | return { 29 | errors: nextProps.errors 30 | }; 31 | } 32 | } 33 | 34 | onChange(e) { 35 | this.setState({ [e.target.name]: e.target.value }); 36 | } 37 | 38 | onSubmit(e) { 39 | e.preventDefault(); 40 | 41 | const userEmail = { 42 | email: this.state.email 43 | }; 44 | 45 | let loadingBtn = document.querySelector(".loading"); 46 | let loadingComp = document.createElement("i"); 47 | loadingComp.classList = "fas fa-circle-notch fa-spin"; 48 | loadingBtn.innerHTML = "Sending "; 49 | loadingBtn.appendChild(loadingComp); 50 | 51 | this.props 52 | .sendResetLink(userEmail) 53 | .then(res => { 54 | if (res.type === "GET_SUCCESS") { 55 | toast.success("Password reset link sent, check your inbox!"); 56 | } 57 | if (res.type === "GET_ERRORS") { 58 | loadingBtn.innerHTML = "Send Reset Link"; 59 | } 60 | }) 61 | .catch(err => console.log(err)); 62 | } 63 | 64 | render() { 65 | const { errors } = this.state; 66 | 67 | return ( 68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |

Reset Password

77 |
78 |

79 | *We will email you a password reset link 80 |

81 |
82 |
83 | 93 | 94 |

95 | Back to Login 96 |

97 |
98 |
104 | 105 |
106 |
107 |
108 | Copyright © Payeroll 2018 109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | ); 117 | } 118 | } 119 | 120 | Forgot.propTypes = { 121 | sendResetLink: PropTypes.func.isRequired, 122 | auth: PropTypes.object.isRequired, 123 | errors: PropTypes.object.isRequired 124 | }; 125 | 126 | const mapStateToProps = state => ({ 127 | auth: state.auth, 128 | errors: state.errors 129 | }); 130 | 131 | export default connect( 132 | mapStateToProps, 133 | { sendResetLink } 134 | )(Forgot); 135 | -------------------------------------------------------------------------------- /client/src/components/level/AddDeductableForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { addDeductable } from "../../actions/levelActions"; 5 | import TextFieldGroup from "../common/TextFieldGroup"; 6 | import SelectListGroup from "../common/SelectListGroup"; 7 | import { toast } from "react-toastify"; 8 | 9 | class AddDeductableForm extends Component { 10 | constructor() { 11 | super(); 12 | 13 | this.state = { 14 | name: "", 15 | amount: "", 16 | level: "", 17 | errors: {} 18 | }; 19 | 20 | this.onSubmit = this.onSubmit.bind(this); 21 | this.onChange = this.onChange.bind(this); 22 | } 23 | 24 | componentDidMount = () => { 25 | this.refs.addBtn.disabled = true; 26 | } 27 | 28 | static getDerivedStateFromProps(nextProps, prevState) { 29 | if (nextProps.errors) { 30 | return { 31 | errors: nextProps.errors 32 | }; 33 | } 34 | } 35 | 36 | onChange(e) { 37 | this.setState({ [e.target.name]: e.target.value }, () => { 38 | if(this.state.level !== ''){ 39 | this.refs.addBtn.disabled = false; 40 | } else { 41 | this.refs.addBtn.disabled = true; 42 | } 43 | }); 44 | } 45 | 46 | onSubmit(e) { 47 | e.preventDefault(); 48 | 49 | const deductableDetails = { 50 | name: this.state.name, 51 | amount: this.state.amount, 52 | level: this.state.level 53 | }; 54 | 55 | let loadingComp = document.createElement("i") 56 | loadingComp.classList = "fas fa-circle-notch fa-spin" 57 | this.refs.addBtn.appendChild(loadingComp) 58 | 59 | this.props 60 | .addDeductable(deductableDetails, this.state.level) 61 | .then(res => { 62 | if (res.type === 'VIEW_LEVELS') { 63 | toast.success("Deductable successfully added!"); 64 | this.setState({ 65 | name: '', 66 | amount: '', 67 | level: '' 68 | }) 69 | this.refs.addBtn.disabled = true; 70 | } 71 | this.refs.addBtn.innerHTML = "Add Deductable " 72 | }) 73 | .catch(err => console.log(err)); 74 | } 75 | render() { 76 | const { errors } = this.state; 77 | 78 | const { levels } = this.props; 79 | 80 | const deductableFormContainer = ( 81 |
82 | 92 | 93 | 104 | 105 | 114 | 115 |
116 | 119 |
120 | 121 | ); 122 | 123 | return ( 124 |
125 |
126 |
127 |

128 | *All fields are required 129 |

130 |
131 |
{deductableFormContainer}
132 |
133 |
134 | ); 135 | } 136 | } 137 | 138 | AddDeductableForm.propTypes = { 139 | addDeductable: PropTypes.func.isRequired, 140 | levels: PropTypes.array.isRequired, 141 | errors: PropTypes.object.isRequired 142 | }; 143 | 144 | const mapStateToProps = state => ({ 145 | errors: state.errors 146 | }); 147 | 148 | export default connect( 149 | mapStateToProps, 150 | { addDeductable } 151 | )(AddDeductableForm); 152 | -------------------------------------------------------------------------------- /client/src/components/exception/AddOtherExceptionForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import TextFieldGroup from "../common/TextFieldGroup"; 5 | import SelectListGroup from "../common/SelectListGroup"; 6 | import { toast } from "react-toastify"; 7 | import { addOtherException } from "../../actions/exceptionActions"; 8 | import Button from "../common/Button"; 9 | 10 | class AddOtherExceptionForm extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | name: "", 16 | amount: "", 17 | employee: "", 18 | costType: "", 19 | errors: {} 20 | }; 21 | 22 | this.onSubmit = this.onSubmit.bind(this); 23 | this.onChange = this.onChange.bind(this); 24 | } 25 | 26 | static getDerivedStateFromProps(nextProps, prevState) { 27 | if (nextProps.errors) { 28 | return { 29 | errors: nextProps.errors 30 | }; 31 | } 32 | } 33 | 34 | onSubmit(e) { 35 | e.preventDefault(); 36 | 37 | const exceptionDetails = { 38 | name: this.state.name, 39 | amount: this.state.amount, 40 | employee: this.state.employee, 41 | costType: this.state.costType 42 | }; 43 | 44 | this.props 45 | .addOtherException(exceptionDetails) 46 | .then(res => { 47 | if (res.type === "ADD_OTHER_EXCEPTION") { 48 | toast.success("Other exception successfully added!"); 49 | this.setState({ 50 | name: "", 51 | amount: "", 52 | employee: "", 53 | costType: "" 54 | }); 55 | } 56 | }) 57 | .catch(err => console.log(err)); 58 | } 59 | 60 | onChange(e) { 61 | this.setState({ [e.target.name]: e.target.value }); 62 | } 63 | 64 | render() { 65 | const { errors } = this.state; 66 | 67 | const { employees } = this.props; 68 | 69 | const options = [ 70 | { name: "Income", value: "income", _id: "income" }, 71 | { name: "Deduction", value: "deduction", _id: "deduction" } 72 | ]; 73 | 74 | const exceptionFormContainer = ( 75 |
76 |

{errors.exception}

77 | 78 | 88 | 89 | 100 | 101 | 110 | 111 | 119 | 120 |
121 |
123 | 124 | ); 125 | 126 | return ( 127 |
128 |
129 |
130 |

131 | *All fields are required 132 |

133 |
134 |
{exceptionFormContainer}
135 |
136 |
137 | ); 138 | } 139 | } 140 | 141 | AddOtherExceptionForm.propTypes = { 142 | employees: PropTypes.array.isRequired, 143 | addOtherException: PropTypes.func.isRequired 144 | }; 145 | 146 | const mapStateToProps = state => ({ 147 | errors: state.errors 148 | }); 149 | 150 | export default connect( 151 | mapStateToProps, 152 | { addOtherException } 153 | )(AddOtherExceptionForm); 154 | -------------------------------------------------------------------------------- /routes/api/record.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const passport = require('passport'); 4 | const protect = passport.authenticate('jwt', { session: false }); 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const multer = require('multer'); 9 | const excelToJson = require('convert-excel-to-json'); 10 | const directory = 'docs/'; 11 | const Employee = require('../../models/Employee'); 12 | const Level = require('../../models/Level'); 13 | 14 | const storage = multer.diskStorage({ 15 | destination: (req, file, cb) => { 16 | if (!fs.existsSync(directory)) { 17 | fs.mkdirSync(directory); 18 | cb(null, 'docs/'); 19 | } else { 20 | cb(null, 'docs/'); 21 | } 22 | }, 23 | filename: (req, file, cb) => { 24 | cb(null, Date.now() + '-' + file.originalname); 25 | }, 26 | }); 27 | 28 | const upload = multer({ 29 | storage, 30 | fileFilter: (req, file, callback) => { 31 | if ( 32 | ['xls', 'xlsx', 'NUMBERS'].indexOf( 33 | file.originalname.split('.')[ 34 | file.originalname.split('.').length - 1 35 | ] 36 | ) === -1 37 | ) { 38 | return callback(new Error('Wrong extension type')); 39 | } 40 | callback(null, true); 41 | }, 42 | }); 43 | 44 | router.post('/', protect, upload.single('file'), (req, res) => { 45 | let recordPath = path.join( 46 | __basedir, 47 | '../../', 48 | 'docs/' + req.file.filename 49 | ); 50 | importToDb(recordPath); 51 | fs.unlinkSync(recordPath); 52 | res.json({ 53 | msg: 'Employee record uploaded and successfully imported', 54 | file: req.file, 55 | }); 56 | }); 57 | 58 | router.get('/download', (req, res) => { 59 | let template = path.join( 60 | __basedir, 61 | '../../', 62 | 'docs/Employee_Record_Template.xlsx' 63 | ); 64 | res.download(template); 65 | }); 66 | 67 | const importToDb = (filePath) => { 68 | console.log(filePath); 69 | const excelData = excelToJson({ 70 | sourceFile: filePath, 71 | sheets: [ 72 | { 73 | name: 'Employees', 74 | header: { 75 | rows: 1, 76 | }, 77 | columnToKey: { 78 | A: 'name', 79 | B: 'email', 80 | C: 'designation', 81 | D: 'department', 82 | E: 'stateResidence', 83 | F: 'bankName', 84 | G: 'accountNumber', 85 | H: 'pfaName', 86 | I: 'pensionAccountNumber', 87 | J: 'levelName', 88 | }, 89 | }, 90 | ], 91 | }); 92 | 93 | excelData.Employees.forEach((employee) => { 94 | let name = employee.name; 95 | let email = employee.email; 96 | let designation = employee.designation; 97 | let department = employee.department; 98 | let stateResidence = employee.stateResidence; 99 | let bankName = employee.bankName; 100 | bankName = bankName.toUpperCase(); 101 | let accountNumber = employee.accountNumber; 102 | let pfaName = employee.pfaName; 103 | let pensionAccountNumber = employee.pensionAccountNumber; 104 | let levelName = employee.levelName; 105 | let fTag = 106 | Math.random().toString(9).substring(2, 7) + 107 | Math.random().toString(36).substring(2, 4); 108 | let tag = 'EMP' + fTag; 109 | 110 | Level.findOne({ name: employee.levelName }) 111 | .where('is_delete') 112 | .equals(0) 113 | .then((level) => { 114 | if (level != null) { 115 | Employee.findOne({ email }) 116 | .where('is_delete') 117 | .equals(0) 118 | .then((employeeRecord) => { 119 | if (!employeeRecord) { 120 | const newEmployee = new Employee({ 121 | tag, 122 | name, 123 | email, 124 | designation, 125 | department, 126 | level: level._id, 127 | stateResidence, 128 | bankName, 129 | accountNumber, 130 | pfaName, 131 | pensionAccountNumber, 132 | levelName, 133 | }); 134 | newEmployee 135 | .save() 136 | .then((employeeDetails) => { 137 | console.log(employeeDetails); 138 | }) 139 | .catch((err) => console.log(err)); 140 | } 141 | }) 142 | .catch((err) => console.log(err)); 143 | } 144 | }) 145 | .catch((err) => console.log(err)); 146 | }); 147 | }; 148 | 149 | module.exports = router; 150 | -------------------------------------------------------------------------------- /client/src/components/payroll/MonthlySalaryTable.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import TextFieldGroup from '../common/TextFieldGroup'; 4 | import SelectListGroup from '../common/SelectListGroup'; 5 | import Pagination from '../common/Pagination'; 6 | 7 | class MonthlySalaryTable extends Component { 8 | 9 | constructor(props){ 10 | super(props) 11 | 12 | this.state = { 13 | search: '', 14 | currentPage: 1, 15 | employeePerPage: "5" 16 | } 17 | 18 | this.onChange = this.onChange.bind(this) 19 | } 20 | 21 | onChange(e){ 22 | this.setState({ [e.target.name]: e.target.value }, () => { 23 | let text = this.state.search.toLowerCase() 24 | document.querySelectorAll('#search-item').forEach(table => { 25 | const item = table.firstChild.textContent; 26 | if(item.toLowerCase().indexOf(text) !== -1){ 27 | table.style.display = 'table-row' 28 | } else { 29 | table.style.display = 'none'; 30 | } 31 | }) 32 | }); 33 | } 34 | 35 | paginate(pageNumber) { 36 | this.setState({ 37 | currentPage: pageNumber 38 | }) 39 | } 40 | 41 | render() { 42 | const { employees } = this.props; 43 | const { currentPage, employeePerPage } = this.state; 44 | 45 | const indexOfLastEmployee = currentPage * employeePerPage; 46 | const indexOfFirstEmployee = indexOfLastEmployee - employeePerPage; 47 | const currentEmployee = employees.slice(indexOfFirstEmployee, indexOfLastEmployee); 48 | let paginateVisibility = parseInt(employeePerPage); 49 | let recordGroup = [ 50 | { _id: "5", name: "5" }, 51 | { name: "10", _id: "10" }, 52 | { name: "20", _id: "20" }, 53 | { name: "30", _id: "30" } 54 | ] 55 | 56 | let employeeDetails = currentEmployee.map(employee => ( 57 | 58 | {employee.name} 59 | {employee.levelName} 60 | {employee.department} 61 | {employee.designation} 62 | 63 | 67 | View 68 | 69 | 70 | 71 | )); 72 | 73 | return ( 74 |
75 |
76 |
77 |
78 |

View to generate/export individual employee payslip

79 |
80 |
81 |
82 |
83 | 93 |
94 |
95 | 103 |
104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | {employeeDetails} 117 |
NameLevelDepartmentDesignationAction
118 |
119 | {employees.length < paginateVisibility ? '' : ()} 120 |
121 |
122 |
123 |
124 | ); 125 | } 126 | } 127 | 128 | export default MonthlySalaryTable; 129 | -------------------------------------------------------------------------------- /client/src/actions/payrollActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | VIEW_PAYROLL, 4 | VIEW_MONTHLY_PAYROLL, 5 | VIEW_PAYROLL_RECORDS, 6 | VIEW_PAYROLL_RECORDS_MONTHLY, 7 | VIEW_PAYROLL_RECORDS_YEARLY, 8 | VIEW_EMPLOYEE_MONTH_YEAR, 9 | VIEW_MONTH_YEAR, 10 | PAYROLL_LOADING, 11 | GET_ERRORS, 12 | CLEAR_ERRORS 13 | } from "./types"; 14 | 15 | //View single payroll 16 | export const getPayroll = id => dispatch => { 17 | dispatch(setPayrollLoading()); 18 | return axios 19 | .get(`/api/payslip/${id}`) 20 | .then(res => 21 | dispatch({ 22 | type: VIEW_PAYROLL, 23 | payload: res.data 24 | }) 25 | ) 26 | .catch(err => 27 | dispatch({ 28 | type: GET_ERRORS, 29 | payload: err.response.data 30 | }) 31 | ); 32 | }; 33 | 34 | //Get aggregate monthly payroll 35 | export const getMonthly = () => dispatch => { 36 | dispatch(setPayrollLoading()); 37 | axios 38 | .get("/api/payslip/record/allmonthlyslip") 39 | .then(res => 40 | dispatch({ 41 | type: VIEW_MONTHLY_PAYROLL, 42 | payload: res.data 43 | }) 44 | ) 45 | .catch(err => 46 | dispatch({ 47 | type: GET_ERRORS, 48 | payload: err.response.data 49 | }) 50 | ); 51 | }; 52 | 53 | //Get employee payslip within a calendar year 54 | export const getEmployeeYearlySlip = id => dispatch => { 55 | dispatch(setPayrollLoading()); 56 | return axios 57 | .get(`/api/payslip/record/employeeallslip/${id}`) 58 | .then(res => 59 | dispatch({ 60 | type: VIEW_PAYROLL_RECORDS, 61 | payload: res.data 62 | }) 63 | ) 64 | .catch(err => 65 | dispatch({ 66 | type: GET_ERRORS, 67 | payload: err.response.data 68 | }) 69 | ); 70 | }; 71 | 72 | //Get all employee payslip within a calendar year 73 | export const getAllYearlyPayslip = () => dispatch => { 74 | dispatch(setPayrollLoading()); 75 | return axios 76 | .get("/api/payslip/record/allyear") 77 | .then(res => 78 | dispatch({ 79 | type: VIEW_PAYROLL_RECORDS_YEARLY, 80 | payload: res.data 81 | }) 82 | ) 83 | .catch(err => 84 | dispatch({ 85 | type: GET_ERRORS, 86 | payload: err.response.data 87 | }) 88 | ); 89 | }; 90 | 91 | //Get monthly employee payslip within a calendar year 92 | export const getMonthlyPayslip = payslipData => dispatch => { 93 | dispatch(clearErrors()); 94 | return axios 95 | .post("/api/payslip/record/singlemonthlyslip", payslipData) 96 | .then(res => 97 | dispatch({ 98 | type: VIEW_PAYROLL_RECORDS_MONTHLY, 99 | payload: res.data 100 | }) 101 | ) 102 | .catch( 103 | err => 104 | dispatch({ 105 | type: GET_ERRORS, 106 | payload: err.response.data 107 | }), 108 | dispatch(() => { 109 | setTimeout(function() { 110 | dispatch(clearErrors()); 111 | }, 5000); 112 | }) 113 | ); 114 | }; 115 | 116 | //Get employee payslip by employee, month and year 117 | export const getEmployeeMonthYear = payslipData => dispatch => { 118 | dispatch(clearErrors); 119 | return axios 120 | .post("/api/payslip/record/byemployeemonthyear", payslipData) 121 | .then(res => 122 | dispatch({ 123 | type: VIEW_EMPLOYEE_MONTH_YEAR, 124 | payload: res.data 125 | }) 126 | ) 127 | .catch( 128 | err => 129 | dispatch({ 130 | type: GET_ERRORS, 131 | payload: err.response.data 132 | }), 133 | dispatch(() => { 134 | setTimeout(function() { 135 | dispatch(clearErrors()); 136 | }, 5000); 137 | }) 138 | ); 139 | }; 140 | 141 | //Get employee payslip by month and year 142 | export const getMonthYear = payslipData => dispatch => { 143 | dispatch(clearErrors()); 144 | return axios 145 | .post("/api/payslip/record/bymonthyear", payslipData) 146 | .then(res => 147 | dispatch({ 148 | type: VIEW_MONTH_YEAR, 149 | payload: res.data 150 | }) 151 | ) 152 | .catch( 153 | err => 154 | dispatch({ 155 | type: GET_ERRORS, 156 | payload: err.response.data 157 | }), 158 | dispatch(() => { 159 | setTimeout(function() { 160 | dispatch(clearErrors()); 161 | }, 5000); 162 | }) 163 | ); 164 | }; 165 | 166 | //Set loding state 167 | export const setPayrollLoading = () => { 168 | return { 169 | type: PAYROLL_LOADING 170 | }; 171 | }; 172 | 173 | //Clear errors 174 | const clearErrors = () => { 175 | return { 176 | type: CLEAR_ERRORS 177 | }; 178 | }; 179 | -------------------------------------------------------------------------------- /client/src/components/auth/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { PropTypes } from "prop-types"; 4 | import { connect } from "react-redux"; 5 | import { loginUser } from "../../actions/authActions"; 6 | 7 | import TextFieldGroup from "../common/TextFieldGroup"; 8 | import Button from "../common/Button"; 9 | 10 | class Login extends Component { 11 | constructor() { 12 | super(); 13 | 14 | this.state = { 15 | email: "", 16 | password: "", 17 | errors: {} 18 | }; 19 | 20 | this.onChange = this.onChange.bind(this); 21 | this.onSubmit = this.onSubmit.bind(this); 22 | } 23 | 24 | static getDerivedStateFromProps(nextProps, prevState) { 25 | if (nextProps.auth.isAuthenticated) { 26 | nextProps.history.push("/dashboard"); 27 | } 28 | if (nextProps.errors) { 29 | return { 30 | errors: nextProps.errors 31 | }; 32 | } 33 | } 34 | 35 | onSubmit(e) { 36 | e.preventDefault(); 37 | 38 | let loadingBtn = document.querySelector(".loading"); 39 | let loadingComp = document.createElement("i"); 40 | loadingComp.classList = "fas fa-circle-notch fa-spin"; 41 | loadingBtn.innerHTML = "Login "; 42 | loadingBtn.appendChild(loadingComp); 43 | 44 | const loginData = { 45 | email: this.state.email, 46 | password: this.state.password 47 | }; 48 | 49 | this.props 50 | .loginUser(loginData) 51 | .then(res => { 52 | if (res && res.type === "GET_ERRORS") { 53 | loadingBtn.innerHTML = "Login"; 54 | } 55 | }) 56 | .catch(err => console.log(err)); 57 | } 58 | 59 | onChange(e) { 60 | this.setState({ [e.target.name]: e.target.value }); 61 | } 62 | 63 | render() { 64 | const { errors } = this.state; 65 | 66 | return ( 67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |

Login

76 |
77 |
78 |
79 | 89 | 90 | 100 |

101 | Forgot password ?{" "} 102 | reset 103 |

104 |
105 |
111 | 112 |
113 |
114 |

115 | Don't have an account?{" "} 116 | Create One 117 |

118 |
119 |
120 |
121 | Copyright © Payeroll 2018 122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | ); 130 | } 131 | } 132 | 133 | Login.propTypes = { 134 | loginUser: PropTypes.func.isRequired, 135 | auth: PropTypes.object.isRequired, 136 | errors: PropTypes.object.isRequired 137 | }; 138 | 139 | const mapStateToProps = state => ({ 140 | auth: state.auth, 141 | errors: state.errors 142 | }); 143 | 144 | export default connect( 145 | mapStateToProps, 146 | { loginUser } 147 | )(Login); 148 | -------------------------------------------------------------------------------- /client/src/components/exception/Addoneoffpayment.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { months } from "../common/Utilities"; 5 | import TextFieldGroup from "../common/TextFieldGroup"; 6 | import SelectListGroup from "../common/SelectListGroup"; 7 | import Button from "../common/Button"; 8 | import { toast } from "react-toastify"; 9 | import { addOneOffPayment } from "../../actions/exceptionActions"; 10 | 11 | class Addoneoffpayment extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | name: "", 17 | amount: "", 18 | employee: "", 19 | costType: "", 20 | month: "", 21 | errors: {} 22 | }; 23 | 24 | this.onSubmit = this.onSubmit.bind(this); 25 | this.onChange = this.onChange.bind(this); 26 | } 27 | 28 | static getDerivedStateFromProps(nextProps, prevState) { 29 | if (nextProps.errors) { 30 | return { 31 | errors: nextProps.errors 32 | }; 33 | } 34 | } 35 | 36 | onSubmit(e) { 37 | e.preventDefault(); 38 | 39 | const exceptionDetails = { 40 | name: this.state.name, 41 | amount: this.state.amount, 42 | employee: this.state.employee, 43 | costType: this.state.costType, 44 | month: this.state.month 45 | }; 46 | 47 | this.props 48 | .addOneOffPayment(exceptionDetails) 49 | .then(res => { 50 | if (res.type === "ADD_ONE_OFF_PAYMENT") { 51 | toast.success("One off payment successfully added!"); 52 | this.setState({ 53 | name: "", 54 | amount: "", 55 | employee: "", 56 | costType: "", 57 | month: "" 58 | }); 59 | } 60 | }) 61 | .catch(err => console.log(err)); 62 | } 63 | 64 | onChange(e) { 65 | this.setState({ [e.target.name]: e.target.value }); 66 | } 67 | 68 | render() { 69 | const { errors } = this.state; 70 | 71 | const { employees } = this.props; 72 | 73 | const options = [ 74 | { name: "Income", value: "income", _id: "income" }, 75 | { name: "Deduction", value: "deduction", _id: "deduction" } 76 | ]; 77 | 78 | const exceptionFormContainer = ( 79 |
80 | 81 | 91 | 92 | 103 | 104 | 113 | 114 | 122 | 123 | 131 | 132 |
133 |
135 | 136 | ); 137 | 138 | return ( 139 |
140 |
141 |
142 |

143 | *All fields are required 144 |

145 |
146 |
147 |

*Added one off payments are automatically disabled in the system after a year

148 | {exceptionFormContainer} 149 |
150 |
151 |
152 | ); 153 | } 154 | } 155 | 156 | Addoneoffpayment.propTypes = { 157 | employees: PropTypes.array.isRequired, 158 | addOneOffPayment: PropTypes.func.isRequired 159 | }; 160 | 161 | const mapStateToProps = state => ({ 162 | errors: state.errors 163 | }); 164 | 165 | export default connect( 166 | mapStateToProps, 167 | { addOneOffPayment } 168 | )(Addoneoffpayment); 169 | -------------------------------------------------------------------------------- /client/public/js/tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2017 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(a,b){'object'==typeof exports&&'undefined'!=typeof module?module.exports=b(require('popper.js')):'function'==typeof define&&define.amd?define(['popper.js'],b):a.Tooltip=b(a.Popper)})(this,function(a){'use strict';function b(a){return a&&'[object Function]'==={}.toString.call(a)}a=a&&'default'in a?a['default']:a;var c=function(a,b){if(!(a instanceof b))throw new TypeError('Cannot call a class as a function')},d=function(){function a(a,b){for(var c,d=0;d
',trigger:'hover focus',offset:0},g=function(){function g(a,b){c(this,g),h.call(this),b=e({},f,b),a.jquery&&(a=a[0]),this.reference=a,this.options=b;var d='string'==typeof b.trigger?b.trigger.split(' ').filter(function(a){return-1!==['click','hover','focus'].indexOf(a)}):[];this._isOpen=!1,this._popperOptions={},this._setEventListeners(a,d,b)}return d(g,[{key:'_create',value:function(a,c,d,e){var f=window.document.createElement('div');f.innerHTML=c.trim();var g=f.childNodes[0];g.id='tooltip_'+Math.random().toString(36).substr(2,10),g.setAttribute('aria-hidden','false');var h=f.querySelector(this.innerSelector);if(1===d.nodeType||11===d.nodeType)e&&h.appendChild(d);else if(b(d)){var i=d.call(a);e?h.innerHTML=i:h.innerText=i}else e?h.innerHTML=d:h.innerText=d;return g}},{key:'_show',value:function(b,c){if(this._isOpen&&!this._isOpening)return this;if(this._isOpen=!0,this._tooltipNode)return this._tooltipNode.style.display='',this._tooltipNode.setAttribute('aria-hidden','false'),this.popperInstance.update(),this;var d=b.getAttribute('title')||c.title;if(!d)return this;var f=this._create(b,c.template,d,c.html);b.setAttribute('aria-describedby',f.id);var g=this._findContainer(c.container,b);return this._append(f,g),this._popperOptions=e({},c.popperOptions,{placement:c.placement}),this._popperOptions.modifiers=e({},this._popperOptions.modifiers,{arrow:{element:this.arrowSelector},offset:{offset:c.offset}}),c.boundariesElement&&(this._popperOptions.modifiers.preventOverflow={boundariesElement:c.boundariesElement}),this.popperInstance=new a(b,f,this._popperOptions),this._tooltipNode=f,this}},{key:'_hide',value:function(){return this._isOpen?(this._isOpen=!1,this._tooltipNode.style.display='none',this._tooltipNode.setAttribute('aria-hidden','true'),this):this}},{key:'_dispose',value:function(){var a=this;return this._events.forEach(function(b){var c=b.func,d=b.event;a.reference.removeEventListener(d,c)}),this._events=[],this._tooltipNode&&(this._hide(),this.popperInstance.destroy(),!this.popperInstance.options.removeOnDestroy&&(this._tooltipNode.parentNode.removeChild(this._tooltipNode),this._tooltipNode=null)),this}},{key:'_findContainer',value:function(a,b){return'string'==typeof a?a=window.document.querySelector(a):!1===a&&(a=b.parentNode),a}},{key:'_append',value:function(a,b){b.appendChild(a)}},{key:'_setEventListeners',value:function(a,b,c){var d=this,e=[],f=[];b.forEach(function(a){'hover'===a?(e.push('mouseenter'),f.push('mouseleave')):'focus'===a?(e.push('focus'),f.push('blur')):'click'===a?(e.push('click'),f.push('click')):void 0}),e.forEach(function(b){var e=function(b){!0===d._isOpening||(b.usedByTooltip=!0,d._scheduleShow(a,c.delay,c,b))};d._events.push({event:b,func:e}),a.addEventListener(b,e)}),f.forEach(function(b){var e=function(b){!0===b.usedByTooltip||d._scheduleHide(a,c.delay,c,b)};d._events.push({event:b,func:e}),a.addEventListener(b,e)})}},{key:'_scheduleShow',value:function(a,b,c){var d=this;this._isOpening=!0;var e=b&&b.show||b||0;this._showTimeout=window.setTimeout(function(){return d._show(a,c)},e)}},{key:'_scheduleHide',value:function(a,b,c,d){var e=this;this._isOpening=!1;var f=b&&b.hide||b||0;window.setTimeout(function(){if((window.clearTimeout(e._showTimeout),!1!==e._isOpen)&&document.body.contains(e._tooltipNode)){if('mouseleave'===d.type){var f=e._setTooltipNodeEvent(d,a,b,c);if(f)return}e._hide(a,c)}},f)}}]),g}(),h=function(){var a=this;this.show=function(){return a._show(a.reference,a.options)},this.hide=function(){return a._hide()},this.dispose=function(){return a._dispose()},this.toggle=function(){return a._isOpen?a.hide():a.show()},this.arrowSelector='.tooltip-arrow, .tooltip__arrow',this.innerSelector='.tooltip-inner, .tooltip__inner',this._events=[],this._setTooltipNodeEvent=function(b,c,d,e){var f=b.relatedreference||b.toElement||b.relatedTarget;return!!a._tooltipNode.contains(f)&&(a._tooltipNode.addEventListener(b.type,function d(f){var g=f.relatedreference||f.toElement||f.relatedTarget;a._tooltipNode.removeEventListener(b.type,d),c.contains(g)||a._scheduleHide(c,e.delay,e,f)}),!0)}};return g}); 5 | //# sourceMappingURL=tooltip.min.js.map -------------------------------------------------------------------------------- /client/src/actions/levelActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | VIEW_LEVELS, 4 | LEVELS_LOADING, 5 | GET_ERRORS, 6 | DELETE_LEVEL, 7 | CLEAR_ERRORS, 8 | ADD_LEVEL 9 | } from "./types"; 10 | 11 | //Get employee levels 12 | export const getLevels = () => dispatch => { 13 | dispatch(setLevelsLoading()); 14 | axios 15 | .get("/api/level/all") 16 | .then(res => 17 | dispatch({ 18 | type: VIEW_LEVELS, 19 | payload: res.data 20 | }) 21 | ) 22 | .catch(err => 23 | dispatch({ 24 | type: GET_ERRORS, 25 | payload: err.response.data 26 | }) 27 | ); 28 | }; 29 | 30 | //Add employee level 31 | export const addLevel = levelDetails => dispatch => { 32 | dispatch(clearErrors()); 33 | return axios 34 | .post("/api/level/", levelDetails) 35 | .then(res => 36 | dispatch({ 37 | type: ADD_LEVEL, 38 | payload: res.data 39 | }) 40 | ) 41 | .catch( 42 | err => 43 | dispatch({ 44 | type: GET_ERRORS, 45 | payload: err.response.data 46 | }), 47 | dispatch(() => { 48 | setTimeout(function() { 49 | dispatch(clearErrors()); 50 | }, 5000); 51 | }) 52 | ); 53 | }; 54 | 55 | //Get single employee level 56 | export const getLevel = id => dispatch => { 57 | dispatch(setLevelsLoading()); 58 | axios 59 | .get(`/api/level/single/${id}`) 60 | .then(res => 61 | dispatch({ 62 | type: VIEW_LEVELS, 63 | payload: res.data 64 | }) 65 | ) 66 | .catch(err => 67 | dispatch({ 68 | type: GET_ERRORS, 69 | payload: err.response.data 70 | }) 71 | ); 72 | }; 73 | 74 | //Edit employee level 75 | export const editLevel = levelData => dispatch => { 76 | return axios 77 | .post("/api/level/", levelData) 78 | .then(res => 79 | dispatch({ 80 | type: VIEW_LEVELS, 81 | payload: res.data 82 | }) 83 | ) 84 | .catch(err => 85 | dispatch({ 86 | type: GET_ERRORS, 87 | payload: err.response.data 88 | }) 89 | ); 90 | }; 91 | 92 | //Delete employee level 93 | export const deleteLevel = id => dispatch => { 94 | return axios 95 | .post(`/api/level/${id}`) 96 | .then(res => 97 | dispatch({ 98 | type: DELETE_LEVEL, 99 | payload: id 100 | }) 101 | ) 102 | .catch(err => 103 | dispatch({ 104 | type: GET_ERRORS, 105 | payload: err.response.data 106 | }) 107 | ); 108 | }; 109 | 110 | //Add bonus to level 111 | export const addBonus = (bonusDetails, id) => dispatch => { 112 | dispatch(clearErrors()); 113 | return axios 114 | .post(`/api/level/bonus/${id}`, bonusDetails) 115 | .then(res => 116 | dispatch({ 117 | type: VIEW_LEVELS, 118 | payload: res.data 119 | }) 120 | ) 121 | .catch( 122 | err => 123 | dispatch({ 124 | type: GET_ERRORS, 125 | payload: err.response.data 126 | }), 127 | dispatch(() => { 128 | setTimeout(function() { 129 | dispatch(clearErrors()); 130 | }, 5000); 131 | }) 132 | ); 133 | }; 134 | 135 | //Delete level bonus 136 | export const deleteBonus = (levelId, bonusId) => dispatch => { 137 | return axios 138 | .delete(`/api/level/bonus/${levelId}/${bonusId}`) 139 | .then(res => 140 | dispatch({ 141 | type: VIEW_LEVELS, 142 | payload: res.data 143 | }) 144 | ) 145 | .catch(err => 146 | dispatch({ 147 | type: GET_ERRORS, 148 | payload: err.response.data 149 | }) 150 | ); 151 | }; 152 | 153 | //Add deductable to level 154 | export const addDeductable = (deductableDetails, id) => dispatch => { 155 | dispatch(clearErrors()); 156 | return axios 157 | .post(`/api/level/deductable/${id}`, deductableDetails) 158 | .then(res => 159 | dispatch({ 160 | type: VIEW_LEVELS, 161 | payload: res.data 162 | }) 163 | ) 164 | .catch(err => 165 | dispatch({ 166 | type: GET_ERRORS, 167 | payload: err.response.data 168 | }), 169 | dispatch(() => { 170 | setTimeout(function() { 171 | dispatch(clearErrors()) 172 | }, 5000); 173 | }) 174 | ); 175 | }; 176 | 177 | //Delete level bonus 178 | export const deleteDeductable = (levelId, deductableId) => dispatch => { 179 | return axios 180 | .delete(`/api/level/deductable/${levelId}/${deductableId}`) 181 | .then(res => 182 | dispatch({ 183 | type: VIEW_LEVELS, 184 | payload: res.data 185 | }) 186 | ) 187 | .catch(err => 188 | dispatch({ 189 | type: GET_ERRORS, 190 | payload: err.response.data 191 | }) 192 | ); 193 | }; 194 | 195 | //Set loding state 196 | export const setLevelsLoading = () => { 197 | return { 198 | type: LEVELS_LOADING 199 | }; 200 | }; 201 | 202 | //Clear errors 203 | const clearErrors = () => { 204 | return { 205 | type: CLEAR_ERRORS 206 | }; 207 | }; 208 | --------------------------------------------------------------------------------