├── .gitignore
├── MUI-table.gif
├── README.md
├── app.yaml
├── bin
└── www
├── client
├── .eslintrc
├── analyze.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── Components
│ │ ├── ComponentHeader.js
│ │ ├── Department
│ │ │ ├── AddNewDepartment.js
│ │ │ ├── DepartmentList.js
│ │ │ ├── EditDepartment.js
│ │ │ ├── SearchFilterDepartment.js
│ │ │ ├── Snackbars
│ │ │ │ ├── EditItemConfirmSnackbar.js
│ │ │ │ ├── EmptyFieldSnackBar.js
│ │ │ │ └── NewItemAddedConfirmSnackbar.js
│ │ │ ├── TableHeadDepartment.js
│ │ │ └── TableToolbarDepartment.js
│ │ ├── Employee
│ │ │ ├── AddNewEmployee.js
│ │ │ ├── CurrentMonthEmployees.js
│ │ │ ├── DateRangeFilterEmployees.js
│ │ │ ├── EditEmployees.js
│ │ │ ├── EmployeeList.js
│ │ │ ├── SearchFilterEmployees.js
│ │ │ ├── ShowEmployees.js
│ │ │ ├── Snackbars
│ │ │ │ ├── EditItemConfirmSnackbar.js
│ │ │ │ ├── EmptyFieldSnackBar.js
│ │ │ │ ├── NewItemAddedConfirmSnackbar.js
│ │ │ │ ├── NoRecordForDateRangeQuerySnackbar.js
│ │ │ │ └── WrongDateRangeSnackBar.js
│ │ │ ├── TableHeadEmployees.js
│ │ │ ├── TableToolbarEmployees.js
│ │ │ └── __test__
│ │ │ │ ├── EmployeeList.test.js
│ │ │ │ └── __snapshots__
│ │ │ │ └── BerthThroughputList.test.js.snap
│ │ ├── LandingPage.js
│ │ ├── NotFound.css
│ │ ├── NotFound.js
│ │ ├── UtilFunctions
│ │ │ ├── MySnackbarContent.js
│ │ │ ├── TablePaginationActionsWrapped-bkp.js
│ │ │ ├── TablePaginationActionsWrapped.js
│ │ │ ├── confirmDelete.css
│ │ │ ├── showEmptyFieldAndDeleteSnackbar.js
│ │ │ ├── snackbar.css
│ │ │ └── tableSortByHeading.js
│ │ └── commonStyles
│ │ │ ├── AddNewItemStyles.js
│ │ │ ├── AddNewItemThemes.js
│ │ │ ├── BootstrapInput.js
│ │ │ ├── ModuleItemListStyles.js
│ │ │ ├── SearchFilter-InputField.css
│ │ │ ├── SearchFilter-Styles.js
│ │ │ ├── combineStyles.js
│ │ │ └── toolbarStyles.js
│ ├── Routes.js
│ ├── assets
│ │ ├── images
│ │ │ ├── 404.svg
│ │ │ ├── LandingPage
│ │ │ │ ├── code-1.jpeg
│ │ │ │ ├── code-2.jpeg
│ │ │ │ ├── code-3.jpeg
│ │ │ │ └── code-4.jpeg
│ │ │ ├── PageNotFound
│ │ │ │ └── PageNotFound.jpg
│ │ │ ├── SVG-Icons
│ │ │ │ └── baseline-people_outline-24px.svg
│ │ │ ├── astronaut.svg
│ │ │ ├── bg_purple.png
│ │ │ ├── earth.svg
│ │ │ ├── logo.png
│ │ │ ├── moon.svg
│ │ │ ├── overlay_stars.svg
│ │ │ └── rocket.svg
│ │ └── js
│ │ │ ├── docs.min.js
│ │ │ ├── ie10-viewport-bug-workaround.js
│ │ │ └── vendor
│ │ │ └── jquery.min.js
│ ├── history.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ └── serviceWorker.js
└── yarn.lock
├── config
├── config.js
├── dev.js
├── keys.js
├── prod.js
└── settings.js
├── index.js
├── models
├── department.js
└── employee.js
├── package-lock.json
├── package.json
├── routes
├── createAllocationLodash.js
├── departmentRoutes.js
└── employeeRoutes.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | */node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 | /client/build
12 | */build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .env
21 | .env.override
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # Ignore docs files
28 | _gh_pages
29 | .ruby-version
30 |
31 | # Numerous always-ignore extensions
32 | *.diff
33 | *.err
34 | *.orig
35 | *.log
36 | *.rej
37 | *.swo
38 | *.swp
39 | *.zip
40 | *.vi
41 | *~
42 | *.~lock*
43 | .~lock*
44 |
45 | # OS or Editor folders
46 | .DS_Store
47 | ._*
48 | Thumbs.db
49 | .cache
50 | .project
51 | .settings
52 | .tmproj
53 | *.esproj
54 | nbproject
55 | *.sublime-project
56 | *.sublime-workspace
57 | .idea
58 |
59 | # Komodo
60 | *.komodoproject
61 | .komodotools
62 |
63 | # grunt-html-validation
64 | validation-status.json
65 | validation-report.json
66 |
67 | # Folders to ignore
68 | node_modules
69 | Project-Note-PAUL
70 | .vscode
71 |
72 | # Ignore all logfiles and tempfiles.
73 | !/log/.keep
74 | /tmp
75 | /.gems
76 |
77 |
--------------------------------------------------------------------------------
/MUI-table.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/MUI-table.gif
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Live Demo running in [Google Cloud Platform](https://steel-aileron-266311.appspot.com)
2 |
3 |
4 |
5 | ### A quite exhaustive React, Node/Express, MongoDB App for rendering tabular data with pagination with full Create, Edit, Delete functionality. Also, have implemented filter functionality for text-based search and date-range based search. Have used Material-UI extensively across the app. Functinality to download the data in `.csv` format.
6 |
7 | ##### Quite a few standard and simple tests have also been implemented with `jest`
8 |
9 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
10 |
11 | ### To launch this project in the local machine.
12 |
13 | run `npm install` in both the server directory, which in this project is the root directory (`./`) and also `./client` directories separately, which will install all the npm packages for server and client respectively.
14 |
15 | Then, start mongodb service with `sudo service mongod start` and then finally run the following command from inside the `server` directory.
16 |
17 | `npm run dev`
18 |
19 | This will start both the client (port 3000) and server (port 8080) and launch the site in port 3000.
20 |
21 | #### To build the project for production and serve the React static files from `/client/build` (i.e. do these steps before deploying to Google Cloud Platform's Google App Engine)
22 |
23 | ```
24 | cd client
25 |
26 | npm run build
27 |
28 | cd ..
29 |
30 | ```
31 |
32 | And check that everything is running properly
33 |
34 | ```
35 | npm start
36 |
37 | ```
38 |
39 | And then finally deploy to Google Cloud
40 |
41 | ```
42 | gcloud app deploy --stop-previous-version
43 |
44 | ```
45 |
46 | ### Other Commands
47 |
48 | #### `npm test`
49 |
50 | Launches the test runner in the interactive watch mode.
51 |
52 | #### To check the bundle size of overall app and various packages of the Client
53 |
54 | ``cd client`` and then run
55 |
56 | ``npm run analyze``
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: nodejs
2 | env: flex
3 |
4 | # https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml
5 | # This sample incurs costs to run on the App Engine flexible environment.
6 | # The settings below are to reduce costs during testing and are not appropriate
7 | # for production use. For more information, see:
8 | # https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml
9 | manual_scaling:
10 | instances: 1
11 |
12 | resources:
13 | cpu: .5
14 | memory_gb: 0.5
15 | disk_size_gb: 10
16 |
17 | handlers:
18 | - url: /
19 | static_files: client/build/index.html
20 | upload: client/build/index.html
21 | - url: /
22 | static_dir: client/build
23 | # manual_scaling - Lock instances to 1 . using App Engine Flexible will create VM instances to serve from. If you want to lower your usage, you might restrict your App to just one 1 instance (probably not what you want in production, but perfect for in prototyping and development cases ).
24 | # https://stackoverflow.com/questions/37381694/google-app-engine-how-to-correctly-deploy-an-app
25 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /* The below four line To deal with env variable thats already set in the machine - sometime my local machine is taking AWS kyes and secret keys something else than what is set in the .env file
4 |
5 | https://github.com/motdotla/dotenv#what-happens-to-environment-variables-that-were-already-set
6 | "We will never modify any environment variables that have already been set. In particular, if there is a variable in your .env file which collides with one that already exists in your environment, then that variable will be skipped. This behavior allows you to override all .env configurations with a machine-specific environment, although it is not recommended."
7 | */
8 | // require("dotenv").config();
9 | const fs = require("fs");
10 | // const dotenv = require("dotenv");
11 | require('dotenv').config()
12 | // const envConfig = dotenv.parse(fs.readFileSync(".env.override"));
13 | // for (let k in envConfig) {
14 | // process.env[k] = envConfig[k];
15 | // }
16 |
17 | // require('dotenv').config();
18 | var app = require('../app');
19 | var debug = require('debug')('mean-app:server');
20 | var http = require('http');
21 | // const path = require("path");
22 |
23 | // Get port from environment and store in Express.
24 | var port = normalizePort(process.env.PORT || '8080');
25 | app.set('port', port);
26 |
27 |
28 | // Create HTTP server.
29 | var server = http.createServer(app);
30 |
31 | // Listen on provided port, on all network interfaces.
32 | server.listen(port);
33 | server.on('error', onError);
34 | server.on('listening', onListening);
35 |
36 | // Normalize a port into a number, string, or false.
37 | function normalizePort(val) {
38 | var port = parseInt(val, 10);
39 |
40 | if (isNaN(port)) {
41 | // named pipe
42 | return val;
43 | }
44 |
45 | if (port >= 0) {
46 | // port number
47 | return port;
48 | }
49 | return false;
50 | }
51 |
52 |
53 | // Event listener for HTTP server "error" event.
54 | function onError(error) {
55 | if (error.syscall !== 'listen') {
56 | console.log(error);
57 | }
58 |
59 | var bind = typeof port === 'string'
60 | ? 'Pipe ' + port
61 | : 'Port ' + port;
62 |
63 | // handle specific listen errors with friendly messages
64 | switch (error.code) {
65 | case 'EACCES':
66 | console.error(bind + ' requires elevated privileges');
67 | process.exit(1);
68 | break;
69 | case 'EADDRINUSE':
70 | console.error(bind + ' is already in use');
71 | process.exit(1);
72 | break;
73 | default:
74 | console.log(error);
75 | }
76 | }
77 |
78 |
79 | // Event listener for HTTP server "listening" event.
80 | function onListening() {
81 | var addr = server.address();
82 | var bind = typeof addr === 'string'
83 | ? 'pipe ' + addr
84 | : 'port ' + addr.port;
85 | debug('Listening on ' + bind);
86 | }
87 |
--------------------------------------------------------------------------------
/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "react-app"
4 | }
--------------------------------------------------------------------------------
/client/analyze.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'production';
2 |
3 | const webpack = require('webpack');
4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
5 | const webpackConfigProd = require('react-scripts/config/webpack.config')('production');
6 |
7 | // this one is optional, just for better feedback on build
8 | const chalk = require('chalk');
9 | const ProgressBarPlugin = require('progress-bar-webpack-plugin');
10 | const green = text => {
11 | return chalk.green.bold(text);
12 | };
13 |
14 | // pushing BundleAnalyzerPlugin to plugins array
15 | webpackConfigProd.plugins.push(new BundleAnalyzerPlugin());
16 |
17 | // optional - pushing progress-bar plugin for better feedback;
18 | // it can and will work without progress-bar,
19 | // but during build time you will not see any messages for 10-60 seconds (depends on the size of the project)
20 | // and decide that compilation is kind of hang up on you; progress bar shows nice progression of webpack compilation
21 | webpackConfigProd.plugins.push(
22 | new ProgressBarPlugin({
23 | format: `${green('analyzing...')} ${green('[:bar]')}${green('[:percent]')}${green('[:elapsed seconds]')} - :msg`,
24 | }),
25 | );
26 |
27 | // actually running compilation and waiting for plugin to start explorer
28 | webpack(webpackConfigProd, (err, stats) => {
29 | if (err || stats.hasErrors()) {
30 | console.error(err);
31 | }
32 | });
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend-portal",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://localhost:8080",
6 | "eslintConfig": {
7 | "extends": "react-app",
8 | "root": true
9 | },
10 | "dependencies": {
11 | "@date-io/date-fns": "1.1.0",
12 | "@fortawesome/fontawesome-svg-core": "^1.2.14",
13 | "@fortawesome/free-solid-svg-icons": "^5.7.1",
14 | "@fortawesome/react-fontawesome": "^0.1.4",
15 | "@material-ui/core": "^3.9.2",
16 | "@material-ui/icons": "^3.0.2",
17 | "axios": "^0.18.0",
18 | "bootstrap": "^4.2.1",
19 | "chart.js": "^2.7.3",
20 | "classnames": "^2.2.6",
21 | "date-fns": "2.0.0-alpha.26",
22 | "eslint": "5.12.0",
23 | "eslint-config-airbnb": "^17.1.0",
24 | "eslint-plugin-import": "^2.16.0",
25 | "eslint-plugin-jsx-a11y": "^6.2.1",
26 | "eslint-plugin-react": "^7.12.4",
27 | "file-loader": "^3.0.1",
28 | "history": "^4.7.2",
29 | "i": "^0.3.6",
30 | "lodash": "^4.17.11",
31 | "material-ui": "^0.20.2",
32 | "material-ui-pickers": "^2.2.1",
33 | "nock": "^10.0.6",
34 | "npm": "^6.7.0",
35 | "prettier-eslint-cli": "^4.7.1",
36 | "prettier-stylelint": "^0.4.2",
37 | "prop-types": "^15.6.2",
38 | "react": "^16.8.1",
39 | "react-chartkick": "^0.3.0",
40 | "react-confirm-alert": "^2.1.0",
41 | "react-csv": "^1.1.1",
42 | "react-dom": "^16.8.1",
43 | "react-helmet": "^5.2.0",
44 | "react-icons": "^3.3.0",
45 | "react-material-ui-form-validator": "^2.0.3",
46 | "react-paginate": "^6.2.1",
47 | "react-required-if": "^1.0.3",
48 | "react-router-dom": "^4.3.1",
49 | "react-scripts": "^2.1.5",
50 | "react-select": "^2.3.0",
51 | "react-social-icons": "^4.1.0",
52 | "react-vis": "^1.11.6",
53 | "react-welcome-page": "^0.2.1",
54 | "reactstrap": "^7.1.0",
55 | "sinon": "^7.2.4",
56 | "styled-components": "^4.1.3"
57 | },
58 | "scripts": {
59 | "start": "react-scripts start",
60 | "build": "react-scripts build",
61 | "test": "react-scripts test --env=jsdom",
62 | "eject": "react-scripts eject",
63 | "analyze": "node ./analyze.js",
64 | "count": "cloc src/"
65 | },
66 | "browserslist": [
67 | ">0.2%",
68 | "not dead",
69 | "not ie <= 11",
70 | "not op_mini all"
71 | ],
72 | "devDependencies": {
73 | "cloc": "^2.5.1",
74 | "enzyme": "^3.9.0",
75 | "enzyme-adapter-react-16": "^1.9.1",
76 | "progress-bar-webpack-plugin": "^2.1.0",
77 | "react-test-renderer": "^16.8.3",
78 | "webpack-bundle-analyzer": "^3.6.0"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route, BrowserRouter, Switch, Router, Link } from "react-router-dom";
3 | // import Routes from "./Routes";
4 | import { createMuiTheme, MuiThemeProvider } from "@material-ui/core";
5 | import history from "./history";
6 | import axios from "axios";
7 | import { library } from "@fortawesome/fontawesome-svg-core";
8 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
9 | import {
10 | faFilter,
11 | faDownload,
12 | faCalendarAlt
13 | } from "@fortawesome/free-solid-svg-icons";
14 | import EmployeeList from "./Components/Employee/EmployeeList";
15 | import DepartmentList from "./Components/Department/DepartmentList";
16 | import NotFound from "./Components/NotFound";
17 | import ComponentHeader from "./Components/ComponentHeader";
18 | import LandingPage from "./Components/LandingPage";
19 |
20 | library.add(faFilter, faDownload, faCalendarAlt);
21 |
22 | const theme = createMuiTheme({
23 | typography: {
24 | useNextVariants: true
25 | }
26 | });
27 |
28 | class App extends Component {
29 | state = {
30 | allDepartmentsForSiblingCommunication: [],
31 | isSideDrawerOpen: false
32 | };
33 |
34 | onSideDrawerOpen = () => {
35 | this.setState({
36 | isSideDrawerOpen: true
37 | });
38 | };
39 |
40 | onSideDrawerClose = () => {
41 | this.setState({
42 | isSideDrawerOpen: false
43 | });
44 | };
45 |
46 | componentDidMount() {
47 | axios
48 | .get("/api/department")
49 | .then(res => {
50 | this.setState({
51 | allDepartmentsForSiblingCommunication: res.data
52 | });
53 | })
54 | .catch(error => {});
55 | }
56 |
57 | setDepartmentForSiblingCommunication = department => {
58 | this.setState({
59 | allDepartmentsForSiblingCommunication: department
60 | });
61 | };
62 |
63 | render() {
64 | const { allDepartmentsForSiblingCommunication } = this.state;
65 |
66 | const pad = 16;
67 | const appBarHeight = 74;
68 | const drawerWidth = 240;
69 |
70 | const left = this.state.isSideDrawerOpen ? drawerWidth : 85;
71 | const top = appBarHeight;
72 |
73 | const width = this.state.isSideDrawerOpen
74 | ? "calc(100% - " + (drawerWidth + 2 * pad) + "px)"
75 | : "calc(100% - " + 2 * (pad + 50) + "px)";
76 |
77 | const contentStyle = {
78 | width: width,
79 | marginTop: top + pad,
80 | marginLeft: left + pad,
81 | marginBottom: pad,
82 | marginRight: pad,
83 |
84 | padding: 0
85 | };
86 |
87 | return (
88 |
89 |
90 |
91 |
92 |
96 |
97 |
98 | } />
99 | (
103 |
109 | )}
110 | />
111 | (
115 |
121 | )}
122 | />
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | );
131 | }
132 | }
133 |
134 | export default App;
135 |
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/client/src/Components/ComponentHeader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import classNames from "classnames";
4 | import { withStyles } from "@material-ui/core/styles";
5 | import Drawer from "@material-ui/core/Drawer";
6 | import AppBar from "@material-ui/core/AppBar";
7 | import Toolbar from "@material-ui/core/Toolbar";
8 | import List from "@material-ui/core/List";
9 | import CssBaseline from "@material-ui/core/CssBaseline";
10 | import Typography from "@material-ui/core/Typography";
11 | import Divider from "@material-ui/core/Divider";
12 | import IconButton from "@material-ui/core/IconButton";
13 | import MenuIcon from "@material-ui/icons/Menu";
14 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
15 | import ChevronRightIcon from "@material-ui/icons/ChevronRight";
16 | import ListItem from "@material-ui/core/ListItem";
17 | import ListItemIcon from "@material-ui/core/ListItemIcon";
18 | import ListItemText from "@material-ui/core/ListItemText";
19 | import InboxIcon from "@material-ui/icons/MoveToInbox";
20 | import MailIcon from "@material-ui/icons/Mail";
21 | import SvgIcon from '@material-ui/core/SvgIcon';
22 | import red from '@material-ui/core/colors/red';
23 |
24 | const drawerWidth = 240;
25 |
26 | const styles = theme => ({
27 | root: {
28 | display: "flex"
29 | },
30 | appBar: {
31 | zIndex: theme.zIndex.drawer + 1,
32 | transition: theme.transitions.create(["width", "margin"], {
33 | easing: theme.transitions.easing.sharp,
34 | duration: theme.transitions.duration.leavingScreen
35 | })
36 | },
37 | appBarShift: {
38 | marginLeft: drawerWidth,
39 | width: `calc(100% - ${drawerWidth}px)`,
40 | transition: theme.transitions.create(["width", "margin"], {
41 | easing: theme.transitions.easing.sharp,
42 | duration: theme.transitions.duration.enteringScreen
43 | })
44 | },
45 | icon: {
46 | margin: theme.spacing.unit * 1
47 |
48 | },
49 | menuButton: {
50 | marginLeft: 12,
51 | marginRight: 36
52 | },
53 | hide: {
54 | display: "none"
55 | },
56 | drawer: {
57 | width: drawerWidth,
58 | flexShrink: 0,
59 | whiteSpace: "nowrap"
60 | },
61 | drawerOpen: {
62 | width: drawerWidth,
63 | transition: theme.transitions.create("width", {
64 | easing: theme.transitions.easing.sharp,
65 | duration: theme.transitions.duration.enteringScreen
66 | })
67 | },
68 | drawerClose: {
69 | transition: theme.transitions.create("width", {
70 | easing: theme.transitions.easing.sharp,
71 | duration: theme.transitions.duration.leavingScreen
72 | }),
73 | overflowX: "hidden",
74 | width: theme.spacing.unit * 7 + 1,
75 | [theme.breakpoints.up("sm")]: {
76 | width: theme.spacing.unit * 9 + 1
77 | }
78 | },
79 | toolbar: {
80 | display: "flex",
81 | alignItems: "center",
82 | justifyContent: "flex-end",
83 | padding: "0 8px",
84 | ...theme.mixins.toolbar
85 | },
86 | content: {
87 | flexGrow: 1,
88 | padding: theme.spacing.unit * 3
89 | }
90 | });
91 |
92 | const ListItemLink = props => {
93 | return ;
94 | };
95 |
96 | const EmployeeIcon = props => {
97 | return (
98 |
99 |
100 |
101 | );
102 | }
103 |
104 | const DepartmentIcon = props => {
105 | return (
106 |
107 |
108 |
109 | );
110 | }
111 |
112 | class ComponentHeader extends React.Component {
113 | state = {
114 | open: false
115 | };
116 |
117 | handleDrawerOpen = () => {
118 | this.setState({ open: true });
119 | this.props.onSideDrawerOpen();
120 | };
121 |
122 | handleDrawerClose = () => {
123 | this.setState({ open: false });
124 | this.props.onSideDrawerClose();
125 | };
126 |
127 | render() {
128 | const { classes, theme } = this.props;
129 |
130 | return (
131 |
132 |
133 |
139 |
140 |
148 |
149 |
150 |
151 | Corporate Records
152 |
153 |
154 |
155 |
169 |
170 |
171 | {theme.direction === "rtl" ? (
172 |
173 | ) : (
174 |
175 | )}
176 |
177 |
178 |
179 |
180 |
181 |
184 |
185 |
186 |
187 |
188 |
189 |
192 |
193 |
194 |
195 |
196 | {["All mail", "Trash", "Spam"].map((text, index) => (
197 |
198 |
199 | {index % 2 === 0 ? : }
200 |
201 |
202 |
203 | ))}
204 |
205 |
206 |
207 | );
208 | }
209 | }
210 |
211 | ComponentHeader.propTypes = {
212 | classes: PropTypes.object.isRequired,
213 | theme: PropTypes.object.isRequired
214 | };
215 |
216 | export default withStyles(styles, { withTheme: true })(ComponentHeader);
217 |
--------------------------------------------------------------------------------
/client/src/Components/Department/AddNewDepartment.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import axios from "axios";
3 | import PropTypes from "prop-types";
4 | import history from "../../history";
5 | import Button from "@material-ui/core/Button";
6 | import TextField from "@material-ui/core/TextField";
7 | import Dialog from "@material-ui/core/Dialog";
8 | import DialogActions from "@material-ui/core/DialogActions";
9 | import DialogContent from "@material-ui/core/DialogContent";
10 | import DialogTitle from "@material-ui/core/DialogTitle";
11 | import { MuiThemeProvider } from "@material-ui/core/styles";
12 | import Fab from "@material-ui/core/Fab";
13 | import { withStyles } from "@material-ui/core";
14 | import AddIcon from "@material-ui/icons/Add";
15 | import { styles } from "../commonStyles/AddNewItemStyles";
16 | import theme from "../commonStyles/AddNewItemThemes";
17 | import EmptyFieldSnackBar from "./Snackbars/EmptyFieldSnackBar";
18 | import NewItemAddedConfirmSnackbar from "./Snackbars/NewItemAddedConfirmSnackbar";
19 | import IconButton from "@material-ui/core/IconButton";
20 | import CancelIcon from "@material-ui/icons/Cancel";
21 |
22 | class AddNewDepartment extends Component {
23 | state = {
24 | open: false,
25 | openNewItemAddedConfirmSnackbar: false,
26 | openEmptyTextFieldSnackbar: false,
27 | vertical: "top",
28 | horizontal: "center",
29 | name: "",
30 | type: ""
31 | };
32 |
33 | handleToggle = () => {
34 | this.setState({
35 | open: !this.state.open
36 | });
37 | };
38 |
39 | handleFabOpen = () => {
40 | this.setState({ open: true });
41 | };
42 |
43 | closeNewItemConfirmSnackbar = () => {
44 | this.setState({ openNewItemAddedConfirmSnackbar: false });
45 | };
46 |
47 | closeEmptyFieldSnackbar = () => {
48 | this.setState({ openEmptyTextFieldSnackbar: false });
49 | };
50 |
51 | onChange = e => {
52 | const state = this.state;
53 | state[e.target.name] = e.target.value;
54 | this.setState(state);
55 | };
56 |
57 | handleFormSubmit = () => {
58 | const { addNewItemToParentState } = this.props;
59 | const { name, type } = this.state;
60 | if (name !== "" && type !== "") {
61 | axios
62 | .post("/api/department/", {
63 | name,
64 | type
65 | })
66 | .then(() => {
67 | addNewItemToParentState({
68 | name,
69 | type
70 | });
71 | this.setState(
72 | {
73 | open: false,
74 | openNewItemAddedConfirmSnackbar: true,
75 | vertical: "top",
76 | horizontal: "center"
77 | },
78 | () => {
79 | history.push("/department");
80 | }
81 | );
82 | })
83 | .catch(error => {
84 | console.log("THE ERROR RESPONSE IS ", error.response);
85 | if (
86 | error.response &&
87 | error.response.data.name === "MongoError" &&
88 | (error.response && error.response.data.code === 11000)
89 | ) {
90 | alert(
91 | "Duplicate Department Name! please select another name for the Department"
92 | );
93 | } else {
94 | alert(
95 | "Ooops something wrong happened while adding new item, please try again"
96 | );
97 | }
98 | });
99 | } else {
100 | this.setState({ openEmptyTextFieldSnackbar: true });
101 | }
102 | };
103 |
104 | handleCancel = () => {
105 | this.setState({ open: false });
106 | };
107 |
108 | handleEnterEscapeKeyPress = e => {
109 | if (e.key === "Enter") {
110 | this.handleFormSubmit();
111 | } else if (e.key === "Escape") {
112 | this.handleCancel();
113 | }
114 | };
115 |
116 | render() {
117 | const { classes } = this.props;
118 | const { name, type } = this.state;
119 |
120 | return (
121 |
122 |
123 |
131 |
132 |
133 |
246 |
252 |
253 |
257 |
258 |
259 | );
260 | }
261 | }
262 |
263 | AddNewDepartment.propTypes = {
264 | classes: PropTypes.object.isRequired
265 | };
266 |
267 | export default withStyles(styles)(AddNewDepartment);
268 |
269 | // The Cancel button color - https://material-ui.com/customization/overrides/
270 |
--------------------------------------------------------------------------------
/client/src/Components/Department/EditDepartment.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import axios from "axios";
3 | import PropTypes from "prop-types";
4 | import history from "../../history";
5 | import Button from "@material-ui/core/Button";
6 | import TextField from "@material-ui/core/TextField";
7 | import Dialog from "@material-ui/core/Dialog";
8 | import DialogActions from "@material-ui/core/DialogActions";
9 | import DialogContent from "@material-ui/core/DialogContent";
10 | import DialogTitle from "@material-ui/core/DialogTitle";
11 | import { MuiThemeProvider } from "@material-ui/core/styles";
12 | import { withStyles } from "@material-ui/core";
13 | import EditIcon from "@material-ui/icons/Edit";
14 | import { styles } from "../commonStyles/AddNewItemStyles";
15 | import theme from "../commonStyles/AddNewItemThemes";
16 | import EmptyFieldSnackBar from "./Snackbars/EmptyFieldSnackBar";
17 | import EditItemConfirmSnackbar from "./Snackbars/EditItemConfirmSnackbar";
18 | import IconButton from "@material-ui/core/IconButton";
19 | import CancelIcon from "@material-ui/icons/Cancel";
20 |
21 | class EditDepartment extends Component {
22 | state = {
23 | open: false,
24 | openEditItemConfirmSnackbar: false,
25 | openEmptyTextFieldSnackbar: false,
26 | vertical: "top",
27 | horizontal: "center",
28 | name: this.props.departmentToEdit[0].name,
29 | type: this.props.departmentToEdit[0].type,
30 | arrowRef: null
31 | };
32 |
33 | handleArrowRef = node => {
34 | this.setState({
35 | arrowRef: node
36 | });
37 | };
38 |
39 | handleToggle = () => {
40 | this.setState({
41 | open: !this.state.open
42 | });
43 | };
44 |
45 | handleFabOpen = () => {
46 | this.setState({ open: true });
47 | };
48 |
49 | closeNewItemConfirmSnackbar = () => {
50 | this.setState({ openEditItemConfirmSnackbar: false });
51 | this.props.unSelectItems();
52 | };
53 |
54 | closeEmptyFieldSnackbar = () => {
55 | this.setState({ openEmptyTextFieldSnackbar: false }, () => {});
56 | };
57 |
58 | handleEditFormSubmit = () => {
59 | const { editItemToParentState } = this.props;
60 | const { name, type } = this.state;
61 | if (name !== "" && type !== "") {
62 | axios
63 | .put(`/api/department/${this.props.departmentToEdit[0]._id}`, {
64 | name,
65 | type
66 | })
67 | .then(() => {
68 | editItemToParentState();
69 | this.setState(
70 | {
71 | open: false,
72 | openEditItemConfirmSnackbar: true,
73 | vertical: "top",
74 | horizontal: "center"
75 | },
76 | () => {
77 | history.push("/department");
78 | }
79 | );
80 | })
81 | .catch(error => {
82 | alert(
83 | "Ooops something wrong happened while editing, please try again"
84 | );
85 | });
86 | } else {
87 | this.setState({ openEmptyTextFieldSnackbar: true });
88 | }
89 | };
90 |
91 | handleCancel = () => {
92 | this.setState({ open: false });
93 | };
94 |
95 | handleEnterEscapeKeyPress = e => {
96 | if (e.key === "Enter") {
97 | this.handleEditFormSubmit();
98 | } else if (e.key === "Escape") {
99 | this.handleCancel();
100 | }
101 | };
102 |
103 | render() {
104 | const { classes } = this.props;
105 | const { name, type } = this.state;
106 |
107 | return (
108 |
109 |
110 |
111 |
112 |
113 |
210 |
214 |
215 |
219 |
220 |
221 | );
222 | }
223 | }
224 |
225 | EditDepartment.propTypes = {
226 | classes: PropTypes.object.isRequired
227 | };
228 |
229 | export default withStyles(styles)(EditDepartment);
230 |
231 | /* The Cancel button's color was done initially implementing MUI override - https://material-ui.com/customization/overrides/
232 | But later changed to regular inline style, as I was not able to differentiate the coloring the Cancel button with Save
233 | */
234 |
--------------------------------------------------------------------------------
/client/src/Components/Department/SearchFilterDepartment.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import Select from "react-select";
4 | import { withStyles } from "@material-ui/core/styles";
5 | import Typography from "@material-ui/core/Typography";
6 | import TextField from "@material-ui/core/TextField";
7 | import Paper from "@material-ui/core/Paper";
8 | import MenuItem from "material-ui/MenuItem";
9 | import styles from "../commonStyles/SearchFilter-Styles.js";
10 | import toolbarStyles from "../commonStyles/toolbarStyles";
11 | import combineStyles from "../commonStyles/combineStyles";
12 | import SelectField from "material-ui/SelectField";
13 | import IconButton from "@material-ui/core/IconButton";
14 | import CancelIcon from "@material-ui/icons/Cancel";
15 | import "../commonStyles/SearchFilter-InputField.css";
16 | import Grid from "@material-ui/core/Grid";
17 | import Tooltip from "@material-ui/core/Tooltip";
18 |
19 | const NoOptionsMessage = props => {
20 | return (
21 |
26 | {props.children}
27 |
28 | );
29 | };
30 |
31 | const inputComponent = ({ inputRef, ...props }) => {
32 | return ;
33 | };
34 |
35 | const Control = props => {
36 | return (
37 |
56 | );
57 | };
58 |
59 | const Option = props => {
60 | return (
61 |
72 | );
73 | };
74 |
75 | const Placeholder = props => {
76 | return (
77 |
82 | {props.children}
83 |
84 | );
85 | };
86 |
87 | const SingleValue = props => {
88 | return (
89 |
93 | {props.children}
94 |
95 | );
96 | };
97 |
98 | const ValueContainer = props => {
99 | return (
100 |
101 | {props.children}
102 |
103 | );
104 | };
105 |
106 | const Menu = props => {
107 | return (
108 |
113 | {props.children}
114 |
115 | );
116 | };
117 |
118 | const components = {
119 | Control,
120 | Menu,
121 | NoOptionsMessage,
122 | Option,
123 | Placeholder,
124 | SingleValue,
125 | ValueContainer
126 | };
127 |
128 | class SearchFilter extends React.Component {
129 | state = {
130 | query: "",
131 | columnToQuery: "name",
132 | arrowRef: null
133 | };
134 |
135 | handleArrowRef = node => {
136 | this.setState({
137 | arrowRef: node
138 | });
139 | };
140 |
141 | render() {
142 | const { columnToQuery } = this.state;
143 |
144 | // conditionally set the value of 'suggestions' with and IIFE
145 | const suggestions = (() => {
146 | switch (columnToQuery) {
147 | case "name":
148 | return this.props.allDepartments.map(item => ({
149 | value: item.name,
150 | label: item.name
151 | }));
152 | case "type":
153 | return this.props.allDepartments.map(item => ({
154 | value: item.type,
155 | label: item.type
156 | }));
157 | }
158 | })();
159 |
160 | const {
161 | classes,
162 | theme,
163 | value,
164 | onChange,
165 | inputValue,
166 | onInputChange
167 | } = this.props;
168 |
169 | const selectStyles = {
170 | input: base => ({
171 | ...base,
172 | width: "20em",
173 | marginTop: "20px",
174 | color: theme.palette.text.primary,
175 | "& input": {
176 | font: "inherit"
177 | }
178 | })
179 | };
180 |
181 | return (
182 |
195 |
202 |
203 |
216 |
223 | this.setState({ columnToQuery: value }, () => {
224 | this.props.handleColumnToQuery(this.state.columnToQuery);
225 | })
226 | }
227 | >
228 |
229 |
230 |
231 |
232 |
235 | Clear the Search Filter
236 |
237 |
238 | }
239 | placement="top-end"
240 | classes={{
241 | tooltip: classes.bootstrapTooltip,
242 | popper: classes.bootstrapPopper,
243 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
244 | tooltipPlacementRight: classes.bootstrapPlacementRight,
245 | tooltipPlacementTop: classes.bootstrapPlacementTop,
246 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
247 | }}
248 | PopperProps={{
249 | popperOptions: {
250 | modifiers: {
251 | arrow: {
252 | enabled: Boolean(this.state.arrowRef),
253 | element: this.state.arrowRef
254 | }
255 | }
256 | }
257 | }}
258 | onOpen={() =>
259 | this.props.setTextFilterTooltip("Clear the Search Filter")
260 | }
261 | >
262 |
270 |
271 |
272 |
273 |
274 |
275 |
276 | );
277 | }
278 | }
279 |
280 | SearchFilter.propTypes = {
281 | classes: PropTypes.object.isRequired,
282 | theme: PropTypes.object.isRequired
283 | };
284 |
285 | const combinedStyles = combineStyles(styles, toolbarStyles);
286 |
287 | export default withStyles(combinedStyles, { withTheme: true })(SearchFilter);
288 |
--------------------------------------------------------------------------------
/client/src/Components/Department/Snackbars/EditItemConfirmSnackbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class EditItemConfirmSnackbar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | openEditItemConfirmSnackbar,
18 | closeNewItemConfirmSnackbar
19 | } = this.props;
20 | return (
21 |
33 |
39 |
40 | );
41 | }
42 | }
43 |
44 | EditItemConfirmSnackbar.propTypes = {
45 | classes: PropTypes.object.isRequired
46 | };
47 |
48 | export default withStyles(styles)(EditItemConfirmSnackbar);
49 |
--------------------------------------------------------------------------------
/client/src/Components/Department/Snackbars/EmptyFieldSnackBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class EmptyFieldSnackBar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | closeEmptyFieldSnackbar,
18 | openEmptyTextFieldSnackbar
19 | } = this.props;
20 | return (
21 |
33 |
39 |
40 | );
41 | }
42 | }
43 |
44 | EmptyFieldSnackBar.propTypes = {
45 | classes: PropTypes.object.isRequired
46 | };
47 |
48 | export default withStyles(styles)(EmptyFieldSnackBar);
49 |
--------------------------------------------------------------------------------
/client/src/Components/Department/Snackbars/NewItemAddedConfirmSnackbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class NewItemAddedConfirmSnackbar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | openNewItemAddedConfirmSnackbar,
18 | closeNewItemConfirmSnackbar
19 | } = this.props;
20 | return (
21 |
34 |
40 |
41 | );
42 | }
43 | }
44 |
45 | NewItemAddedConfirmSnackbar.propTypes = {
46 | classes: PropTypes.object.isRequired
47 | };
48 |
49 | export default withStyles(styles)(NewItemAddedConfirmSnackbar);
50 |
--------------------------------------------------------------------------------
/client/src/Components/Department/TableHeadDepartment.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import TableCell from "@material-ui/core/TableCell";
4 | import TableHead from "@material-ui/core/TableHead";
5 | import TableRow from "@material-ui/core/TableRow";
6 | import TableSortLabel from "@material-ui/core/TableSortLabel";
7 | import Checkbox from "@material-ui/core/Checkbox";
8 | import Tooltip from "@material-ui/core/Tooltip";
9 | import { styles } from "../commonStyles/ModuleItemListStyles";
10 | import toolbarStyles from "../commonStyles/toolbarStyles";
11 | import combineStyles from "../commonStyles/combineStyles";
12 | import { withStyles } from "@material-ui/core";
13 |
14 | // make sure the 'tableHeaderProp' below is the property name of the mongodb schema's property name of the relevant model
15 | const rows = [
16 | {
17 | tableHeaderProp: "name",
18 | disablePadding: true,
19 | label: "Department Name"
20 | },
21 | {
22 | tableHeaderProp: "type",
23 | disablePadding: false,
24 | label: "Department Type"
25 | }
26 | ];
27 |
28 | class TableHeadDepartment extends React.Component {
29 | state = {
30 | arrowRef: null
31 | };
32 |
33 | // function to handle the placement of the arrow on top of the Tooltip
34 | handleArrowRef = node => {
35 | this.setState({
36 | arrowRef: node
37 | });
38 | };
39 |
40 | createSortHandler = property => event => {
41 | this.props.onRequestSort(event, property);
42 | };
43 |
44 | render() {
45 | const {
46 | onSelectAllClick,
47 | order,
48 | orderBy,
49 | numSelected,
50 | page,
51 | rowsPerPage,
52 | count,
53 | noOfItemsInCurrentPage
54 | } = this.props;
55 |
56 | const { classes } = this.props;
57 |
58 | return (
59 |
60 |
61 |
64 |
65 | {noOfItemsInCurrentPage > 1
66 | ? `Select all ${noOfItemsInCurrentPage}`
67 | : `Select the item `}
68 |
69 |
70 |
71 | }
72 | enterDelay={300}
73 | placement={"top-end"}
74 | classes={{
75 | tooltip: classes.bootstrapTooltip,
76 | popper: classes.bootstrapPopper,
77 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
78 | tooltipPlacementRight: classes.bootstrapPlacementRight,
79 | tooltipPlacementTop: classes.bootstrapPlacementTop,
80 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
81 | }}
82 | PopperProps={{
83 | popperOptions: {
84 | modifiers: {
85 | arrow: {
86 | enabled: Boolean(this.state.arrowRef),
87 | element: this.state.arrowRef
88 | }
89 | }
90 | }
91 | }}
92 | >
93 |
94 | 0 && numSelected < rowsPerPage
98 | : numSelected > 0 && numSelected < noOfItemsInCurrentPage
99 | }
100 | checked={
101 | noOfItemsInCurrentPage === 0
102 | ? false
103 | : page !== Math.max(0, Math.ceil(count / rowsPerPage) - 1)
104 | ? numSelected === rowsPerPage
105 | : noOfItemsInCurrentPage < rowsPerPage
106 | ? numSelected === noOfItemsInCurrentPage
107 | : numSelected === rowsPerPage
108 | }
109 | onChange={onSelectAllClick}
110 | />
111 |
112 |
113 | {rows.map(
114 | row => (
115 |
122 |
125 | Sort by {row.label}
126 |
130 |
131 | }
132 | enterDelay={300}
133 | placement={"top-end"}
134 | classes={{
135 | tooltip: classes.bootstrapTooltip,
136 | popper: classes.bootstrapPopper,
137 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
138 | tooltipPlacementRight: classes.bootstrapPlacementRight,
139 | tooltipPlacementTop: classes.bootstrapPlacementTop,
140 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
141 | }}
142 | PopperProps={{
143 | popperOptions: {
144 | modifiers: {
145 | arrow: {
146 | enabled: Boolean(this.state.arrowRef),
147 | element: this.state.arrowRef
148 | }
149 | }
150 | }
151 | }}
152 | >
153 |
158 | {row.label}
159 |
160 |
161 |
162 | ),
163 | this
164 | )}
165 |
166 |
167 | );
168 | }
169 | }
170 |
171 | TableHeadDepartment.propTypes = {
172 | numSelected: PropTypes.number.isRequired,
173 | onRequestSort: PropTypes.func.isRequired,
174 | onSelectAllClick: PropTypes.func.isRequired,
175 | order: PropTypes.string.isRequired,
176 | orderBy: PropTypes.string.isRequired,
177 | rowsPerPage: PropTypes.number.isRequired,
178 | classes: PropTypes.object.isRequired
179 | };
180 |
181 | const combinedStyles = combineStyles(styles, toolbarStyles);
182 |
183 | export default withStyles(combinedStyles)(TableHeadDepartment);
184 |
185 | /* The expresseion < Math.max(0, Math.ceil(count / rowsPerPage) - 1) > is the index value of the variable 'page' ONLY when I am on the last page. So the condition //#endregion
186 | < page === Math.max(0, Math.ceil(count / rowsPerPage) - 1) > WILL ONLY BE TRUE on the last page.
187 |
188 |
189 | Note, - The variable 'page' is a built-in prop of TablePagination API (https://material-ui.com/api/table-pagination/) - and the value of 'page' is a zero-based index of the current page.
190 | */
191 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/CurrentMonthEmployees.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { withStyles } from "@material-ui/core";
4 | import CancelIcon from "@material-ui/icons/Cancel";
5 | import IconButton from "@material-ui/core/IconButton";
6 | import styles from "../commonStyles/SearchFilter-Styles.js";
7 | import toolbarStyles from "../commonStyles/toolbarStyles";
8 | import combineStyles from "../commonStyles/combineStyles";
9 | import Paper from "@material-ui/core/Paper";
10 | import Tooltip from "@material-ui/core/Tooltip";
11 | const moment = require("moment");
12 |
13 | class CurrentMonthEmployee extends Component {
14 | state = {
15 | arrowRef: null
16 | };
17 |
18 | // function to handle the placement of the arrow on top of the Tooltip
19 | handleArrowRef = node => {
20 | this.setState({
21 | arrowRef: node
22 | });
23 | };
24 |
25 | closeCurrentMonthData = () => {
26 | this.props.closeCurrentMonthDataCompOnClick();
27 | this.props.clearDateRangeTooltip();
28 | };
29 |
30 | render() {
31 | const { classes } = this.props;
32 |
33 | const momentMonthNumber = moment().month();
34 | const currentMonth = moment(moment().month(momentMonthNumber)).format(
35 | "MMMM"
36 | );
37 |
38 | return (
39 |
40 | Showing Employees only for {currentMonth}
41 |
44 |
45 | Clear filter for only {currentMonth} and show all Employees
46 |
47 |
48 |
49 | }
50 | placement="top-end"
51 | classes={{
52 | tooltip: classes.bootstrapTooltip,
53 | popper: classes.bootstrapPopper,
54 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
55 | tooltipPlacementRight: classes.bootstrapPlacementRight,
56 | tooltipPlacementTop: classes.bootstrapPlacementTop,
57 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
58 | }}
59 | PopperProps={{
60 | popperOptions: {
61 | modifiers: {
62 | arrow: {
63 | enabled: Boolean(this.state.arrowRef),
64 | element: this.state.arrowRef
65 | }
66 | }
67 | }
68 | }}
69 | onOpen={() =>
70 | this.props.setDateRangeTooltip("Clear the Date Range Search Filter")
71 | }
72 | >
73 |
84 |
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | CurrentMonthEmployee.propTypes = {
93 | classes: PropTypes.object.isRequired
94 | };
95 |
96 | const combinedStyles = combineStyles(styles, toolbarStyles);
97 |
98 | export default withStyles(combinedStyles)(CurrentMonthEmployee);
99 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/SearchFilterEmployees.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import Select from "react-select";
4 | import { withStyles } from "@material-ui/core/styles";
5 | import Typography from "@material-ui/core/Typography";
6 | import TextField from "@material-ui/core/TextField";
7 | import Paper from "@material-ui/core/Paper";
8 | import MenuItem from "material-ui/MenuItem";
9 | import styles from "../commonStyles/SearchFilter-Styles.js";
10 | import toolbarStyles from "../commonStyles/toolbarStyles";
11 | import combineStyles from "../commonStyles/combineStyles";
12 | import SelectField from "material-ui/SelectField";
13 | import IconButton from "@material-ui/core/IconButton";
14 | import CancelIcon from "@material-ui/icons/Cancel";
15 | import "../commonStyles/SearchFilter-InputField.css";
16 | import Grid from "@material-ui/core/Grid";
17 | import Tooltip from "@material-ui/core/Tooltip";
18 |
19 | const NoOptionsMessage = props => {
20 | return (
21 |
26 | {props.children}
27 |
28 | );
29 | };
30 |
31 | const inputComponent = ({ inputRef, ...props }) => {
32 | return ;
33 | };
34 |
35 | const Control = props => {
36 | return (
37 |
56 | );
57 | };
58 |
59 | const Option = props => {
60 | return (
61 |
72 | );
73 | };
74 |
75 | const Placeholder = props => {
76 | return (
77 |
82 | {props.children}
83 |
84 | );
85 | };
86 |
87 | const SingleValue = props => {
88 | return (
89 |
93 | {props.children}
94 |
95 | );
96 | };
97 |
98 | const ValueContainer = props => {
99 | return (
100 |
101 | {props.children}
102 |
103 | );
104 | };
105 |
106 | const Menu = props => {
107 | return (
108 |
113 | {props.children}
114 |
115 | );
116 | };
117 |
118 | const components = {
119 | Control,
120 | Menu,
121 | NoOptionsMessage,
122 | Option,
123 | Placeholder,
124 | SingleValue,
125 | ValueContainer
126 | };
127 |
128 | class SearchFilter extends React.Component {
129 | state = {
130 | query: "",
131 | columnToQuery: "department_name",
132 | arrowRef: null
133 | };
134 |
135 | handleArrowRef = node => {
136 | this.setState({
137 | arrowRef: node
138 | });
139 | };
140 |
141 | render() {
142 | const { columnToQuery } = this.state;
143 |
144 | // conditionally set the value of 'suggestions' with an IIFE
145 | const suggestions = (() => {
146 | switch (columnToQuery) {
147 | case "date":
148 | return this.props.totalItemsFormatted.map(item => ({
149 | value: item.date,
150 | label: item.date
151 | }));
152 | case "employee_name":
153 | return this.props.totalItemsFormatted.map(item => ({
154 | value: item.employee_name,
155 | label: item.employee_name
156 | }));
157 | case "date":
158 | return this.props.totalItemsFormatted.map(item => ({
159 | value: item.work_description,
160 | label: item.work_description
161 | }));
162 | case "department_name":
163 | return this.props.totalItemsFormatted.map(item => ({
164 | value: item.department_name,
165 | label: item.department_name
166 | }));
167 | }
168 | })();
169 |
170 | const {
171 | classes,
172 | theme,
173 | value,
174 | onChange,
175 | inputValue,
176 | onInputChange
177 | } = this.props;
178 |
179 | const selectStyles = {
180 | input: base => ({
181 | ...base,
182 | width: "20em",
183 | marginTop: "20px",
184 | color: theme.palette.text.primary,
185 | "& input": {
186 | font: "inherit"
187 | }
188 | })
189 | };
190 |
191 | return (
192 |
205 |
212 |
213 |
226 |
233 | this.setState({ columnToQuery: value }, () => {
234 | this.props.handleColumnToQuery(this.state.columnToQuery);
235 | })
236 | }
237 | >
238 |
239 |
243 |
244 |
245 |
246 |
247 |
250 | Clear the Search Filter
251 |
252 |
253 | }
254 | placement="top-end"
255 | classes={{
256 | tooltip: classes.bootstrapTooltip,
257 | popper: classes.bootstrapPopper,
258 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
259 | tooltipPlacementRight: classes.bootstrapPlacementRight,
260 | tooltipPlacementTop: classes.bootstrapPlacementTop,
261 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
262 | }}
263 | PopperProps={{
264 | popperOptions: {
265 | modifiers: {
266 | arrow: {
267 | enabled: Boolean(this.state.arrowRef),
268 | element: this.state.arrowRef
269 | }
270 | }
271 | }
272 | }}
273 | onOpen={() =>
274 | this.props.setTextFilterTooltip("Clear the Search Filter")
275 | }
276 | >
277 |
285 |
286 |
287 |
288 |
289 |
290 |
291 | );
292 | }
293 | }
294 |
295 | SearchFilter.propTypes = {
296 | classes: PropTypes.object.isRequired,
297 | theme: PropTypes.object.isRequired
298 | };
299 |
300 | const combinedStyles = combineStyles(styles, toolbarStyles);
301 |
302 | export default withStyles(combinedStyles, { withTheme: true })(SearchFilter);
303 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/ShowEmployees.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import TextField from "@material-ui/core/TextField";
4 | import Dialog from "@material-ui/core/Dialog";
5 | import DialogContent from "@material-ui/core/DialogContent";
6 | import DialogTitle from "@material-ui/core/DialogTitle";
7 | import { MuiThemeProvider } from "@material-ui/core/styles";
8 | import { withStyles } from "@material-ui/core";
9 | import VisibilityIcon from "@material-ui/icons/Visibility";
10 | import { MuiPickersUtilsProvider, DatePicker } from "material-ui-pickers";
11 | import DateFnsUtils from "@date-io/date-fns";
12 | import { styles } from "../commonStyles/AddNewItemStyles";
13 | import theme from "../commonStyles/AddNewItemThemes";
14 | import IconButton from "@material-ui/core/IconButton";
15 | import CancelIcon from "@material-ui/icons/Cancel";
16 |
17 | class ShowEmployee extends Component {
18 | state = {
19 | open: false
20 | };
21 |
22 | handleToggle = () => {
23 | this.setState({
24 | open: !this.state.open
25 | });
26 | };
27 |
28 | handleFabOpen = () => {
29 | this.setState({ open: true });
30 | };
31 |
32 | handleCancel = () => {
33 | this.setState({ open: false });
34 | };
35 |
36 | handleEscapeKeyPress = e => {
37 | if (e.key === "Escape") {
38 | this.handleCancel();
39 | }
40 | };
41 |
42 | render() {
43 | const { classes } = this.props;
44 |
45 | const employee_name =
46 | this.props.itemToEdit[0] && this.props.itemToEdit[0].employee_name;
47 | const work_description =
48 | this.props.itemToEdit[0] && this.props.itemToEdit[0].work_description;
49 |
50 | const avg_employee_productivity =
51 | this.props.itemToEdit[0] &&
52 | this.props.itemToEdit[0].avg_employee_productivity;
53 | const benchmark_employee_productivity =
54 | this.props.itemToEdit[0] &&
55 | this.props.itemToEdit[0].benchmark_employee_productivity;
56 | const department_name =
57 | this.props.itemToEdit[0] && this.props.itemToEdit[0].department_name;
58 | const date = this.props.itemToEdit[0] && this.props.itemToEdit[0].date;
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 |
211 |
212 |
213 |
214 | );
215 | }
216 | }
217 |
218 | ShowEmployee.propTypes = {
219 | classes: PropTypes.object.isRequired
220 | };
221 |
222 | export default withStyles(styles)(ShowEmployee);
223 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/Snackbars/EditItemConfirmSnackbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class EditItemConfirmSnackbar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | openNewItemAddedConfirmSnackbar,
18 | closeNewItemConfirmSnackbar
19 | } = this.props;
20 | return (
21 |
33 |
39 |
40 | );
41 | }
42 | }
43 |
44 | EditItemConfirmSnackbar.propTypes = {
45 | classes: PropTypes.object.isRequired
46 | };
47 |
48 | export default withStyles(styles)(EditItemConfirmSnackbar);
49 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/Snackbars/EmptyFieldSnackBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class EmptyFieldSnackBar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | closeEmptyFieldSnackbar,
18 | openEmptyTextFieldSnackbar
19 | } = this.props;
20 | return (
21 |
33 |
39 |
40 | );
41 | }
42 | }
43 |
44 | EmptyFieldSnackBar.propTypes = {
45 | classes: PropTypes.object.isRequired
46 | };
47 |
48 | export default withStyles(styles)(EmptyFieldSnackBar);
49 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/Snackbars/NewItemAddedConfirmSnackbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class NewItemAddedConfirmSnackbar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | openNewItemAddedConfirmSnackbar,
18 | closeNewItemConfirmSnackbar
19 | } = this.props;
20 | return (
21 |
34 |
40 |
41 | );
42 | }
43 | }
44 |
45 | NewItemAddedConfirmSnackbar.propTypes = {
46 | classes: PropTypes.object.isRequired
47 | };
48 |
49 | export default withStyles(styles)(NewItemAddedConfirmSnackbar);
50 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/Snackbars/NoRecordForDateRangeQuerySnackbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class NoRecordForDateRangeQuerySnackbar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | openNoRecordForDateRangeQuery,
18 | closeNoRecordForDateRangeQuery
19 | } = this.props;
20 | return (
21 |
33 |
39 |
40 | );
41 | }
42 | }
43 |
44 | NoRecordForDateRangeQuerySnackbar.propTypes = {
45 | classes: PropTypes.object.isRequired
46 | };
47 |
48 | export default withStyles(styles)(NoRecordForDateRangeQuerySnackbar);
49 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/Snackbars/WrongDateRangeSnackBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Snackbar from "@material-ui/core/Snackbar";
3 | import MySnackbarContent from "../../UtilFunctions/MySnackbarContent";
4 | import { withStyles } from "@material-ui/core";
5 | import { styles } from "../../commonStyles/AddNewItemStyles";
6 | import PropTypes from "prop-types";
7 |
8 | class WrongDateRangeSnackBar extends Component {
9 | state = {
10 | vertical: "top",
11 | horizontal: "center"
12 | };
13 |
14 | render() {
15 | const {
16 | classes,
17 | closeWrongDateRangeSnackBar,
18 | openWrongDateRangeSnackBar
19 | } = this.props;
20 | return (
21 |
33 |
39 |
40 | );
41 | }
42 | }
43 |
44 | WrongDateRangeSnackBar.propTypes = {
45 | classes: PropTypes.object.isRequired
46 | };
47 |
48 | export default withStyles(styles)(WrongDateRangeSnackBar);
49 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/TableHeadEmployees.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import TableCell from "@material-ui/core/TableCell";
4 | import TableHead from "@material-ui/core/TableHead";
5 | import TableRow from "@material-ui/core/TableRow";
6 | import TableSortLabel from "@material-ui/core/TableSortLabel";
7 | import Checkbox from "@material-ui/core/Checkbox";
8 | import Tooltip from "@material-ui/core/Tooltip";
9 | import { styles } from "../commonStyles/ModuleItemListStyles";
10 | import toolbarStyles from "../commonStyles/toolbarStyles";
11 | import combineStyles from "../commonStyles/combineStyles";
12 | import { withStyles } from "@material-ui/core";
13 |
14 | const rows = [
15 | {
16 | tableHeaderProp: "department_name",
17 | disablePadding: true,
18 | label: "Department"
19 | },
20 | {
21 | tableHeaderProp: "employee_name",
22 | disablePadding: true,
23 | label: "Employee Name"
24 | },
25 | {
26 | tableHeaderProp: "work_description",
27 | disablePadding: true,
28 | label: "Work Description"
29 | },
30 | {
31 | tableHeaderProp: "avg_employee_productivity",
32 | disablePadding: false,
33 | label: "Avg. Employee Productivity"
34 | },
35 | {
36 | tableHeaderProp: "benchmark_employee_productivity",
37 | disablePadding: false,
38 | label: "Benchmark Employee Productivity"
39 | },
40 |
41 | {
42 | tableHeaderProp: "date",
43 | disablePadding: false,
44 | label: "Date of Employment"
45 | }
46 | ];
47 |
48 | class TableHeadEmployee extends React.Component {
49 | state = {
50 | arrowRef: null
51 | };
52 |
53 | // function to handle the placement of the arrow on top of the Tooltip
54 | handleArrowRef = node => {
55 | this.setState({
56 | arrowRef: node
57 | });
58 | };
59 |
60 | createSortHandler = property => event => {
61 | this.props.onRequestSort(event, property);
62 | };
63 |
64 | render() {
65 | const {
66 | onSelectAllClick,
67 | order,
68 | orderBy,
69 | numSelected,
70 | page,
71 | rowsPerPage,
72 | count,
73 | noOfItemsInCurrentPage
74 | } = this.props;
75 |
76 | const { classes } = this.props;
77 |
78 | return (
79 |
80 |
81 |
84 |
85 | {noOfItemsInCurrentPage > 1
86 | ? `Select all ${noOfItemsInCurrentPage}`
87 | : `Select the item`}
88 |
89 |
90 |
91 | }
92 | enterDelay={300}
93 | placement={"top-end"}
94 | classes={{
95 | tooltip: classes.bootstrapTooltip,
96 | popper: classes.bootstrapPopper,
97 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
98 | tooltipPlacementRight: classes.bootstrapPlacementRight,
99 | tooltipPlacementTop: classes.bootstrapPlacementTop,
100 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
101 | }}
102 | PopperProps={{
103 | popperOptions: {
104 | modifiers: {
105 | arrow: {
106 | enabled: Boolean(this.state.arrowRef),
107 | element: this.state.arrowRef
108 | }
109 | }
110 | }
111 | }}
112 | >
113 |
114 | 0 && numSelected < rowsPerPage
118 | : numSelected > 0 && numSelected < noOfItemsInCurrentPage
119 | }
120 | checked={
121 | noOfItemsInCurrentPage === 0
122 | ? false
123 | : page !== Math.max(0, Math.ceil(count / rowsPerPage) - 1)
124 | ? numSelected === rowsPerPage
125 | : noOfItemsInCurrentPage < rowsPerPage
126 | ? numSelected === noOfItemsInCurrentPage
127 | : numSelected === rowsPerPage
128 | }
129 | onChange={onSelectAllClick}
130 | />
131 |
132 |
133 | {rows.map(
134 | row => (
135 |
142 |
145 | Sort by {row.label}
146 |
150 |
151 | }
152 | enterDelay={300}
153 | placement={"top-end"}
154 | classes={{
155 | tooltip: classes.bootstrapTooltip,
156 | popper: classes.bootstrapPopper,
157 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
158 | tooltipPlacementRight: classes.bootstrapPlacementRight,
159 | tooltipPlacementTop: classes.bootstrapPlacementTop,
160 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
161 | }}
162 | PopperProps={{
163 | popperOptions: {
164 | modifiers: {
165 | arrow: {
166 | enabled: Boolean(this.state.arrowRef),
167 | element: this.state.arrowRef
168 | }
169 | }
170 | }
171 | }}
172 | >
173 |
178 | {row.label}
179 |
180 |
181 |
182 | ),
183 | this
184 | )}
185 |
186 |
187 | );
188 | }
189 | }
190 |
191 | TableHeadEmployee.propTypes = {
192 | numSelected: PropTypes.number.isRequired,
193 | onRequestSort: PropTypes.func.isRequired,
194 | onSelectAllClick: PropTypes.func.isRequired,
195 | order: PropTypes.string.isRequired,
196 | orderBy: PropTypes.string.isRequired,
197 | rowsPerPage: PropTypes.number.isRequired,
198 | classes: PropTypes.object.isRequired
199 | };
200 |
201 | const combinedStyles = combineStyles(styles, toolbarStyles);
202 |
203 | export default withStyles(combinedStyles)(TableHeadEmployee);
204 |
205 | /* The expression < Math.max(0, Math.ceil(count / rowsPerPage) - 1) > is the index value of the variable 'page' ONLY when I am on the last page. So the condition //#endregion
206 | < page === Math.max(0, Math.ceil(count / rowsPerPage) - 1) > WILL ONLY BE TRUE on the last page.
207 |
208 | Note, - The variable 'page' is a built-in prop of TablePagination API (https://material-ui.com/api/table-pagination/) - and the value of 'page' is a zero-based index of the current page.
209 | */
210 |
--------------------------------------------------------------------------------
/client/src/Components/Employee/__test__/EmployeeList.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import expect from "expect";
4 | import { mount, render } from "enzyme";
5 | import { createShallow } from "@material-ui/core/test-utils";
6 | import sinon from "sinon";
7 | import { Helmet } from "react-helmet";
8 | import Paper from "@material-ui/core/Paper";
9 | import axios from "axios";
10 | import nock from "nock";
11 | import TableRow from "@material-ui/core/TableRow";
12 | import TableHeadEmployee from "../TableHeadEmployees";
13 | import TableToolbarEmployee from "../TableToolbarEmployees";
14 | import AddNewEmployee from "../AddNewEmployee";
15 | import EmployeeList from "../EmployeeList";
16 | // const moment = require("moment");
17 | // import Table from "@material-ui/core/Table";
18 | // import TableBody from "@material-ui/core/TableBody";
19 | // import TableCell from "@material-ui/core/TableCell";
20 | // import IconButton from "@material-ui/core/IconButton";
21 | // import { Icon } from "@material-ui/core";
22 |
23 | const host = "http://localhost:3000";
24 |
25 | const mockEmployeeData = [
26 | {
27 | _id: "5c793b825b9fac072bab18c8",
28 | department_objectId: {
29 | _id: "5c6c0f839c84ea3c7194a7dd",
30 | name: "Coal",
31 | type: "Minerals",
32 | createdAt: "2019-02-19T14:15:31.140Z",
33 | updatedAt: "2019-02-19T14:15:31.140Z",
34 | __v: 0
35 | },
36 | avg_employee_productivity: 2,
37 | benchmark_employee_productivity: 1,
38 | date: "2019-03-03T00:00:00.000Z",
39 | createdAt: "2019-03-01T14:02:42.177Z",
40 | updatedAt: "2019-03-03T08:48:11.678Z",
41 | __v: 0
42 | },
43 | {
44 | _id: "5c7937665b9fac072bab18c6",
45 | department_objectId: {
46 | _id: "5c651f71ddff9f780e4cdbcd",
47 | name: "Platinum",
48 | type: "Precious Metals",
49 | createdAt: "2019-02-14T07:57:37.736Z",
50 | updatedAt: "2019-02-28T12:53:50.876Z",
51 | __v: 0
52 | },
53 | avg_employee_productivity: 11,
54 | benchmark_employee_productivity: 111,
55 | date: "2019-03-01T13:43:56.249Z",
56 | createdAt: "2019-03-01T13:45:10.027Z",
57 | updatedAt: "2019-03-01T13:45:10.027Z",
58 | __v: 0
59 | }
60 | ];
61 |
62 | const departmentListProps = [
63 | {
64 | _id: "5c6c0f969c84ea3c7194a7de",
65 | name: "Ash",
66 | type: "Industrial Material",
67 | createdAt: "2019-02-19T14:15:50.690Z",
68 | updatedAt: "2019-02-19T14:15:50.690Z",
69 | __v: 0
70 | },
71 | {
72 | _id: "5c6c0f839c84ea3c7194a7dd",
73 | name: "Coal",
74 | type: "Minerals",
75 | createdAt: "2019-02-19T14:15:31.140Z",
76 | updatedAt: "2019-02-19T14:15:31.140Z",
77 | __v: 0
78 | },
79 |
80 | {
81 | _id: "5c651f44ddff9f780e4cdbcc",
82 | name: "Steel123",
83 | type: "Department",
84 | createdAt: "2019-02-14T07:56:52.310Z",
85 | updatedAt: "2019-02-27T11:05:55.121Z",
86 | __v: 0
87 | }
88 | ];
89 |
90 | const initialState = {
91 | totalItemsUnformatted: [],
92 | totalItemsFormatted: [],
93 | order: "desc",
94 | orderBy: "date",
95 | selected: [],
96 | page: 0,
97 | rowsPerPage: 5,
98 | queryStringFromChild: "",
99 | columnToQuery: "department_name",
100 | itemsDateRangePaginated: [],
101 | totalDateRangeSearchResultParent: [],
102 | start_date: new Date(),
103 | end_date: new Date(),
104 | ifUserSearchedDateRange: false,
105 | ifUserClickedForCurrentMonth: false,
106 | currentMonthPaginated: [],
107 | currentMonthTotal: [],
108 | currentDatePaginated: [],
109 | currentDateTotal: [],
110 | arrowRef: null
111 | };
112 |
113 | describe("", () => {
114 | const shallow = createShallow();
115 |
116 | afterEach(() => {
117 | // if (!nock.isDone()) {
118 | // this.test.error(new Error('Not all nock interceptors were used!'));
119 | nock.cleanAll();
120 | // nock.restore();
121 | // }
122 | });
123 |
124 | beforeEach(done => {
125 | if (!nock.isActive()) nock.activate();
126 | done();
127 | });
128 |
129 | test("EmployeeList component renders", () => {
130 | const wrapper = shallow();
131 | expect(wrapper.exists()).toBe(true);
132 | });
133 |
134 | it("should renders without crashing", () => {
135 | const div = document.createElement("div");
136 | ReactDOM.render(
137 | ,
140 | div
141 | );
142 | ReactDOM.unmountComponentAtNode(div);
143 | });
144 |
145 | test("renders", () => {
146 | const wrapper = shallow(
147 |
151 | );
152 |
153 | expect(wrapper).toMatchSnapshot();
154 | });
155 |
156 | test("returns the default empty array when there is no data to map through or what would happen when I render the component with no data", () => {
157 | const wrapper = shallow();
158 | expect(wrapper).toMatchSnapshot();
159 | });
160 |
161 | test("doesn't break without list of data", () => {
162 | const wrapper = shallow();
163 | expect(wrapper.find("li")).toHaveLength(0);
164 | });
165 |
166 | test("doesn't break with an empty array", () => {
167 | const wrapper = shallow();
168 | expect(wrapper.find("li")).toHaveLength(0);
169 | });
170 |
171 | it("should render a placeholder", () => {
172 | const placeholder_text = "type anything here";
173 | const wrapper = shallow();
174 | expect(wrapper.prop("placeholder")).toEqual(placeholder_text);
175 | });
176 |
177 | it("should render a correct type", () => {
178 | const type = "password";
179 | const wrapper = shallow();
180 | expect(wrapper.prop("type")).toEqual(type);
181 | });
182 |
183 | it("renders one components", () => {
184 | const wrapper = shallow();
185 | expect(wrapper.dive().find(TableHeadEmployee)).toHaveLength(1);
186 | });
187 |
188 | it("renders one components", () => {
189 | const wrapper = shallow();
190 | expect(wrapper.dive().find(TableToolbarEmployee)).toHaveLength(1);
191 | });
192 |
193 | it("renders one components", () => {
194 | const wrapper = shallow();
195 | expect(wrapper.dive().find(AddNewEmployee)).toHaveLength(1);
196 | });
197 |
198 | it("renders the Helmet wrapper component", () => {
199 | const component = shallow();
200 | const title = component
201 | .dive()
202 | .find(Helmet)
203 | .text();
204 |
205 | expect(title).toBe("");
206 | });
207 |
208 | it("should render react-helment title", () => {
209 | const wrapper = mount(
210 |
213 | );
214 | // this will return all the markup assigned to helmet
215 | // which will get rendered inside head.
216 | const helmet = Helmet.peek();
217 | expect(helmet.title).toBe("MyCompany | Employee!");
218 | // expect(helmet.metaTags[1].name).toBe("description");
219 | // expect(helmet.metaTags[2].content).toBe("MyCompany List of Vessel Types!");
220 | // console.log("HELMET IS ", helmet.content);
221 | });
222 |
223 | it("calls componentDidMount", () => {
224 | sinon.spy(EmployeeList.prototype, "componentDidMount");
225 | const wrapper = mount(
226 |
229 | );
230 | expect(EmployeeList.prototype.componentDidMount).toHaveProperty(
231 | "callCount",
232 | 1
233 | );
234 | });
235 |
236 | it("allows us to set props", () => {
237 | const wrapper = mount(
238 |
242 | );
243 | expect(wrapper.props().bar).toEqual("baz");
244 | wrapper.setProps({ bar: "foo" });
245 | expect(wrapper.props().bar).toEqual("foo");
246 | });
247 |
248 | it("should have the initial state set properly", () => {
249 | const wrapper = shallow();
250 | expect(wrapper.dive().state()).toEqual(initialState);
251 | });
252 |
253 | it("should render correct number of Paper and TableRow MUI component", () => {
254 | const component = mount(
255 |
258 | );
259 | expect(component.find(Paper)).toHaveLength(2);
260 | expect(component.find(TableRow)).toHaveLength(3);
261 | });
262 |
263 | // Test proptypes for allDepartmentsForSiblingCommunication
264 | it("check the type of allDepartmentsForSiblingCommunication", () => {
265 | const props = {
266 | allDepartmentsForSiblingCommunication: [
267 | {
268 | _id: "5c6c0f969c84ea3c7194a7de",
269 | name: "Ash",
270 | type: "Industrial Material",
271 | createdAt: "2019-02-19T14:15:50.690Z",
272 | updatedAt: "2019-02-19T14:15:50.690Z",
273 | __v: 0
274 | }
275 | ]
276 | };
277 | const wrapper = mount();
278 | expect(wrapper.prop("allDepartmentsForSiblingCommunication")).toHaveLength(
279 | 1
280 | );
281 | });
282 |
283 | it("API test-1 - GET (/api/employee)", done => {
284 | nock(host)
285 | .defaultReplyHeaders({
286 | "access-control-allow-origin": "*",
287 | "Content-Type": "application/json"
288 | })
289 |
290 | .get("/api/employee")
291 | .reply(200, "Get data");
292 | // .log(console.log);
293 |
294 | axios.get("http://localhost:3000/api/employee").then(response => {
295 | expect(response.data).toBe("Get data");
296 | expect(typeof response.data).toBe("string");
297 | done();
298 | });
299 | });
300 | });
301 |
302 | /* Shallow tests with 'withStyles' - needed to switch from `shallow` to `mount` since `withStyles` is a higher order component and 'wraps' the other component - (https://github.com/mui-org/material-ui/issues/9266) */
303 |
--------------------------------------------------------------------------------
/client/src/Components/LandingPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Welcome from "react-welcome-page";
3 |
4 | // A functional component only for the landing page
5 | // The height & width has no effect though (I included for testing)
6 | const LandingPage = () => ({
7 | render() {
8 | return (
9 |
10 |
50 |
51 | );
52 | }
53 | });
54 |
55 | export default LandingPage;
56 |
--------------------------------------------------------------------------------
/client/src/Components/NotFound.css:
--------------------------------------------------------------------------------
1 | /* The below google font is overwriting my fonts in main comopnent, hence commented out */
2 | /* @import url('https://fonts.googleapis.com/css?family=Dosis:300,400,500'); */
3 |
4 | @-moz-keyframes rocket-movement {
5 | 100% {
6 | -moz-transform: translate(1200px, -600px);
7 | }
8 | }
9 |
10 | @-webkit-keyframes rocket-movement {
11 | 100% {
12 | -webkit-transform: translate(1200px, -600px);
13 | }
14 | }
15 |
16 | @keyframes rocket-movement {
17 | 100% {
18 | transform: translate(1200px, -600px);
19 | }
20 | }
21 |
22 | @-moz-keyframes spin-earth {
23 | 100% {
24 | -moz-transform: rotate(-360deg);
25 | transition: transform 20s;
26 | }
27 | }
28 |
29 | @-webkit-keyframes spin-earth {
30 | 100% {
31 | -webkit-transform: rotate(-360deg);
32 | transition: transform 20s;
33 | }
34 | }
35 |
36 | @keyframes spin-earth {
37 | 100% {
38 | -webkit-transform: rotate(-360deg);
39 | transform: rotate(-360deg);
40 | transition: transform 20s;
41 | }
42 | }
43 |
44 | @-moz-keyframes move-astronaut {
45 | 100% {
46 | -moz-transform: translate(-160px, -160px);
47 | }
48 | }
49 |
50 | @-webkit-keyframes move-astronaut {
51 | 100% {
52 | -webkit-transform: translate(-160px, -160px);
53 | }
54 | }
55 |
56 | @keyframes move-astronaut {
57 | 100% {
58 | -webkit-transform: translate(-160px, -160px);
59 | transform: translate(-160px, -160px);
60 | }
61 | }
62 |
63 | @-moz-keyframes rotate-astronaut {
64 | 100% {
65 | -moz-transform: rotate(-720deg);
66 | }
67 | }
68 |
69 | @-webkit-keyframes rotate-astronaut {
70 | 100% {
71 | -webkit-transform: rotate(-720deg);
72 | }
73 | }
74 |
75 | @keyframes rotate-astronaut {
76 | 100% {
77 | -webkit-transform: rotate(-720deg);
78 | transform: rotate(-720deg);
79 | }
80 | }
81 |
82 | @-moz-keyframes glow-star {
83 | 40% {
84 | -moz-opacity: 0.3;
85 | }
86 |
87 | 90%,
88 | 100% {
89 | -moz-opacity: 1;
90 | -moz-transform: scale(1.2);
91 | }
92 | }
93 |
94 | @-webkit-keyframes glow-star {
95 | 40% {
96 | -webkit-opacity: 0.3;
97 | }
98 |
99 | 90%,
100 | 100% {
101 | -webkit-opacity: 1;
102 | -webkit-transform: scale(1.2);
103 | }
104 | }
105 |
106 | @keyframes glow-star {
107 | 40% {
108 | -webkit-opacity: 0.3;
109 | opacity: 0.3;
110 | }
111 |
112 | 90%,
113 | 100% {
114 | -webkit-opacity: 1;
115 | opacity: 1;
116 | -webkit-transform: scale(1.2);
117 | transform: scale(1.2);
118 | border-radius: 999999px;
119 | }
120 | }
121 |
122 | .spin-earth-on-hover {
123 |
124 | transition: ease 200s !important;
125 | transform: rotate(-3600deg) !important;
126 | }
127 |
128 | html,
129 | body {
130 | margin: 0;
131 | width: 100%;
132 | height: 100%;
133 | font-family: 'Dosis', sans-serif;
134 | font-weight: 300;
135 | -webkit-user-select: none;
136 | /* Safari 3.1+ */
137 | -moz-user-select: none;
138 | /* Firefox 2+ */
139 | -ms-user-select: none;
140 | /* IE 10+ */
141 | user-select: none;
142 | /* Standard syntax */
143 | }
144 |
145 | .bg-purple {
146 | background: url("../assets/images/bg_purple.png");
147 | background-repeat: repeat-x;
148 | background-size: cover;
149 | background-position: left top;
150 | height: 100vh;
151 | overflow: hidden;
152 |
153 | }
154 |
155 | /* --------------------------- */
156 |
157 |
158 | .custom-navbar {
159 | padding-top: 15px;
160 | }
161 |
162 | .brand-logo {
163 | margin-left: 25px;
164 | margin-top: 5px;
165 | display: inline-block;
166 | }
167 |
168 | .navbar-links {
169 | display: inline-block;
170 | float: right;
171 | margin-right: 15px;
172 | text-transform: uppercase;
173 |
174 |
175 | }
176 |
177 | ul {
178 | list-style-type: none;
179 | margin: 0;
180 | padding: 0;
181 | align-items: center;
182 | }
183 |
184 | li {
185 |
186 | padding: 0px 15px;
187 | }
188 |
189 | li a {
190 | display: block;
191 | color: white;
192 | text-align: center;
193 | text-decoration: none;
194 | letter-spacing: 2px;
195 | font-size: 12px;
196 |
197 | -webkit-transition: all 0.3s ease-in;
198 | -moz-transition: all 0.3s ease-in;
199 | -ms-transition: all 0.3s ease-in;
200 | -o-transition: all 0.3s ease-in;
201 | transition: all 0.3s ease-in;
202 | }
203 |
204 | li a:hover {
205 | color: #ffcb39;
206 | }
207 |
208 | /* clear */
209 | .btn-request {
210 | padding: 10px 25px;
211 | border: 1px solid #FFCB39;
212 | border-radius: 100px;
213 | font-weight: 400;
214 | }
215 |
216 | .btn-request:hover {
217 | background-color: #FFCB39;
218 | color: #fff;
219 | transform: scale(1.05);
220 | box-shadow: 0px 20px 20px rgba(0, 0, 0, 0.1);
221 | }
222 |
223 | .btn-go-home {
224 | position: relative;
225 | z-index: 200;
226 | margin: 15px auto;
227 | width: 100px;
228 | padding: 10px 15px;
229 | border: 1px solid #FFCB39;
230 | border-radius: 100px;
231 | font-weight: 400;
232 | display: block;
233 | color: white;
234 | text-align: center;
235 | text-decoration: none;
236 | letter-spacing: 2px;
237 | font-size: 11px;
238 |
239 | -webkit-transition: all 0.3s ease-in;
240 | -moz-transition: all 0.3s ease-in;
241 | -ms-transition: all 0.3s ease-in;
242 | -o-transition: all 0.3s ease-in;
243 | transition: all 0.3s ease-in;
244 | }
245 |
246 | .btn-go-home:hover {
247 | background-color: #FFCB39;
248 | color: #fff;
249 | transform: scale(1.05);
250 | box-shadow: 0px 20px 20px rgba(0, 0, 0, 0.1);
251 | }
252 |
253 | /* clear */
254 | .central-body {
255 | /* width: 100%;*/
256 | padding: 17% 5% 10% 5%;
257 | text-align: center;
258 | }
259 |
260 | .objects img {
261 | z-index: 90;
262 | pointer-events: none;
263 | }
264 |
265 | /* ============================= */
266 | .object_rocket {
267 | z-index: 95;
268 | position: absolute;
269 | transform: translateX(-50px);
270 | top: 75%;
271 | pointer-events: none;
272 | animation: rocket-movement 200s linear infinite both running;
273 | }
274 |
275 | .object_earth {
276 | position: absolute;
277 | top: 20%;
278 | left: 15%;
279 | z-index: 90;
280 | /* animation: spin-earth 100s infinite linear both;*/
281 | }
282 |
283 | .object_moon {
284 | position: absolute;
285 | top: 12%;
286 | left: 25%;
287 | /*
288 | transform: rotate(0deg);
289 | transition: transform ease-in 99999999999s;
290 | */
291 | }
292 |
293 | .earth-moon {}
294 |
295 | .object_astronaut {
296 | animation: rotate-astronaut 200s infinite linear both alternate;
297 | }
298 |
299 | .box_astronaut {
300 | z-index: 110 !important;
301 | position: absolute;
302 | top: 60%;
303 | right: 20%;
304 | will-change: transform;
305 | animation: move-astronaut 50s infinite linear both alternate;
306 | }
307 |
308 | .image-404 {
309 | position: relative;
310 | z-index: 100;
311 | pointer-events: none;
312 | }
313 |
314 | .stars {
315 | background: url("../assets/images/overlay_stars.svg");
316 | background-repeat: repeat;
317 | background-size: contain;
318 | background-position: left top;
319 | }
320 |
321 | .glowing_stars .star {
322 | position: absolute;
323 | border-radius: 100%;
324 | background-color: #fff;
325 | width: 3px;
326 | height: 3px;
327 | opacity: 0.3;
328 | will-change: opacity;
329 | }
330 |
331 | .glowing_stars .star:nth-child(1) {
332 | top: 80%;
333 | left: 25%;
334 | animation: glow-star 2s infinite ease-in-out alternate 1s;
335 | }
336 |
337 | .glowing_stars .star:nth-child(2) {
338 | top: 20%;
339 | left: 40%;
340 | animation: glow-star 2s infinite ease-in-out alternate 3s;
341 | }
342 |
343 | .glowing_stars .star:nth-child(3) {
344 | top: 25%;
345 | left: 25%;
346 | animation: glow-star 2s infinite ease-in-out alternate 5s;
347 | }
348 |
349 | .glowing_stars .star:nth-child(4) {
350 | top: 75%;
351 | left: 80%;
352 | animation: glow-star 2s infinite ease-in-out alternate 7s;
353 | }
354 |
355 | .glowing_stars .star:nth-child(5) {
356 | top: 90%;
357 | left: 50%;
358 | animation: glow-star 2s infinite ease-in-out alternate 9s;
359 | }
360 |
361 | @media only screen and (max-width: 600px) {
362 | .navbar-links {
363 | display: none;
364 | }
365 |
366 | .custom-navbar {
367 | text-align: center;
368 | }
369 |
370 | .brand-logo img {
371 | width: 120px;
372 | }
373 |
374 | .box_astronaut {
375 | top: 70%;
376 | }
377 |
378 | .central-body {
379 | padding-top: 25%;
380 | }
381 | }
--------------------------------------------------------------------------------
/client/src/Components/NotFound.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 | import "./NotFound.css";
4 |
5 | export default class NotFound extends Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
12 |
})
16 |
17 |
18 |
19 | -
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 |
28 |
})
33 |
34 | Return to Home Page
35 |
36 |
37 |
38 |
})
43 |
44 |
})
49 |
})
54 |
55 |
56 |
})
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/client/src/Components/UtilFunctions/MySnackbarContent.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import classNames from "classnames";
4 | import CheckCircleIcon from "@material-ui/icons/CheckCircle";
5 | import ErrorIcon from "@material-ui/icons/Error";
6 | import InfoIcon from "@material-ui/icons/Info";
7 | import CloseIcon from "@material-ui/icons/Close";
8 | import green from "@material-ui/core/colors/green";
9 | import amber from "@material-ui/core/colors/amber";
10 | import IconButton from "@material-ui/core/IconButton";
11 | import SnackbarContent from "@material-ui/core/SnackbarContent";
12 | import WarningIcon from "@material-ui/icons/Warning";
13 | import { withStyles } from "@material-ui/core/styles";
14 |
15 | const variantIcon = {
16 | success: CheckCircleIcon,
17 | warning: WarningIcon,
18 | error: ErrorIcon,
19 | info: InfoIcon
20 | };
21 |
22 | const styles1 = theme => ({
23 | close: {
24 | padding: theme.spacing.unit * 2
25 | },
26 | success: {
27 | backgroundColor: green[600]
28 | },
29 | error: {
30 | backgroundColor: theme.palette.error.dark
31 | },
32 | info: {
33 | backgroundColor: theme.palette.primary.dark
34 | },
35 | warning: {
36 | backgroundColor: amber[700]
37 | },
38 | icon: {
39 | fontSize: 20
40 | },
41 | iconVariant: {
42 | opacity: 0.9,
43 | marginRight: theme.spacing.unit
44 | },
45 | message: {
46 | display: "flex",
47 | alignItems: "center"
48 | }
49 | });
50 |
51 | function MySnackbarContent(props) {
52 | const { classes, className, message, onClose, variant, ...other } = props;
53 | const Icon = variantIcon[variant];
54 |
55 | return (
56 |
61 |
62 | {message}
63 |
64 | }
65 | action={[
66 |
73 |
74 |
75 | ]}
76 | {...other}
77 | />
78 | );
79 | }
80 |
81 | MySnackbarContent.propTypes = {
82 | classes: PropTypes.object.isRequired,
83 | className: PropTypes.string,
84 | message: PropTypes.node,
85 | onClose: PropTypes.func,
86 | variant: PropTypes.oneOf(["success", "warning", "error", "info"]).isRequired
87 | };
88 |
89 | export default withStyles(styles1)(MySnackbarContent);
90 |
--------------------------------------------------------------------------------
/client/src/Components/UtilFunctions/TablePaginationActionsWrapped-bkp.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { withStyles } from "@material-ui/core/styles";
4 | import IconButton from "@material-ui/core/IconButton";
5 | import FirstPageIcon from "@material-ui/icons/FirstPage";
6 | import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft";
7 | import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
8 | import LastPageIcon from "@material-ui/icons/LastPage";
9 | import toolbarStyles from "../commonStyles/toolbarStyles";
10 | import Tooltip from "@material-ui/core/Tooltip";
11 |
12 | const actionsStyles = theme => ({
13 | root: {
14 | flexShrink: 0,
15 | color: theme.palette.text.secondary,
16 | marginLeft: theme.spacing.unit * 2.5
17 | }
18 | });
19 |
20 | class TablePaginationActions extends React.Component {
21 | state = {
22 | arrowRef: null
23 | };
24 |
25 | // function to handle the placement of the arrow on top of the Tooltip
26 | handleArrowRef = node => {
27 | this.setState({
28 | arrowRef: node
29 | });
30 | };
31 |
32 | handleFirstPageButtonClick = event => {
33 | this.props.onChangePage(event, 0);
34 | };
35 |
36 | handleBackButtonClick = event => {
37 | this.props.onChangePage(event, this.props.page - 1);
38 | };
39 |
40 | handleNextButtonClick = event => {
41 | this.props.onChangePage(event, this.props.page + 1);
42 | };
43 |
44 | handleLastPageButtonClick = event => {
45 | this.props.onChangePage(
46 | event,
47 | Math.max(0, Math.ceil(this.props.count / this.props.rowsPerPage) - 1)
48 | );
49 | };
50 |
51 | render() {
52 | const { classes, count, page, rowsPerPage, theme } = this.props;
53 |
54 | return (
55 |
56 |
59 | First page
60 |
61 |
62 | }
63 | placement="bottom-start"
64 | classes={{
65 | tooltip: classes.bootstrapTooltip,
66 | popper: classes.bootstrapPopper,
67 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
68 | tooltipPlacementRight: classes.bootstrapPlacementRight,
69 | tooltipPlacementTop: classes.bootstrapPlacementTop,
70 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
71 | }}
72 | PopperProps={{
73 | popperOptions: {
74 | modifiers: {
75 | arrow: {
76 | enabled: Boolean(this.state.arrowRef),
77 | element: this.state.arrowRef
78 | }
79 | }
80 | }
81 | }}
82 | >
83 |
88 | {theme.direction === "rtl" ? : }
89 |
90 |
91 |
94 | Previous page
95 |
96 |
97 | }
98 | placement="bottom-start"
99 | classes={{
100 | tooltip: classes.bootstrapTooltip,
101 | popper: classes.bootstrapPopper,
102 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
103 | tooltipPlacementRight: classes.bootstrapPlacementRight,
104 | tooltipPlacementTop: classes.bootstrapPlacementTop,
105 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
106 | }}
107 | PopperProps={{
108 | popperOptions: {
109 | modifiers: {
110 | arrow: {
111 | enabled: Boolean(this.state.arrowRef),
112 | element: this.state.arrowRef
113 | }
114 | }
115 | }
116 | }}
117 | >
118 |
123 | {theme.direction === "rtl" ? (
124 |
125 | ) : (
126 |
127 | )}
128 |
129 |
130 |
133 | Next page
134 |
135 |
136 | }
137 | placement="bottom-start"
138 | classes={{
139 | tooltip: classes.bootstrapTooltip,
140 | popper: classes.bootstrapPopper,
141 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
142 | tooltipPlacementRight: classes.bootstrapPlacementRight,
143 | tooltipPlacementTop: classes.bootstrapPlacementTop,
144 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
145 | }}
146 | PopperProps={{
147 | popperOptions: {
148 | modifiers: {
149 | arrow: {
150 | enabled: Boolean(this.state.arrowRef),
151 | element: this.state.arrowRef
152 | }
153 | }
154 | }
155 | }}
156 | >
157 | = Math.ceil(count / rowsPerPage) - 1}
160 | aria-label="Next Page"
161 | >
162 | {theme.direction === "rtl" ? (
163 |
164 | ) : (
165 |
166 | )}
167 |
168 |
169 |
172 | Last page
173 |
174 |
175 | }
176 | placement="bottom-start"
177 | classes={{
178 | tooltip: classes.bootstrapTooltip,
179 | popper: classes.bootstrapPopper,
180 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
181 | tooltipPlacementRight: classes.bootstrapPlacementRight,
182 | tooltipPlacementTop: classes.bootstrapPlacementTop,
183 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
184 | }}
185 | PopperProps={{
186 | popperOptions: {
187 | modifiers: {
188 | arrow: {
189 | enabled: Boolean(this.state.arrowRef),
190 | element: this.state.arrowRef
191 | }
192 | }
193 | }
194 | }}
195 | >
196 | = Math.ceil(count / rowsPerPage) - 1}
199 | aria-label="Last Page"
200 | >
201 | {theme.direction === "rtl" ? : }
202 |
203 |
204 |
205 | );
206 | }
207 | }
208 |
209 | TablePaginationActions.propTypes = {
210 | classes: PropTypes.object.isRequired,
211 | count: PropTypes.number.isRequired,
212 | onChangePage: PropTypes.func.isRequired,
213 | page: PropTypes.number.isRequired,
214 | rowsPerPage: PropTypes.number.isRequired,
215 | theme: PropTypes.object.isRequired
216 | };
217 |
218 | // Make an HOC from the above TablePaginationActions component which will be exported and used in DevelopmentList.js
219 | const TablePaginationActionsWrapped = withStyles(actionsStyles, {
220 | withTheme: true
221 | })(TablePaginationActions);
222 |
223 | export default withStyles(toolbarStyles)(TablePaginationActionsWrapped);
224 |
--------------------------------------------------------------------------------
/client/src/Components/UtilFunctions/TablePaginationActionsWrapped.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { withStyles } from "@material-ui/core/styles";
4 | import IconButton from "@material-ui/core/IconButton";
5 | import FirstPageIcon from "@material-ui/icons/FirstPage";
6 | import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft";
7 | import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
8 | import LastPageIcon from "@material-ui/icons/LastPage";
9 | import Tooltip from "@material-ui/core/Tooltip";
10 |
11 | const actionsStyles = theme => ({
12 | root: {
13 | flexShrink: 0,
14 | color: theme.palette.text.secondary,
15 | marginLeft: theme.spacing.unit * 2.5
16 | }
17 | });
18 |
19 | class TablePaginationActions extends React.Component {
20 | state = {
21 | arrowRef: null
22 | };
23 |
24 | // function to handle the placement of the arrow on top of the Tooltip
25 | handleArrowRef = node => {
26 | this.setState({
27 | arrowRef: node
28 | });
29 | };
30 |
31 | handleFirstPageButtonClick = event => {
32 | this.props.onChangePage(event, 0);
33 | };
34 |
35 | handleBackButtonClick = event => {
36 | this.props.onChangePage(event, this.props.page - 1);
37 | };
38 |
39 | handleNextButtonClick = event => {
40 | this.props.onChangePage(event, this.props.page + 1);
41 | };
42 |
43 | handleLastPageButtonClick = event => {
44 | this.props.onChangePage(
45 | event,
46 | Math.max(0, Math.ceil(this.props.count / this.props.rowsPerPage) - 1)
47 | );
48 | };
49 |
50 | render() {
51 | const { classes, count, page, rowsPerPage, theme } = this.props;
52 |
53 | return (
54 |
55 |
58 | First page
59 |
60 |
61 | }
62 | placement="bottom-start"
63 | classes={{
64 | tooltip: classes.bootstrapTooltip,
65 | popper: classes.bootstrapPopper,
66 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
67 | tooltipPlacementRight: classes.bootstrapPlacementRight,
68 | tooltipPlacementTop: classes.bootstrapPlacementTop,
69 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
70 | }}
71 | PopperProps={{
72 | popperOptions: {
73 | modifiers: {
74 | arrow: {
75 | enabled: Boolean(this.state.arrowRef),
76 | element: this.state.arrowRef
77 | }
78 | }
79 | }
80 | }}
81 | >
82 |
87 | {theme.direction === "rtl" ? : }
88 |
89 |
90 |
93 | Previous page
94 |
95 |
96 | }
97 | placement="bottom-start"
98 | classes={{
99 | tooltip: classes.bootstrapTooltip,
100 | popper: classes.bootstrapPopper,
101 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
102 | tooltipPlacementRight: classes.bootstrapPlacementRight,
103 | tooltipPlacementTop: classes.bootstrapPlacementTop,
104 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
105 | }}
106 | PopperProps={{
107 | popperOptions: {
108 | modifiers: {
109 | arrow: {
110 | enabled: Boolean(this.state.arrowRef),
111 | element: this.state.arrowRef
112 | }
113 | }
114 | }
115 | }}
116 | >
117 |
122 | {theme.direction === "rtl" ? (
123 |
124 | ) : (
125 |
126 | )}
127 |
128 |
129 |
132 | Next page
133 |
134 |
135 | }
136 | placement="bottom-start"
137 | classes={{
138 | tooltip: classes.bootstrapTooltip,
139 | popper: classes.bootstrapPopper,
140 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
141 | tooltipPlacementRight: classes.bootstrapPlacementRight,
142 | tooltipPlacementTop: classes.bootstrapPlacementTop,
143 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
144 | }}
145 | PopperProps={{
146 | popperOptions: {
147 | modifiers: {
148 | arrow: {
149 | enabled: Boolean(this.state.arrowRef),
150 | element: this.state.arrowRef
151 | }
152 | }
153 | }
154 | }}
155 | >
156 | = Math.ceil(count / rowsPerPage) - 1}
159 | aria-label="Next Page"
160 | >
161 | {theme.direction === "rtl" ? (
162 |
163 | ) : (
164 |
165 | )}
166 |
167 |
168 |
171 | Last page
172 |
173 |
174 | }
175 | placement="bottom-start"
176 | classes={{
177 | tooltip: classes.bootstrapTooltip,
178 | popper: classes.bootstrapPopper,
179 | tooltipPlacementLeft: classes.bootstrapPlacementLeft,
180 | tooltipPlacementRight: classes.bootstrapPlacementRight,
181 | tooltipPlacementTop: classes.bootstrapPlacementTop,
182 | tooltipPlacementBottom: classes.bootstrapPlacementBottom
183 | }}
184 | PopperProps={{
185 | popperOptions: {
186 | modifiers: {
187 | arrow: {
188 | enabled: Boolean(this.state.arrowRef),
189 | element: this.state.arrowRef
190 | }
191 | }
192 | }
193 | }}
194 | >
195 | = Math.ceil(count / rowsPerPage) - 1}
198 | aria-label="Last Page"
199 | >
200 | {theme.direction === "rtl" ? : }
201 |
202 |
203 |
204 | );
205 | }
206 | }
207 |
208 | TablePaginationActions.propTypes = {
209 | classes: PropTypes.object.isRequired,
210 | count: PropTypes.number.isRequired,
211 | onChangePage: PropTypes.func.isRequired,
212 | page: PropTypes.number.isRequired,
213 | rowsPerPage: PropTypes.number.isRequired,
214 | theme: PropTypes.object.isRequired
215 | };
216 |
217 | // Make an HOC from the above TablePaginationActions component which will be exported and used in DevelopmentList.js
218 | const TablePaginationActionsWrapped = withStyles(actionsStyles, {
219 | withTheme: true
220 | })(TablePaginationActions);
221 |
222 | export default TablePaginationActionsWrapped;
223 |
--------------------------------------------------------------------------------
/client/src/Components/UtilFunctions/confirmDelete.css:
--------------------------------------------------------------------------------
1 | .custom-ui {
2 | text-align: center;
3 | width: 500px;
4 | padding: 40px;
5 | background: #28bae6;
6 | box-shadow: 0 20px 75px rgba(0, 0, 0, 0.23);
7 | color: #fff;
8 | }
9 |
10 | .custom-ui>h1 {
11 | margin-top: 0;
12 | }
13 |
14 | .custom-ui>button {
15 | width: 160px;
16 | padding: 10px;
17 | border: 1px solid #fff;
18 | margin: 10px;
19 | cursor: pointer;
20 | background: none;
21 | color: #fff;
22 | font-size: 14px;
23 | }
--------------------------------------------------------------------------------
/client/src/Components/UtilFunctions/showEmptyFieldAndDeleteSnackbar.js:
--------------------------------------------------------------------------------
1 | // This snackbar will be rendered when I submit an empty description field in each of the individual modules like "Development Works", "Income & Port Dues" etc
2 | // Also this same showEmptyFieldSnackbar will render the snackbar after successfully deleting a document, port-notice, GO, Tariff or Tidal data
3 |
4 | module.exports = {
5 | showDeleteSnackbar: () => {
6 | var x = document.getElementById("snackbar");
7 | x.className = "show";
8 | setTimeout(() => {
9 | x.className = x.className.replace("show", "");
10 | }, 2000);
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/client/src/Components/UtilFunctions/snackbar.css:
--------------------------------------------------------------------------------
1 | #snackbar {
2 | visibility: hidden;
3 | min-width: 250px;
4 | margin-left: -250px;
5 | background-color: #333;
6 | color: #fff;
7 | text-align: center;
8 | border-radius: 2px;
9 | padding: 16px;
10 | position: fixed;
11 | z-index: 1;
12 | left: 50%;
13 | top: 100px;
14 | font-size: 17px;
15 | }
16 |
17 | #snackbar.show {
18 | visibility: visible;
19 | -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
20 | animation: fadein 0.5s, fadeout 0.5s 2.5s;
21 | }
22 |
23 | @-webkit-keyframes fadein {
24 | from {
25 | top: 0;
26 | opacity: 0;
27 | }
28 |
29 | to {
30 | top: 80px;
31 | opacity: 1;
32 | }
33 | }
34 |
35 | @keyframes fadein {
36 | from {
37 | top: 0;
38 | opacity: 0;
39 | }
40 |
41 | to {
42 | top: 80px;
43 | opacity: 1;
44 | }
45 | }
46 |
47 | @-webkit-keyframes fadeout {
48 | from {
49 | top: 80px;
50 | opacity: 1;
51 | }
52 |
53 | to {
54 | top: 0;
55 | opacity: 0;
56 | }
57 | }
58 |
59 | @keyframes fadeout {
60 | from {
61 | top: 80px;
62 | opacity: 1;
63 | }
64 |
65 | to {
66 | top: 0;
67 | opacity: 0;
68 | }
69 | }
--------------------------------------------------------------------------------
/client/src/Components/UtilFunctions/tableSortByHeading.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | desc: (a, b, orderBy) => {
3 | if (b[orderBy] < a[orderBy]) {
4 | return -1;
5 | }
6 | if (b[orderBy] > a[orderBy]) {
7 | return 1;
8 | }
9 | return 0;
10 | },
11 |
12 | /* stableSort() higher order function to sort by a table heading
13 | 1> the argument 'comparisonCB' is the getSorting() defined below.
14 | 2> The sort() method takes a single compareFunction as a callback and compares each pair of successive elements.
15 | */
16 | stableSort: (array, comparisonCB) => {
17 | const stabilizedThis = array.map((el, index) => [el, index]);
18 | stabilizedThis.sort((a, b) => {
19 | const order = comparisonCB(a[0], b[0]);
20 | if (order !== 0) return order;
21 | return a[1] - b[1];
22 | });
23 | return stabilizedThis.map(el => el[0]);
24 | },
25 |
26 | /* getSorting() to be passed as a callback to the above stableSort(). The argument 'orderBy' will initially start (as a state) its value with 'imported_date' (which is a table header label) and then will take on whichever table header label the user clicks on.
27 | A> First the 'orderBy' value will be set by handleRequestSort() with its argument 'property'
28 | B> Then this function will be passed down as a prop 'onRequestSort' to EnhancedTableHead child component.
29 | C> In EnhancedTableHead, it will be called within createSortHandler() function and will be invoked on onClick and passed row.tableHeaderProp (which is the Table-header field value)
30 | */
31 | getSorting: (order, orderBy) => {
32 | return order === "desc"
33 | ? (a, b) => module.exports.desc(a, b, orderBy)
34 | : (a, b) => -module.exports.desc(a, b, orderBy);
35 | }
36 | };
37 |
38 | /* Special Note - Inside the getSorting() function I am calling a “local” function (desc()) within module.exports from another function (getSorting()) in module.exports with < module.exports >
39 |
40 | */
41 |
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/AddNewItemStyles.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | styles: theme => ({
3 | palette: {
4 | primary: { main: '#9E9E9E' },
5 | secondary: { main: '#ee0053' },
6 | error: { main: '#ee0053' }
7 | },
8 | root: {
9 | width: '100%',
10 | background: 'linear-gradient(45deg, #E91E63 30%, #FF8E53 90%)',
11 | marginTop: theme.spacing.unit * 10,
12 | backgroundColor: '#E0E0E0'
13 | },
14 | paper: {
15 | backgroundColor: '#F5F5F5',
16 | boxShadow: 'none',
17 | overflow: 'hidden'
18 | },
19 | table: {
20 | minWidth: 1020
21 | },
22 | tableWrapper: {
23 | overflowX: 'auto'
24 | },
25 | gutters: theme.mixins.gutters(),
26 | styledHeader: {
27 | background: '#66CCFF',
28 | '& h1': {
29 | color: '#f9f9f6'
30 | },
31 | color: '#f9f9f6',
32 | fontSize: 25
33 | },
34 | styledFooter: {
35 | background: '#FAFAFA',
36 | '& h2': {
37 | color: '#FAFAFA'
38 | }
39 | },
40 | space: {
41 | marginTop: theme.spacing.unit * 4,
42 | marginBottom: theme.spacing.unit * 4
43 | },
44 | spaceDialogAction: {
45 | marginTop: theme.spacing.unit * 2,
46 | marginBottom: theme.spacing.unit,
47 | marginRight: theme.spacing.unit
48 | },
49 | fab: {
50 | margin: theme.spacing.unit * 4
51 | },
52 | extendedIcon: {
53 | marginRight: theme.spacing.unit
54 | },
55 | margin: {
56 | margin: theme.spacing.unit * 4
57 | },
58 |
59 | lightTooltip: {
60 | background: theme.palette.common.white,
61 | color: theme.palette.text.primary,
62 | boxShadow: theme.shadows[1],
63 | fontSize: 16
64 | },
65 | close: {
66 | padding: theme.spacing.unit * 2,
67 | fontSize: 25
68 | },
69 | formControl: {
70 | margin: theme.spacing.unit,
71 | maxWidth: 300
72 | },
73 | selectEmpty: {
74 | marginTop: theme.spacing.unit * 2
75 | },
76 | container: {
77 | display: 'flex',
78 | flexWrap: 'wrap'
79 | },
80 | notchedOutline: {
81 | border: '1px solid red'
82 | },
83 | overrides: {
84 | MuiOutlinedInput: {
85 | focused: {
86 | border: '1px solid #4A90E2'
87 | },
88 | '& $notchedOutline': {
89 | border: '1px solid #4A90E2'
90 | }
91 | }
92 | },
93 | underline: {
94 | '&:after': {
95 | borderBottomColor: 'rgb(70, 197, 29)',
96 | borderWidth: '1px'
97 | }
98 | }
99 | })
100 | };
101 |
102 | /*
103 | 1> In above, the 'space' will control the vertical line spacing between TestFields and DatePicker inside the Dialog (i.e. inside AddNewDevelopmentWork component)
104 |
105 | And 'spaceDialogAction' will control the vertical space
106 | */
107 |
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/AddNewItemThemes.js:
--------------------------------------------------------------------------------
1 | import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles";
2 |
3 | export default createMuiTheme({
4 | typography: {
5 | useNextVariants: true
6 | },
7 | palette: {
8 | primary: { main: "#2196f3" },
9 | secondary: { main: "#fdff00" },
10 | error: { main: "#f44336" }
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/BootstrapInput.js:
--------------------------------------------------------------------------------
1 | import { withStyles } from "@material-ui/core";
2 | import InputBase from "@material-ui/core/InputBase";
3 |
4 | const BootstrapInput = withStyles(theme => ({
5 | root: {
6 | "label + &": {
7 | marginTop: theme.spacing.unit * 3
8 | }
9 | },
10 | input: {
11 | borderRadius: 4,
12 | position: "relative",
13 | backgroundColor: theme.palette.background.paper,
14 | border: "1px solid #ced4da",
15 | fontSize: 16,
16 | width: "auto",
17 | padding: "10px 26px 10px 12px",
18 | transition: theme.transitions.create(["border-color", "box-shadow"]),
19 | // Use the system font instead of the default Roboto font.
20 | fontFamily: [
21 | "-apple-system",
22 | "BlinkMacSystemFont",
23 | '"Segoe UI"',
24 | "Roboto",
25 | '"Helvetica Neue"',
26 | "Arial",
27 | "sans-serif",
28 | '"Apple Color Emoji"',
29 | '"Segoe UI Emoji"',
30 | '"Segoe UI Symbol"'
31 | ].join(","),
32 | "&:focus": {
33 | borderRadius: 4,
34 | borderColor: "#80bdff",
35 | boxShadow: "0 0 0 0.2rem rgba(0,123,255,.25)"
36 | }
37 | }
38 | }))(InputBase);
39 |
40 | export default BootstrapInput;
41 |
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/ModuleItemListStyles.js:
--------------------------------------------------------------------------------
1 | export const styles = theme => ({
2 | palette: {
3 | primary: { main: "#2196f3" },
4 | secondary: { main: "#fdff00" },
5 | error: { main: "#ee0053" }
6 | },
7 | root: {
8 | width: "100%",
9 | marginTop: theme.spacing.unit * 3,
10 | overflow: "auto"
11 | },
12 | space: {
13 | marginTop: theme.spacing.unit * 2
14 | },
15 | fab: {
16 | margin: theme.spacing.unit
17 | },
18 | fabButton: {
19 | position: "fixed",
20 | zIndex: 1,
21 | top: "auto",
22 | bottom: 0,
23 | marginBottom: 10,
24 | right: 10,
25 | margin: "0 auto"
26 | },
27 | extendedIcon: {
28 | marginRight: theme.spacing.unit
29 | },
30 | lightTooltip: {
31 | background: theme.palette.common.white,
32 | color: theme.palette.text.primary,
33 | boxShadow: theme.shadows[1],
34 | fontSize: 16
35 | },
36 | table: {
37 | minWidth: 500,
38 | tableLayout: "auto",
39 | overflowX: "fixed"
40 | },
41 | tableWrapper: {
42 | overflow: "auto"
43 | },
44 | customTableCell: {
45 | whiteSpace: "nowrap",
46 | maxWidth: 0,
47 | overflow: "hidden",
48 | textOverflow: "ellipsis"
49 | },
50 | row: {
51 | "&:nth-of-type(odd)": {
52 | backgroundColor: theme.palette.background.default
53 | }
54 | }
55 | });
56 |
57 | /*
58 | To clip text with an ellipsis when it overflows a table cell, you will need to set the max-width CSS property on each td class for the overflow to work.
59 |
60 | For responsive layouts; use the max-width CSS property to specify the effective minimum width of the column, or just use max-width: 0; for unlimited flexibility.
61 | */
62 |
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/SearchFilter-InputField.css:
--------------------------------------------------------------------------------
1 | #fb-gg-search {
2 | overflow: hidden;
3 | max-width: 200px;
4 | white-space: nowrap;
5 | text-overflow: ellipsis;
6 | opacity: 1 !important;
7 | }
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/SearchFilter-Styles.js:
--------------------------------------------------------------------------------
1 | import { emphasize } from "@material-ui/core/styles/colorManipulator";
2 |
3 | var styles = theme => ({
4 | root: {
5 | flexGrow: 1
6 | },
7 | input: {
8 | display: "flex",
9 | borderRadius: 2,
10 | border: "1px solid #ced4da",
11 | backgroundColor: theme.palette.common.white,
12 | fontSize: 14,
13 | minHeight: 28,
14 | padding: "8px 0 8px 20px",
15 | opacity: "1 !important",
16 | justifyContent: "center"
17 | },
18 | valueContainer: {
19 | display: "flex",
20 | flexWrap: "wrap",
21 | flex: 1,
22 | alignItems: "center",
23 | overflow: "hidden",
24 | justifyContent: "center"
25 | },
26 | margin: {
27 | margin: theme.spacing.unit
28 | },
29 | chip: {
30 | margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 4}px`,
31 | borderRadius: 0,
32 | backgroundColor: "#ffffff",
33 | fontWeight: "bold"
34 | },
35 | chipFocused: {
36 | backgroundColor: emphasize(
37 | theme.palette.type === "light"
38 | ? theme.palette.grey[300]
39 | : theme.palette.grey[700],
40 | 0.08
41 | )
42 | },
43 | noOptionsMessage: {
44 | padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`
45 | },
46 | singleValue: {
47 | fontSize: 14,
48 | display: "none"
49 | },
50 | placeholder: {
51 | position: "absolute",
52 | left: 22,
53 | fontSize: 14,
54 | fontWeight: "normal",
55 | color: "rgba(9, 10, 60,0.38)"
56 | },
57 | paper: {
58 | position: "absolute",
59 | zIndex: 1,
60 | marginTop: theme.spacing.unit,
61 | left: 0,
62 | right: 0
63 | },
64 | textSearchFilter: {
65 | padding: theme.spacing.unit * 2,
66 | textAlign: "center",
67 | color: theme.palette.text.secondary,
68 | display: "flex",
69 | width: "30em",
70 | flexDirection: "row"
71 | },
72 |
73 | currentMonthData: {
74 | width: "740px",
75 | height: "55px",
76 | paddingLeft: "15px",
77 | verticalAlign: "center",
78 | textAlign: "center",
79 | color: "white",
80 | backgroundColor: "#007bff",
81 | borderRadius: "10px",
82 | fontSize: 20,
83 | cursor: "pointer"
84 | },
85 | currentMonthDataEmployee: {
86 | width: "800px",
87 | height: "55px",
88 | paddingLeft: "15px",
89 | verticalAlign: "center",
90 | textAlign: "center",
91 | color: "white",
92 | backgroundColor: "#007bff",
93 | borderRadius: "10px",
94 | fontSize: 20,
95 | cursor: "pointer"
96 | },
97 | divider: {
98 | height: theme.spacing.unit * 2
99 | },
100 | labelRoot: {
101 | fontSize: 22,
102 | fontWeight: "bold",
103 | marginBottom: 0,
104 | color: theme.palette.primary.dark,
105 | position: "absolute",
106 | left: "-64px",
107 | top: "-12px"
108 | }
109 | });
110 |
111 | export default styles;
112 |
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/combineStyles.js:
--------------------------------------------------------------------------------
1 | // Utility function to combine multiple Material-UI styles
2 |
3 | const combineStyles = (...styles) => {
4 | return function CombineStyles(theme) {
5 | const outStyles = styles.map(arg => {
6 | // Apply the "theme" object for style functions.
7 | if (typeof arg === "function") {
8 | return arg(theme);
9 | }
10 | // Objects need no change.
11 | return arg;
12 | });
13 | return outStyles.reduce((acc, val) => Object.assign(acc, val));
14 | };
15 | };
16 |
17 | export default combineStyles;
18 |
--------------------------------------------------------------------------------
/client/src/Components/commonStyles/toolbarStyles.js:
--------------------------------------------------------------------------------
1 | function arrowGenerator(color) {
2 | return {
3 | '&[x-placement*="bottom"] $arrow': {
4 | top: 0,
5 | left: 0,
6 | marginTop: "-0.95em",
7 | width: "3em",
8 | height: "1em",
9 | "&::before": {
10 | borderWidth: "0 1em 1em 1em",
11 | borderColor: `transparent transparent ${color} transparent`
12 | }
13 | },
14 | '&[x-placement*="top"] $arrow': {
15 | bottom: 0,
16 | left: 0,
17 | marginBottom: "-0.95em",
18 | width: "3em",
19 | height: "1em",
20 | "&::before": {
21 | borderWidth: "1em 1em 0 1em",
22 | borderColor: `${color} transparent transparent transparent`
23 | }
24 | },
25 | '&[x-placement*="right"] $arrow': {
26 | left: 0,
27 | marginLeft: "-0.95em",
28 | height: "3em",
29 | width: "1em",
30 | "&::before": {
31 | borderWidth: "1em 1em 1em 0",
32 | borderColor: `transparent ${color} transparent transparent`
33 | }
34 | },
35 | '&[x-placement*="left"] $arrow': {
36 | right: 0,
37 | marginRight: "-0.95em",
38 | height: "3em",
39 | width: "1em",
40 | "&::before": {
41 | borderWidth: "1em 0 1em 1em",
42 | borderColor: `transparent transparent transparent ${color}`
43 | }
44 | }
45 | };
46 | }
47 |
48 | const toolbarStyles = theme => ({
49 | root: {
50 | paddingRight: theme.spacing.unit
51 | },
52 | monthlyData: {
53 | marginRight: "12px",
54 | width: "650px",
55 | minHeight: "40px",
56 | paddingTop: "10px",
57 | verticalAlign: "center",
58 | textAlign: "center",
59 | borderRadius: "10px",
60 | cursor: "pointer"
61 | },
62 | todaysData: {
63 | marginRight: "12px",
64 | width: "200px",
65 | paddingTop: "6px",
66 | verticalAlign: "center",
67 | textAlign: "center",
68 | borderRadius: "10px",
69 | cursor: "pointer"
70 | },
71 | arrowPopper: arrowGenerator(theme.palette.common.black),
72 | arrow: {
73 | position: "absolute",
74 | fontSize: 6,
75 | width: "3em",
76 | height: "3em",
77 | "&::before": {
78 | content: '""',
79 | margin: "auto",
80 | display: "block",
81 | width: 0,
82 | height: 0,
83 | borderStyle: "solid"
84 | }
85 | },
86 | bootstrapPopper: arrowGenerator(theme.palette.common.black),
87 | bootstrapTooltip: {
88 | backgroundColor: theme.palette.common.black,
89 | paddingTop: "12px"
90 | },
91 | bootstrapPlacementLeft: {
92 | margin: "0 8px"
93 | },
94 | bootstrapPlacementRight: {
95 | margin: "0 8px"
96 | },
97 | bootstrapPlacementTop: {
98 | margin: "8px 0"
99 | },
100 | bootstrapPlacementBottom: {
101 | margin: "8px 0"
102 | },
103 | highlight:
104 | theme.palette.type === "light"
105 | ? {
106 | color: theme.palette.secondary.main,
107 | backgroundColor: "#66CCFF"
108 | }
109 | : {
110 | color: theme.palette.text.primary,
111 | backgroundColor: theme.palette.secondary.dark
112 | },
113 |
114 | spacer: {
115 | flex: "1 2 100%"
116 | },
117 | spacerCurrentMonth: {
118 | flex: "1 2 100%"
119 | },
120 |
121 | actions: {
122 | color: theme.palette.text.secondary
123 | },
124 | title: {
125 | flex: "0 0 auto"
126 | }
127 | });
128 |
129 | export default toolbarStyles;
130 |
131 | /*
132 | 1> Explanation of < flex: 1 1 100% > - With this I am making 'spacer' to stay full-width of the available space. And then adding two 'spacer' class to both side of the '{currentMonth} month's Exports' to make the currentMonth get centered within the available space
133 |
134 | A) The 100% in the above is the 'flex-basis', a sub-property of the Flexible Box Layout module. It specifies the initial size of the flexible item, before any available space is distributed according to the flex factors.
135 |
136 | B) flex - This is the shorthand for flex-grow, flex-shrink and flex-basis combined. Defaults is 0 1 auto.
137 |
138 | B) In other words, the general syntax -
139 | flex : flex-grow | flex-shrink | flex-basis
140 | (https://developer.mozilla.org/en-US/docs/Web/CSS/flex)
141 |
142 | C) The general way flex work - I need to add display:flex to the parent tag and then flex:1 to the child to enable the child to expand to 100% of parent.
143 |
144 | */
145 |
--------------------------------------------------------------------------------
/client/src/Routes.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route, BrowserRouter, Switch, Router, Link } from "react-router-dom";
3 | import EmployeeList from "./Components/Employee/EmployeeList";
4 | import DepartmentList from "./Components/Department/DepartmentList";
5 | import NotFound from "./Components/NotFound";
6 |
7 | class Routes extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | (
19 |
25 | )}
26 | />
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | export default Routes;
37 |
--------------------------------------------------------------------------------
/client/src/assets/images/LandingPage/code-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/src/assets/images/LandingPage/code-1.jpeg
--------------------------------------------------------------------------------
/client/src/assets/images/LandingPage/code-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/src/assets/images/LandingPage/code-2.jpeg
--------------------------------------------------------------------------------
/client/src/assets/images/LandingPage/code-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/src/assets/images/LandingPage/code-3.jpeg
--------------------------------------------------------------------------------
/client/src/assets/images/LandingPage/code-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/src/assets/images/LandingPage/code-4.jpeg
--------------------------------------------------------------------------------
/client/src/assets/images/PageNotFound/PageNotFound.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/src/assets/images/PageNotFound/PageNotFound.jpg
--------------------------------------------------------------------------------
/client/src/assets/images/SVG-Icons/baseline-people_outline-24px.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/images/bg_purple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/src/assets/images/bg_purple.png
--------------------------------------------------------------------------------
/client/src/assets/images/earth.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohan-paul/material-ui-table-with-node-mongodb-running-in-GCP-also-the-source-code-for-GCP-Blog/911edd8dc9fa46c5eeec320379560b719acfb4df/client/src/assets/images/logo.png
--------------------------------------------------------------------------------
/client/src/assets/images/moon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/images/overlay_stars.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/images/rocket.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/js/ie10-viewport-bug-workaround.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * IE10 viewport hack for Surface/desktop Windows 8 bug
3 | * Copyright 2014-2015 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | // See the Getting Started docs for more information:
8 | // http://getbootstrap.com/getting-started/#support-ie10-width
9 |
10 | (function () {
11 | 'use strict';
12 |
13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
14 | var msViewportStyle = document.createElement('style')
15 | msViewportStyle.appendChild(
16 | document.createTextNode(
17 | '@-ms-viewport{width:auto!important}'
18 | )
19 | )
20 | document.querySelector('head').appendChild(msViewportStyle)
21 | }
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/client/src/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 |
3 | export default createBrowserHistory();
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/client/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/client/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/config/config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const mongoose = require("mongoose");
3 |
4 | module.exports = {
5 | database: process.env.MONGO_DB,
6 |
7 | options: {
8 | reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
9 | reconnectInterval: 1000, // Reconnect every 500ms
10 | poolSize: 10 // Maintain up to 10 socket connections
11 | },
12 |
13 | // Connect connection with MongoDB Database
14 | connectDB: function() {
15 | mongoose.connect(this.database, this.options);
16 | },
17 |
18 | // Disconnect connection with MongoDB Database
19 | disconnectDB: function() {
20 | mongoose.disconnect(this.database);
21 | }
22 | };
23 | // on mongo connection open event print a console statement
24 | mongoose.connection.on("open", function() {
25 | console.log("Connected to Database (MongoDB) ");
26 | });
27 |
28 | // on mongo connection error event
29 | mongoose.connection.on("error", function() {
30 | console.log("error in Database (MongoDB) connections");
31 | });
32 |
33 | // on mongo connection disconnected event
34 | mongoose.connection.on("disconnected", () => {
35 | console.log("Mongoose default connection disconnected");
36 | });
37 |
38 | //listen for SIGINT event(generated with +C in the terminal) and close db connection on that event
39 | process.on("SIGINT", () => {
40 | mongoose.connection.close(() => {
41 | console.log("Mongoose disconnected through app termination");
42 | process.exit(0);
43 | });
44 | });
45 |
46 | /* Our application exits ( generated with 'SIGINT' event ) will also close database connection.
47 |
48 | What is the cause? - EventLoop
49 |
50 | As we know NodeJS will exit when EventLoop queue is empty and nothing left to do.
51 |
52 | But sometime your application can have more functions and will not exit automatically, in this point comes our last work to do.
53 | We need to exit from process using process.exit function.
54 |
55 | Argument 0 means exit with a “success” code.
56 | To exit with a “failure” code use 1.
57 | */
58 |
--------------------------------------------------------------------------------
/config/dev.js:
--------------------------------------------------------------------------------
1 | // prod.js - production keys here!!
2 |
--------------------------------------------------------------------------------
/config/keys.js:
--------------------------------------------------------------------------------
1 | // keys.js - figure out what set of credentials to return
2 | if (process.env.NODE_ENV === "production") {
3 | // we are in production - return the prod set of keys.
4 | module.exports = require("./prod");
5 | } else {
6 | // we are in development - return the dev keys!!!
7 | module.exports = require("./dev");
8 | }
9 |
--------------------------------------------------------------------------------
/config/prod.js:
--------------------------------------------------------------------------------
1 | // prod.js - production keys here!!
2 |
--------------------------------------------------------------------------------
/config/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | secret: "auth"
3 | };
4 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // const dotenv = require("dotenv");
2 | require("dotenv").config();
3 | const fs = require("fs");
4 |
5 | // const envConfig = dotenv.parse(fs.readFileSync(".env.override"));
6 | // for (let k in envConfig) {
7 | // process.env[k] = envConfig[k];
8 | // }
9 | const express = require("express");
10 | const app = express();
11 | const path = require("path");
12 | const morgan = require("morgan");
13 | const bodyParser = require("body-parser");
14 | const methodOverride = require("method-override");
15 | const url = require("url");
16 |
17 | const departmentRoutes = require("./routes/departmentRoutes");
18 | const employeeRoutes = require("./routes/employeeRoutes");
19 |
20 | const config = require("./config/config");
21 |
22 | const addRequestId = require("express-request-id")();
23 |
24 | config.connectDB();
25 |
26 | // Generate UUID for request and add it to X-Request-Id header. To work along with morgan logging. Adding a request id to the request object, to facilitate tying different log entries to each other. So a Request log and its associated Response log would have the same id.
27 | app.use(addRequestId);
28 | app.use(morgan()); // I am both writing to a log file while showing logs on the console.
29 | app.use(methodOverride("_method"));
30 | app.use(bodyParser.json());
31 | app.use(bodyParser.urlencoded({ extended: true }));
32 |
33 | morgan.token("id", function getId(req) {
34 | return req.id;
35 | });
36 |
37 | // Serving build's index.html file for Deploying to Google App Engine is MUST for production
38 |
39 | // From - https://facebook.github.io/create-react-app/docs/deployment
40 | app.use(express.static(path.join(__dirname, "/client/build")));
41 |
42 | // The below two routes are also Perfectly Working with GAE, but I used the 'catchAll' routes below
43 | // app.get("/employee", function(req, res) {
44 | // res.status(200).sendFile(path.join(__dirname, "/client/build", "index.html"));
45 | // });
46 |
47 | // app.get("/department", function(req, res) {
48 | // res.status(200).sendFile(path.join(__dirname, "/client/build", "index.html"));
49 | // });
50 |
51 | // The below IS ALSO WORKING when run < npm run dev > locally - but ofcourse it will not refer to the /client/build folder
52 | // app.get("/", function(req, res) {
53 | // res.sendFile(path.join(__dirname, "/client/public", "index.html"));
54 | // });
55 |
56 | // Morgan - For saving logs to a log file
57 | let accessLogStream = fs.createWriteStream(__dirname + "/access.log", {
58 | flags: "a"
59 | });
60 |
61 | // my custom log format, just adding ":id" to the pre-defined "combined" format from morgan
62 | // let loggerFormat =
63 | // ':id :remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :req[header] :response-time ms';
64 |
65 | let loggerFormat = `:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]`;
66 |
67 | app.use(morgan(loggerFormat, { stream: accessLogStream }));
68 |
69 | // Below two functions are for showing logs on the console. Define streams for Morgan. This logs to stderr for status codes greater than 400 and stdout for status codes less than 400.
70 | app.use(
71 | morgan(loggerFormat, {
72 | skip: function(req, res) {
73 | return res.statusCode < 400;
74 | },
75 | stream: process.stderr
76 | })
77 | );
78 |
79 | app.use(
80 | morgan(loggerFormat, {
81 | skip: function(req, res) {
82 | return res.statusCode >= 400;
83 | },
84 | stream: process.stdout
85 | })
86 | );
87 |
88 | app.use("/api/department", departmentRoutes);
89 | app.use("/api/employee", employeeRoutes);
90 |
91 | // Only now, AFTER the above /api/ routes, the "catchall" handler routes: for any request that doesn't match any route after "/" below and send back React's index.html file.
92 | // Note, this 'catchall" route MUST be put after the above two /api/ routes. Otherwise those api routes will never be hit
93 | app.get("/*", (req, res) => {
94 | res.sendFile(path.join(__dirname, "/client/build/index.html"));
95 | });
96 |
97 | app.use((err, req, res, next) => {
98 | res.status(422).send({ error: err._message });
99 | });
100 |
101 | app.listen(8080, () => {
102 | console.log("Express Server running on port 8080");
103 | });
104 |
105 | // Graceful shutdown, on sigint ( generated with +C in the terminal ) - kill/close database connection and exit
106 | process.on("SIGINT", () => {
107 | config.disconnectDB();
108 | process.exit(0);
109 | });
110 |
--------------------------------------------------------------------------------
/models/department.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const mongoose = require("mongoose"),
3 | Schema = mongoose.Schema;
4 |
5 | let departmentSchema = new Schema(
6 | {
7 | name: { type: String, unique: true },
8 | type: { type: String }
9 | },
10 | {
11 | // createdAt,updatedAt fields are automatically added into records
12 | timestamps: true
13 | }
14 | );
15 |
16 | // exports
17 | module.exports = mongoose.model("Department", departmentSchema);
18 |
--------------------------------------------------------------------------------
/models/employee.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const mongoose = require("mongoose"),
4 | autopopulate = require("mongoose-autopopulate"),
5 | Department = require("./department"),
6 | Schema = mongoose.Schema,
7 | mongoosePaginate = require("mongoose-paginate");
8 |
9 | let employeeSchema = new Schema(
10 | {
11 | department_objectId: {
12 | type: Schema.Types.ObjectId,
13 | ref: "Department",
14 | autopopulate: true
15 | },
16 | employee_name: { type: String },
17 | work_description: { type: String },
18 | avg_employee_productivity: { type: Number }, // in Tonnes
19 | benchmark_employee_productivity: { type: Number }, // in Tonnes
20 | date: { type: Date }
21 | },
22 | {
23 | // createdAt,updatedAt fields are automatically added into records
24 | timestamps: true
25 | }
26 | );
27 |
28 | // plugins
29 | employeeSchema.plugin(autopopulate);
30 | employeeSchema.plugin(mongoosePaginate);
31 |
32 | module.exports = mongoose.model("Employee", employeeSchema);
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "engines": {
5 | "node": "10.15.3"
6 | },
7 | "description": "",
8 | "homepage": "https://mui-table-3.appspot.com/employee",
9 | "main": "index.js",
10 | "scripts": {
11 | "start": "nodemon index.js ",
12 | "server": "nodemon index.js",
13 | "client": "npm start --prefix client",
14 | "dev": "concurrently \"npm run server\" \"npm run client\"",
15 | "heroku-postbuild": "cd client && npm install && npm run build",
16 | "fix-code": "prettier-eslint --write 'src/**/*.{js,jsx}' ",
17 | "fix-styles": "prettier-stylelint --write 'src/**/*.{css,scss}' "
18 | },
19 | "author": "",
20 | "license": "ISC",
21 | "dependencies": {
22 | "adm-zip": "^0.4.13",
23 | "async": "^2.6.1",
24 | "body-parser": "^1.18.3",
25 | "buffer-to-stream": "^1.0.0",
26 | "client-sessions": "^0.8.0",
27 | "concurrently": "^4.1.0",
28 | "cookie-session": "^2.0.0-beta.3",
29 | "dotenv": "^6.2.0",
30 | "eslint": "5.12.0",
31 | "eslint-config-airbnb": "^17.1.0",
32 | "eslint-plugin-import": "^2.16.0",
33 | "eslint-plugin-jsx-a11y": "^6.2.1",
34 | "eslint-plugin-react": "^7.12.4",
35 | "express": "^4.16.4",
36 | "express-request-id": "^1.4.1",
37 | "jquery": "^3.3.1",
38 | "lodash": "^4.17.11",
39 | "method-override": "^3.0.0",
40 | "mocha": "^5.2.0",
41 | "mongoose": "^5.4.11",
42 | "mongoose-autopopulate": "^0.9.1",
43 | "mongoose-paginate": "^5.0.3",
44 | "mongoose-validator": "^2.1.0",
45 | "morgan": "^1.9.1",
46 | "nodemon": "^1.18.10",
47 | "pm2": "^3.2.9",
48 | "prettier-eslint": "^8.8.2",
49 | "prettier-eslint-cli": "^4.7.1",
50 | "prettier-stylelint": "^0.4.2"
51 | }
52 | }
--------------------------------------------------------------------------------
/routes/createAllocationLodash.js:
--------------------------------------------------------------------------------
1 | /* Utility function (createAllocationExports) to create a top-level field (named 'department_objectId') from a field from the nested Department-object (i.e. from the referenced Department schema).
2 | When I send a data back to client (front-end), it should give me the item's Department name ('department_name' filed) as a top-level field to the schema instead of this being fetched from the nested object. (See I have "department_name": "Gold", as a top-level field that has been created in my backend routes file). The key requirement is, for the referenced Model (Department), I am passing only the ObjectId for the field 'department_objectId' with req.body but in the returned data, I will need the field 'department_name' as a separate top level field with data populated from the referenced Model 'Department'
3 |
4 | The reason I need this variable as a top-level field rather than as a nested object, is because of the sort functionality of Material-UI table. While I was able to render the table rows properly by fetching this value from the nested returned object. But the sort functionality's various util functions were becoming too uncontrollable without a top-level field value.
5 | */
6 |
7 | module.exports = {
8 | createAllocationEmployee: item => ({
9 | _id: item._id,
10 | department_objectId:
11 | item.department_objectId && item.department_objectId._id,
12 | employee_name: item.employee_name,
13 | work_description: item.work_description,
14 | avg_employee_productivity: item.avg_employee_productivity,
15 | benchmark_employee_productivity: item.benchmark_employee_productivity,
16 | date: item.date,
17 | department_name: item.department_objectId && item.department_objectId.name
18 | })
19 | };
20 |
--------------------------------------------------------------------------------
/routes/departmentRoutes.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | let express = require("express"),
4 | router = express.Router(),
5 | Department = require("../models/department");
6 |
7 | // To GET ALL Departments (without mongoose-paginate)-working
8 | router.get("/", (req, res, next) => {
9 | Department.find(
10 | {},
11 | null,
12 | {
13 | sort: { createdAt: -1 }
14 | },
15 | (err, docs) => {
16 | if (err) {
17 | return next(err);
18 | } else {
19 | res.status(200).json(docs);
20 | }
21 | }
22 | );
23 | });
24 |
25 | // To Add New Department
26 | router.post("/", (req, res, next) => {
27 | let department = new Department(req.body);
28 | department.save((error, newDocument) => {
29 | if (error) {
30 | if (error.name === "MongoError" && error.code === 11000) {
31 | // console.log("DUPLICATE IN BACK END IS ", error);
32 | res.status(400).send(error);
33 | } else {
34 | next(error);
35 | }
36 | } else {
37 | res.status(200).send(newDocument);
38 | }
39 | });
40 | });
41 |
42 | // Edit/update by department's _id - Working
43 | router.put("/:id", (req, res) => {
44 | Department.findByIdAndUpdate(
45 | req.params.id,
46 | {
47 | $set: {
48 | name: req.body.name,
49 | type: req.body.type
50 | }
51 | },
52 | { new: true },
53 | (err, updatedRecord) => {
54 | if (err) {
55 | console.error(err);
56 | res.send(err);
57 | } else {
58 | res.status(200).send(updatedRecord);
59 | }
60 | }
61 | );
62 | });
63 |
64 | // Delete by _id - Working
65 | router.route("/delete").delete((req, res, next) => {
66 | Department.remove(
67 | { _id: { $in: req.body.department_id_list_arr } },
68 | (err, result) => {
69 | if (err) {
70 | console.log(err);
71 | return next(err);
72 | } else {
73 | res.status(200).send(result);
74 | }
75 | }
76 | );
77 | });
78 |
79 | module.exports = router;
80 |
81 | /*
82 | 1> < Department.find({}, "-updatedAt -createdAt -__v", (err, records) > Means exclude updatedAt and createdAt, include other fields
83 |
84 | https://mongoosejs.com/docs/api.html#model_Model.find
85 |
86 | https://mongoosejs.com/docs/api.html#query_Query-select
87 |
88 | 2> To Test Delete route in Postman - (http://localhost:3000/api/department/delete)
89 |
90 | {
91 | "department_id_list_arr":["5c5eefa0518f005ac93cb4da", "5c5eef9b518f005ac93cb4d9"]
92 | }
93 |
94 | */
95 |
--------------------------------------------------------------------------------
/routes/employeeRoutes.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const express = require("express"),
4 | Department = require("../models/department"),
5 | router = express.Router(),
6 | moment = require("moment"),
7 | Employee = require("../models/employee");
8 |
9 | const _ = require("lodash");
10 | const flatMap = require("lodash/flatMap");
11 | const { createAllocationEmployee } = require("./createAllocationLodash");
12 |
13 | // General route to get raw database from direct call to mongo (not needed in the app)
14 | router.get("/directdatafrommongo", (req, res, next) => {
15 | Employee.find(
16 | {},
17 | null,
18 | {
19 | sort: { createdAt: -1 }
20 | },
21 | (err, docs) => {
22 | if (err) {
23 | return next(err);
24 | } else {
25 | res.status(200).json(docs);
26 | }
27 | }
28 | );
29 | });
30 |
31 | // To GET ALL Imports without mongoose-paginate - Working in Postman*
32 | router.get("/", (req, res, next) => {
33 | Employee.find(
34 | {},
35 | null,
36 | {
37 | sort: { createdAt: -1 }
38 | },
39 | (err, records) => {
40 | if (err) {
41 | return next(err);
42 | } else {
43 | const flatDocs = _.flatMap(records, item => [
44 | createAllocationEmployee(item)
45 | ]);
46 | res.status(200).json(flatDocs);
47 | }
48 | }
49 | );
50 | });
51 |
52 | // To GET ALL Imports with mongoose-paginate - max 5 or 10 or 15 at a time (determined by whatever users sets it to be at the front-end MAT-UI). By sating < lean: true > the returned data will be made lean i.e. the "department_objectId": "5c6121e4722f134f27c49ee4", will be a single line instead of a nested object with all details. Working in Postman*
53 | router.get("/paginate", (req, res, next) => {
54 | let pageOptions = {
55 | page: parseInt(req.query.page) || 0,
56 | limit: parseInt(req.query.rowsperpage) || 5
57 | };
58 |
59 | if (req.query.page && req.query.rowsperpage) {
60 | Employee.paginate(
61 | {},
62 | {
63 | offset: pageOptions.page * pageOptions.limit,
64 | limit: pageOptions.limit,
65 | sort: { createdAt: -1 }
66 | },
67 |
68 | (err, records) => {
69 | if (err) {
70 | console.log(err);
71 | next(err);
72 | } else {
73 | const flatDocs = _.flatMap(records.docs, item => [
74 | createAllocationEmployee(item)
75 | ]);
76 | res.status(200).json(flatDocs);
77 | }
78 | }
79 | );
80 | }
81 | });
82 |
83 | // Create a new item (note, I have to have an object_id reference of the relevant department) -Working in Postman*
84 | router.post("/", (req, res, next) => {
85 | let employee = new Employee(req.body);
86 | employee.save((err, newImport) => {
87 | if (err) {
88 | console.log("Failed to post new data because ", err);
89 | return next(err);
90 | } else {
91 | res.status(200).send(newImport);
92 | }
93 | });
94 | });
95 |
96 | // Edit / Update by _id - Working in Postman*
97 | router.put("/:id", (req, res, next) => {
98 | let editedItem = req.body;
99 |
100 | // I also had to reformat the date, else mongo was saving the date record as one day previous
101 | editedItem.date = moment(editedItem.date).format("YYYY-MM-DD");
102 |
103 | Employee.findByIdAndUpdate(req.params.id, editedItem, {
104 | new: true
105 | }).exec((err, record) => {
106 | if (err) {
107 | console.log(err);
108 | return next(err);
109 | } else {
110 | res.status(200).json(record);
111 | }
112 | });
113 | });
114 |
115 | // Delete by _id array - Working in Postman
116 | router.delete("/delete", (req, res, next) => {
117 | Employee.remove(
118 | { _id: { $in: req.body.employee_id_list_arr } },
119 | (err, records) => {
120 | if (err) {
121 | console.log(err);
122 | return next(err);
123 | } else {
124 | res.status(200).send(records);
125 | }
126 | }
127 | );
128 | });
129 |
130 | // get Imports by date range WITHOUT PAGINATION - Working in Postman
131 | router.post("/daterange", (req, res, next) => {
132 | let startDateByUser = moment(req.body.start_date).format("YYYY-MM-DD");
133 | let endDateByUser = moment(req.body.end_date).format("YYYY-MM-DD");
134 | // Now, Increment the user input date by one, because the query should return records for both the user-input date inclusive data
135 | let incremented_endDateByUser = moment(endDateByUser)
136 | .add(1, "days")
137 | .format("YYYY-MM-DD");
138 |
139 | Employee.find(
140 | {
141 | date: {
142 | $gte: startDateByUser,
143 | $lt: incremented_endDateByUser
144 | }
145 | },
146 | (err, records) => {
147 | if (err) {
148 | return next(err);
149 | } else {
150 | const flatDocs = _.flatMap(records, item => [
151 | createAllocationEmployee(item)
152 | ]);
153 | res.status(200).json(flatDocs);
154 | }
155 | }
156 | );
157 | });
158 |
159 | // get Employee works by date range - WORKING WITH PAGINATION - Working in Postman
160 | // The signature is - Model.paginate([query], [options], [callback])
161 | router.post("/paginate/daterange", (req, res, next) => {
162 | let startDateByUser = moment(req.body.start_date).format("YYYY-MM-DD");
163 | let endDateByUser = moment(req.body.end_date).format("YYYY-MM-DD");
164 |
165 | // Now, Increment the user input date by one, because the query should return records for both the user-input date inclusive data
166 | let incremented_endDateByUser = moment(endDateByUser)
167 | .add(1, "days")
168 | .format("YYYY-MM-DD");
169 |
170 | let pageOptions = {
171 | page: parseInt(req.query.page) || 0,
172 | limit: parseInt(req.query.rowsperpage) || 5
173 | };
174 |
175 | Employee.paginate(
176 | {
177 | date: {
178 | $gte: startDateByUser,
179 | $lt: incremented_endDateByUser
180 | }
181 | },
182 | {
183 | offset: pageOptions.page * pageOptions.limit,
184 | limit: pageOptions.limit,
185 | sort: { createdAt: -1 }
186 | },
187 |
188 | (err, records) => {
189 | if (err) {
190 | console.log(err);
191 | return next(err);
192 | } else {
193 | const flatDocs = _.flatMap(records.docs, item => [
194 | createAllocationEmployee(item)
195 | ]);
196 | res.status(200).json(flatDocs);
197 | }
198 | }
199 | );
200 | });
201 |
202 | // get employee by Today's Date WITHOUT pagination- WORKING IN POSTMAN
203 | router.get("/current/today", (req, res, next) => {
204 | let today = moment().format("YYYY-MM-DD");
205 | let tomorrow = moment(today)
206 | .add(1, "days")
207 | .format("YYYY-MM-DD");
208 | Employee.find(
209 | {
210 | date: {
211 | $gte: today,
212 | $lt: tomorrow
213 | }
214 | },
215 | (err, records) => {
216 | if (err) {
217 | return next(err);
218 | }
219 | const flatDocs = _.flatMap(records, item => [
220 | createAllocationEmployee(item)
221 | ]);
222 | res.status(200).json(flatDocs);
223 | }
224 | );
225 | });
226 |
227 | // get employee by Today's Date - Rohan's code with pagination(Working in Postman)
228 | router.post("/paginate/current/today", (req, res, next) => {
229 | let pageOptions = {
230 | page: parseInt(req.query.page) || 0,
231 | limit: parseInt(req.query.rowsperpage) || 5
232 | };
233 | let today = moment().format("YYYY-MM-DD");
234 | let tomorrow = moment(today)
235 | .add(1, "days")
236 | .format("YYYY-MM-DD");
237 |
238 | Employee.paginate(
239 | {
240 | date: {
241 | $gte: today,
242 | $lt: tomorrow
243 | }
244 | },
245 | {
246 | offset: pageOptions.page * pageOptions.limit,
247 | limit: pageOptions.limit,
248 | sort: { createdAt: -1 }
249 | },
250 | (err, records) => {
251 | if (err) {
252 | console.log(err);
253 | return next(err);
254 | }
255 | const flatDocs = _.flatMap(records.docs, item => [
256 | createAllocationEmployee(item)
257 | ]);
258 | res.status(200).json(flatDocs);
259 | }
260 | );
261 | });
262 |
263 | // get employee by current month WITHOUT mongoose-pagination - Rohan's code (Working in Postman)
264 | router.post("/currentmonth", (req, res, next) => {
265 | var startDate = moment(new Date())
266 | .startOf("month")
267 | .format("YYYY-MM-DD");
268 | var endDate = moment(new Date())
269 | .endOf("month")
270 | .add(1, "days")
271 | .format("YYYY-MM-DD");
272 |
273 | Employee.find(
274 | {
275 | date: {
276 | $gte: startDate,
277 | $lt: endDate
278 | }
279 | },
280 | (err, records) => {
281 | if (err) {
282 | return next(err);
283 | }
284 | const flatDocs = _.flatMap(records, item => [
285 | createAllocationEmployee(item)
286 | ]);
287 | res.status(200).json(flatDocs);
288 | }
289 | );
290 | });
291 |
292 | // get employee by current month WITH mongoose-pagination - Rohan's code (Working in Postman)
293 | router.post("/paginate/currentmonth", (req, res, next) => {
294 | var startDate = moment(new Date())
295 | .startOf("month")
296 | .format("YYYY-MM-DD");
297 | var endDate = moment(new Date())
298 | .endOf("month")
299 | .add(1, "days")
300 | .format("YYYY-MM-DD");
301 |
302 | let pageOptions = {
303 | page: parseInt(req.query.page) || 0,
304 | limit: parseInt(req.query.rowsperpage) || 5
305 | };
306 |
307 | Employee.paginate(
308 | {
309 | date: {
310 | $gte: startDate,
311 | $lt: endDate
312 | }
313 | },
314 | {
315 | offset: pageOptions.page * pageOptions.limit,
316 | limit: pageOptions.limit,
317 | sort: { createdAt: -1 }
318 | },
319 |
320 | (err, records) => {
321 | if (err) {
322 | return next(err);
323 | }
324 | const flatDocs = _.flatMap(records.docs, item => [
325 | createAllocationEmployee(item)
326 | ]);
327 | res.status(200).json(flatDocs);
328 | }
329 | );
330 | });
331 |
332 | module.exports = router;
333 |
334 | /* 1> To test the POST (New Item) route http://localhost:3000/api/employee in Postman - ie. for adding a new Employee, I have to take an existing object_id from the department module, meaning a real Department data should be existing already, i.e. Department should be created before creating Employee
335 |
336 | And If I dont yet have a department module take an arbitrary _id for filling up the objectId field for the 'department_objectId' prop of the Employee scheme.
337 |
338 | The below one is with a real object_id
339 |
340 | {
341 | "department_objectId":"5c6c0f969c84ea3c7194a7de",
342 | "avg_employee_productivity":"6",
343 | "benchmark_employee_productivity":"20",
344 | "date":"2019-02-26"
345 | }
346 |
347 | ==========
348 |
349 | 2> To test Edit route - http://localhost:3000/api/employee/5c75512bc014924f462b753b
350 |
351 | {
352 | "department_objectId":"5c6c0f839c84ea3c7194a7dd",
353 | "avg_employee_productivity":"6",
354 | "benchmark_employee_productivity":"25",
355 | "date":"2019-02-26"
356 | }
357 |
358 | 3> Testing DELETE - router.delete("/delete"
359 |
360 | {
361 | "employee_id_list_arr":["5c6a6ecc1c01895fcfa3fe5d"]
362 | }
363 |
364 | 4> To test router.post("/paginate/daterange", - First post a a document with date set to as below
365 |
366 | "date":"2019-02-12",
367 |
368 | Then send a POST request with Params of page=0 and rowsperpage=2
369 |
370 | {
371 | "start_date": "2019-02-12",
372 | "end_date": "2019-02-12"
373 | }
374 |
375 |
376 | */
377 |
--------------------------------------------------------------------------------