',
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 |
67 | Delete
68 |
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 |
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 |
65 | Delete
66 |
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 |
65 | Delete
66 |
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 |
74 | Delete
75 |
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 |
77 | Delete
78 |
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 |
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 =
69 | }
70 |
71 | } else {
72 | employeeTable = No previous employee entries! ;
73 | generateBtn = '';
74 | }
75 | }
76 |
77 | return (
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
Employee monthly salary
87 |
88 | {generateBtn}
89 | {employeeTable}
90 |
91 |
92 |
93 |
94 |
95 | );
96 | }
97 | }
98 |
99 | MonthlySalary.propTypes = {
100 | getEmployees: PropTypes.func.isRequired
101 | };
102 |
103 | const mapStateToProps = state => ({
104 | employees: state.employees
105 | });
106 |
107 | export default connect(
108 | mapStateToProps,
109 | { getEmployees }
110 | )(MonthlySalary);
111 |
--------------------------------------------------------------------------------
/client/src/components/dashboard/SearchBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import {Link} from 'react-router-dom';
3 | import Avatar from "./avatar.png";
4 | import { connect } from "react-redux";
5 | import PropTypes from "prop-types";
6 | import { logoutUser } from "../../actions/authActions";
7 |
8 | class SearchBar extends Component {
9 |
10 | state = {
11 | isOpen: false
12 | }
13 |
14 | logOut(e) {
15 | e.preventDefault();
16 |
17 | this.props.logoutUser();
18 | }
19 |
20 | render() {
21 | const { user } = this.props.auth;
22 |
23 | return (
24 |
99 | );
100 | }
101 | }
102 |
103 | SearchBar.propTypes = {
104 | auth: PropTypes.object.isRequired
105 | };
106 |
107 | const mapStateToProps = state => ({
108 | auth: state.auth
109 | });
110 |
111 | export default connect(
112 | mapStateToProps,
113 | { logoutUser }
114 | )(SearchBar);
115 |
--------------------------------------------------------------------------------
/client/src/components/payroll/all/records/AllTimeYear.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import TextFieldGroup from "../../../common/TextFieldGroup";
4 | import SearchBar from "../../../dashboard/SearchBar";
5 | import SideBar from "../../../dashboard/SideBar";
6 | import Spinner from "../../../common/Spinner";
7 | import { toast } from "react-toastify";
8 | import Button from "../../../common/Button";
9 | import Axios from "axios";
10 | import AllTimeYearTable from "./AllTimeYearTable";
11 |
12 | export default () => {
13 | const [year, setYear] = useState("");
14 | const [error, setError] = useState("");
15 | const [loading, setLoading] = useState(false);
16 | const [payslips, setPayslip] = useState([]);
17 |
18 | const onChangeYear = e => {
19 | setYear(e.target.value);
20 | };
21 |
22 | const onSubmit = e => {
23 | e.preventDefault();
24 |
25 | const payslipYear = {
26 | year
27 | };
28 | setLoading(true);
29 |
30 | Axios.post(`/api/payslip/record/byyear/`, payslipYear)
31 | .then(res => {
32 | setPayslip(res.data);
33 | setLoading(false);
34 | })
35 | .catch(err => {
36 | if (err.response.data.year) {
37 | setError(err.response.data.year);
38 | setTimeout(function() {
39 | setError("");
40 | }, 5000);
41 | setLoading(false);
42 | }
43 | if (err.response.data.payslips) {
44 | toast.warn(err.response.data.payslips);
45 | setLoading(false);
46 | }
47 | });
48 | };
49 |
50 | let payslipTableContainer;
51 |
52 | if (loading) {
53 | payslipTableContainer = ;
54 | }
55 | if (Object.keys(payslips).length > 0) {
56 | payslipTableContainer = ;
57 | }
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
Payroll report
69 |
70 |
71 |
72 | Search generated employee payslips by year
73 |
74 |
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 | Name
53 | Designation
54 | Pension Contribution
55 |
56 |
57 |
58 | {payrolls.payslip.map(payrollItem => (
59 |
60 | {payrollItem.name}
61 | {payrollItem.designation}
62 | ₦ {formatMoney(payrollItem.pension.toFixed(2))}
63 |
64 | ))}
65 |
66 | Total Sum
67 | ---
68 | ₦ {formatMoney(payrolls.pensionSum.toFixed(2))}
69 |
70 |
71 |
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 |
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 |
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 |
42 |
48 |
49 |
50 |
58 |
64 |
65 |
66 |
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 |
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 |
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 |
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 | Name
110 | Level
111 | Department
112 | Designation
113 | Action
114 |
115 |
116 | {employeeDetails}
117 |
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 |
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 |
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 |
--------------------------------------------------------------------------------