├── assets
├── css
│ ├── message.css
│ ├── greeting.css
│ ├── footer.css
│ ├── index.css
│ ├── stock-update-modal.css
│ ├── loginform.css
│ ├── paginate.css
│ ├── data-table.css
│ └── vendor
│ │ └── bootstrap-4.0.0
│ │ ├── bootstrap-reboot.min.css
│ │ └── bootstrap-reboot.css
└── js
│ ├── greeting.jsx
│ ├── footer.jsx
│ ├── message.jsx
│ ├── env.jsx
│ ├── paginate.jsx
│ ├── loginform.jsx
│ ├── data-table.jsx
│ ├── index.jsx
│ └── stock-update-modal.jsx
├── src
├── css
│ ├── message.css
│ ├── greeting.css
│ ├── app-name-title.css
│ ├── loginform.css
│ ├── footer.css
│ ├── index.css
│ ├── header.css
│ ├── paginate.css
│ ├── stock-update-modal.css
│ ├── data-table.css
│ └── vendor
│ │ └── bootstrap-4.0.0
│ │ ├── bootstrap-reboot.min.css
│ │ └── bootstrap-reboot.css
├── version.js
├── index.js
├── app-name-title.js
├── greeting.js
├── footer.js
├── data-table-caption.js
├── message.js
├── header.js
├── setupTests.js
├── data-table-nav.js
├── data-table-head.js
├── img
│ └── box.svg
├── truck-table.js
├── stock-update-delete.js
├── data-table.js
├── truck.js
├── data-table-data.js
├── paginate.js
├── loginform.js
├── api.js
├── stock-update-modal.js
└── api-request.js
├── meta
└── img
│ ├── screenshot_1.png
│ ├── screenshot_10.png
│ ├── screenshot_2.png
│ ├── screenshot_3.png
│ ├── screenshot_4.png
│ ├── screenshot_5.png
│ ├── screenshot_6.png
│ ├── screenshot_7.png
│ ├── screenshot_8.png
│ └── screenshot_9.png
├── public
├── .htaccess
├── manifest.json
└── index.html
├── .gitignore
├── webpack-stats.json
├── .env.production_DEFAULT
├── webpack.config.js
├── package.json
├── prod.webpack.config.js
└── README.md
/assets/css/message.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/css/message.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/css/greeting.css:
--------------------------------------------------------------------------------
1 | div.container {
2 | vertical-align:middle!important;
3 | }
--------------------------------------------------------------------------------
/assets/css/greeting.css:
--------------------------------------------------------------------------------
1 | div.container {
2 | vertical-align:middle!important;
3 | }
--------------------------------------------------------------------------------
/src/version.js:
--------------------------------------------------------------------------------
1 | const APP_VERSION = '[client 4.1.8.2]';
2 | export default APP_VERSION;
3 |
--------------------------------------------------------------------------------
/assets/css/footer.css:
--------------------------------------------------------------------------------
1 | div.footer {
2 | font-size: 0.75em;
3 | margin: auto;
4 | text-align: center;
5 | }
--------------------------------------------------------------------------------
/meta/img/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_1.png
--------------------------------------------------------------------------------
/meta/img/screenshot_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_10.png
--------------------------------------------------------------------------------
/meta/img/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_2.png
--------------------------------------------------------------------------------
/meta/img/screenshot_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_3.png
--------------------------------------------------------------------------------
/meta/img/screenshot_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_4.png
--------------------------------------------------------------------------------
/meta/img/screenshot_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_5.png
--------------------------------------------------------------------------------
/meta/img/screenshot_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_6.png
--------------------------------------------------------------------------------
/meta/img/screenshot_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_7.png
--------------------------------------------------------------------------------
/meta/img/screenshot_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_8.png
--------------------------------------------------------------------------------
/meta/img/screenshot_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ConsciousUniverse/simple-stock-management-frontend/HEAD/meta/img/screenshot_9.png
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 | Options -MultiViews
2 | RewriteEngine On
3 | RewriteCond %{REQUEST_FILENAME} !-f
4 | RewriteRule ^ index.html [QSA,L]
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './app.js';
4 |
5 | ReactDOM.render(
6 | , document.getElementById('root'));
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | build/
3 | assets/bundles/
4 | node_modules/
5 | tests/
6 | .eslintcache
7 | *swp
8 | package-lock.json
9 | devel/
10 | nvm/
11 | .env.production
12 | .env.dev
13 | *~
14 |
--------------------------------------------------------------------------------
/webpack-stats.json:
--------------------------------------------------------------------------------
1 | {"status":"done","chunks":{"main":[{"name":"main-50730b48ae5ab175c0ab.js","path":"/home/dan/Data/LocalRepositories/DEVELOPMENT/StockManagement/stock_control_frontend/react/assets/bundles/main-50730b48ae5ab175c0ab.js"}]}}
--------------------------------------------------------------------------------
/assets/css/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | background-color: #001e00;
3 | }
4 |
5 | div.app-main {
6 | padding: 1em;
7 | color: #fcfcfc;
8 | background-color: #001a00;
9 | margin: auto;
10 | }
11 |
12 | h1, h2, h3, h4, h5, h6 {
13 | color: yellow;
14 | }
--------------------------------------------------------------------------------
/src/css/app-name-title.css:
--------------------------------------------------------------------------------
1 | div.container {
2 | vertical-align:middle!important;
3 | }
4 |
5 | span.appTitle {
6 | display: block;
7 | transform: rotateX(360deg) scale(1);
8 | transition: all 2000ms ease-in-out;
9 | }
10 |
11 | span.appTitle:hover {
12 | }
--------------------------------------------------------------------------------
/src/app-name-title.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './css/app-name-title.css';
3 | /*
4 | Functional component to display the greeting
5 | */
6 | const AppNameTitle = () => {
7 | return (
8 |
{process.env.REACT_APP_NAME_TITLE}
9 | );
10 | };
11 | export default AppNameTitle;
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Simple Stock Management",
3 | "name": "Simple Stock Management App, by dan@uplandsdynamic.com",
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": "#000000"
15 | }
16 |
--------------------------------------------------------------------------------
/src/css/loginform.css:
--------------------------------------------------------------------------------
1 | div.login div.form-field {
2 | display: inline-block;
3 | margin: 0 1em 1em 0;
4 | }
5 |
6 | div.login div.logout-button-field {
7 | text-align: right;
8 | }
9 |
10 | div.login input.login {
11 | display: block;
12 | }
13 |
14 | span.welcome {
15 | display: inline-block;
16 | color: #001e00;
17 | margin: 0 1em 0 1em;
18 | }
19 |
20 | .logout-button-field button {
21 | margin: 0.25em;
22 | }
--------------------------------------------------------------------------------
/src/css/footer.css:
--------------------------------------------------------------------------------
1 | div.footer {
2 | padding: 1vh;
3 | margin: auto auto 1vh auto;
4 | font-size: 0.75em;
5 | text-align: center;
6 | color: silver;
7 | background-color:teal;
8 | -webkit-border-bottom-right-radius: 45px;
9 | -webkit-border-bottom-left-radius: 45px;
10 | -moz-border-radius-bottomright: 45px;
11 | -moz-border-radius-bottomleft: 45px;
12 | border-bottom-right-radius: 45px;
13 | border-bottom-left-radius: 45px;
14 | }
--------------------------------------------------------------------------------
/assets/css/stock-update-modal.css:
--------------------------------------------------------------------------------
1 | button.close-button {
2 | margin-left:auto;
3 | }
4 |
5 | .close-modal-button-cell {
6 | text-align: right;
7 | }
8 |
9 | label {
10 | margin: 0.5em;
11 | font-weight: bold;
12 | }
13 |
14 | form.transfer-form {
15 | background-color: yellow;
16 | color: #001e00;
17 | }
18 |
19 | form.transfer-form-danger {
20 | background-color: #530000;
21 | color: yellow;
22 | }
23 |
24 | button.delete-btn, button.cancel-btn {
25 | margin: 0.25em;
26 | }
--------------------------------------------------------------------------------
/.env.production_DEFAULT:
--------------------------------------------------------------------------------
1 | REACT_APP_API_ROUTE = 'https://mydomain.tld/api'
2 | REACT_APP_API_DATA_ROUTE = 'https://mydomain.tld/api/v2'
3 | REACT_APP_ORG_NAME = 'My Organisation name'
4 | REACT_APP_SHORT_ORG_NAME = 'Org Name'
5 | REACT_APP_COPYRIGHT = 'Application developed by Uplands Dynamic, apps@uplandsdynamic.com'
6 | REACT_APP_FOOTER = 'My app footer'
7 | REACT_APP_NAME_TITLE = 'Simple Stock Management'
8 | REACT_APP_ROWS_PER_TABLE = 25
9 | REACT_APP_PAGER_MAIN_SIZE = 5
10 | REACT_APP_PAGER_END_SIZE = 3
11 | REACT_APP_VERSION = '4.0.0'
12 |
--------------------------------------------------------------------------------
/assets/js/greeting.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import '../css/greeting.css'
3 | /*
4 | Functional component to display the greeting (passed in as prop from loginform.jsx, set in state
5 | in index.jsx).
6 | */
7 | const Greeting = ({greeting = ''} = {}) => {
8 | return (
9 |
10 |
11 |
12 | {greeting}
13 |
14 |
15 |
16 | );
17 | };
18 | export default Greeting;
--------------------------------------------------------------------------------
/src/greeting.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './css/greeting.css'
3 | /*
4 | Functional component to display the greeting (passed in as prop from loginform.js, set in state
5 | in index.js).
6 | */
7 | const Greeting = ({greeting = ''} = {}) => {
8 | return (
9 |
10 |
11 |
12 | {greeting}
13 |
14 |
15 |
16 | );
17 | };
18 | export default Greeting;
--------------------------------------------------------------------------------
/src/css/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | /* background-color: #001e00; */
3 | height: 100%;
4 | }
5 |
6 | body {
7 | background-color: #333;
8 | }
9 |
10 | div.app-main {
11 | margin: 1vh 1vh 0 1vh;
12 | padding: 1vh;
13 | color: #fcfcfc;
14 | background-color: #001a00;
15 | min-height: 90vh;
16 | -webkit-border-radius: 15px;
17 | -moz-border-radius: 15px;
18 | border-radius: 15px;
19 | }
20 |
21 | h1, h2, h3, h4, h5, h6 {
22 | color: yellow;
23 | }
24 |
25 | div.account_type h1 {
26 | color:#001a00;
27 | }
28 |
29 | button.table-btn {
30 | transition: all 200ms ease-in-out
31 | }
32 |
33 | button.table-btn:hover {
34 | transform: scale(1.25);
35 | z-index: 10;
36 | }
--------------------------------------------------------------------------------
/assets/css/loginform.css:
--------------------------------------------------------------------------------
1 | div.login {
2 | margin: 1em auto 1em auto;
3 | padding: 1em;
4 | background-color: yellow;
5 | -webkit-border-radius: 3px;
6 | -moz-border-radius: 3px;
7 | border-radius: 3px;
8 | font-weight: bold;
9 | vertical-align: middle!important;
10 | color: #001e00;
11 | }
12 |
13 | div.login div.form-field {
14 | display: inline-block;
15 | margin: 0 1em 1em 0;
16 | }
17 |
18 | div.login div.logout-button-field {
19 | text-align: right;
20 | }
21 |
22 | div.login input.login {
23 | display: block;
24 | }
25 |
26 | span.welcome {
27 | display: inline-block;
28 | color: #001e00;
29 | margin: 0 1em 0 1em;
30 | }
31 |
32 | .logout-button-field button {
33 | margin: 0.25em;
34 | }
--------------------------------------------------------------------------------
/src/css/header.css:
--------------------------------------------------------------------------------
1 | div.header {
2 | margin: 1em auto 1em auto;
3 | padding: 1em;
4 | background-color: yellow;
5 | -webkit-border-radius: 3px;
6 | -moz-border-radius: 3px;
7 | border-radius: 3px;
8 | font-weight: bold;
9 | vertical-align: middle !important;
10 | color: #001e00;
11 | }
12 |
13 | .logo {
14 | animation: Box-logo-scale 500ms 6 alternate-reverse ease-in-out;
15 | color: transparent; /* hides alt text while image loading */
16 | transition: all 500ms ease-in-out;
17 | }
18 |
19 | .logo:hover {
20 | transform: rotate(360deg);
21 | }
22 |
23 | @keyframes Box-logo-scale {
24 | from {
25 | transform: scale3d(0,0,0)
26 | }
27 | to {
28 | transform: scale3d(1,1,1);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/assets/js/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import '../css/footer.css'
3 | /*
4 | Functional component to display the greeting (passed in as prop from loginform.jsx, set in state
5 | in index.jsx).
6 | */
7 | const Footer = ({copyright = '', footer = '', version = ''} = {}) => {
8 | return (
9 |
10 |
11 |
12 | {footer}
13 |
14 |
15 |
16 |
17 | {copyright}
18 |
19 |
20 |
21 |
22 | {version}
23 |
24 |
25 |
26 | );
27 | };
28 | export default Footer;
--------------------------------------------------------------------------------
/src/footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './css/footer.css'
3 | /*
4 | Functional component to display the greeting (passed in as prop from loginform.js, set in state
5 | in index.js).
6 | */
7 | const Footer = ({copyright = '', footer = '', version = ''} = {}) => {
8 | return (
9 |
10 |
11 |
12 | {footer}
13 |
14 |
15 |
16 |
17 | {copyright}
18 |
19 |
20 |
21 |
22 | Version {version}
23 |
24 |
25 |
26 | );
27 | };
28 | export default Footer;
--------------------------------------------------------------------------------
/assets/css/paginate.css:
--------------------------------------------------------------------------------
1 | .page-item.active .page-link {
2 | background: yellow;
3 | color: #001e00;
4 | border: 3px solid orange;
5 | font-weight: bolder;
6 | padding: 0.5em !important;
7 | }
8 |
9 | div.linkedPage {
10 | display: inline-block;
11 | float: left;
12 | margin-right: auto;
13 | }
14 |
15 | div.linkedPage label {
16 | display: inline-block;
17 | float: left;
18 | }
19 |
20 | .linkedPage input {
21 | max-width: 7em;
22 | background-color: #001e00;
23 | color: yellow;
24 | font-weight: bolder;
25 | border: 3px solid orange;
26 | -webkit-border-radius: 3px;
27 | -moz-border-radius: 3px;
28 | border-radius: 3px;
29 | margin: 0 0.5em;
30 | }
31 |
32 | .page-item .page-link {
33 | background: #001e00;
34 | color: yellow;
35 | border: 2px solid darkorange;
36 | font-weight: bold;
37 | padding: 1em !important;
38 | }
39 |
40 | ul.pagination, li.page-item {
41 | display: inline-flex;
42 | }
43 |
44 | div.pager {
45 | text-align: right;
46 | }
47 |
48 | span.splitter {
49 | margin: 0 0.5em;
50 | }
51 |
52 | input.page-input {
53 | max-width: 3em;
54 | }
--------------------------------------------------------------------------------
/src/data-table-caption.js:
--------------------------------------------------------------------------------
1 | import './css/data-table.css';
2 | import React from 'react'
3 |
4 | const DataTableCaption = ({ stockRecord, accountMode, accountModes, _formatUTCDateTime } = {}) => {
5 | if (accountMode === accountModes.WAREHOUSE) {
6 | return (
7 | {process.env.REACT_APP_SHORT_ORG_NAME} Warehouse Stock Data
8 | {stockRecord.meta.datetime_of_request ?
9 | `[Request returned:
10 | ${_formatUTCDateTime({
11 | dateTime: stockRecord.meta.datetime_of_request
12 | })}]` : ''}
13 |
14 | )
15 | } else if (accountMode === accountModes.STORE) {
16 | return (
17 | {process.env.REACT_APP_SHORT_ORG_NAME} Account Stock Data
18 | {stockRecord.meta.datetime_of_request ?
19 | `[Request returned:
20 | ${_formatUTCDateTime({
21 | dateTime: stockRecord.meta.datetime_of_request
22 | })}]` : ''}
23 |
24 | )
25 | }
26 | }
27 | export default DataTableCaption;
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
22 | Simple Stock Management
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/css/paginate.css:
--------------------------------------------------------------------------------
1 | .page-item.active .page-link {
2 | background: yellow;
3 | color: #001e00;
4 | border: 3px solid orange;
5 | font-weight: bolder;
6 | padding: 0.5em !important;
7 | }
8 |
9 | div.linkedPage {
10 | display: inline-block;
11 | float: left;
12 | margin-right: auto;
13 | }
14 |
15 | div.linkedPage label {
16 | display: inline-block;
17 | float: left;
18 | }
19 |
20 | .linkedPage input {
21 | max-width: 7em;
22 | background-color: #001e00;
23 | color: yellow;
24 | font-weight: bolder;
25 | border: 3px solid orange;
26 | -webkit-border-radius: 3px;
27 | -moz-border-radius: 3px;
28 | border-radius: 3px;
29 | margin: 0 0.5em;
30 | }
31 |
32 | .page-item .page-link {
33 | background: #001e00;
34 | color: yellow;
35 | border: 2px solid darkorange;
36 | font-weight: bold;
37 | padding: 1em !important;
38 | z-index: 0!important;
39 | }
40 |
41 | ul.pagination, li.page-item {
42 | display: inline-flex;
43 | }
44 |
45 | div.pager {
46 | text-align: right;
47 | }
48 |
49 | span.splitter {
50 | margin: 0 0.5em;
51 | }
52 |
53 | input.page-input {
54 | max-width: 4em;
55 | background-color: darkorange;
56 | color: #001e00;
57 | border: 2px solid darkgoldenrod !important;
58 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const BundleTracker = require('webpack-bundle-tracker');
3 | const statsFileURL = './webpack-stats.json';
4 |
5 | module.exports = {
6 |
7 | context: __dirname,
8 |
9 | mode: 'development',
10 |
11 | devtool: 'inline-source-map',
12 |
13 | entry: `./assets/js/index.jsx`,
14 |
15 | output: {
16 | path: path.resolve(`./assets/bundles/`),
17 | filename: "[name]-[hash].js"
18 | },
19 |
20 | resolve: {
21 | extensions: [".jsx", ".js"]
22 | },
23 |
24 | plugins: [
25 | new BundleTracker({filename: statsFileURL}),
26 | ],
27 |
28 | module: {
29 | rules: [
30 | {
31 | test: /\.js$/,
32 | loader: 'babel-loader',
33 | exclude: /node_modules/
34 | }, {
35 | test: /\.jsx$/,
36 | loader: 'babel-loader',
37 | exclude: /node_modules/
38 | },
39 | {
40 | test: /\.(png|woff|woff2|eot|ttf|svg)$/,
41 | loader: 'url-loader?limit=100000'
42 | },
43 | {
44 | test: /\.css$/,
45 | loader: ['style-loader', 'css-loader'],
46 | //exclude: /node_modules/
47 | }
48 |
49 | ]
50 | }
51 | };
--------------------------------------------------------------------------------
/src/message.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./css/data-table.css";
3 |
4 | class Message extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | // keep real-time props changes in component state
8 | this.state = {
9 | message: ""
10 | };
11 | }
12 |
13 | componentWillUnmount() {}
14 |
15 | static getDerivedStateFromProps(nextProps) {
16 | return {
17 | message: nextProps.message,
18 | messageClass: nextProps.messageClass
19 | };
20 | }
21 |
22 | renderMessage = () => {
23 | let message = this.state.message
24 | ? this.state.message
25 | : this.state.datetimeOfRequest
26 | ? `Stock data queried ${this.state.datetimeOfRequest}`
27 | : "";
28 | let messageClass = this.state.messageClass
29 | ? this.state.messageClass
30 | : "alert alert-info";
31 | return (
32 |
41 | );
42 | };
43 |
44 | render() {
45 | return {this.state.message ? this.renderMessage() : null}
;
46 | }
47 | }
48 |
49 | export default Message;
50 |
--------------------------------------------------------------------------------
/src/css/stock-update-modal.css:
--------------------------------------------------------------------------------
1 | button.close-button {
2 | margin-left: auto;
3 | }
4 |
5 | .modal-button-cell {
6 | text-align: right;
7 | }
8 |
9 | label {
10 | margin: 0.5em;
11 | font-weight: bold;
12 | }
13 |
14 | form.transfer-form {
15 | background-color: yellow;
16 | color: #001e00;
17 | }
18 |
19 | form.transfer-form-danger {
20 | background-color: #530000;
21 | color: white;
22 | min-height: 350px;
23 | }
24 |
25 | button.delete-btn, button.cancel-btn {
26 | margin: 2pc;
27 | }
28 |
29 | button.delete-btn:hover, button.cancel-btn:hover {
30 | transform: scale(1.75);
31 | transition: all 100ms ease-in-out;
32 | }
33 |
34 | button.stockActionButton {
35 | margin-bottom: 1vmax;
36 | }
37 |
38 | button.stockActionButton:hover {
39 | transform: scale(1.25);
40 | transition: all 100ms ease-in-out;
41 | }
42 |
43 | input {
44 | border-style: none !important;
45 | }
46 |
47 | .transfer {
48 | vertical-align: top;
49 | }
50 |
51 | .xfer-button {
52 | margin: 1em 0 0 0;
53 | text-align: right;
54 | }
55 |
56 | .ReactModal__Overlay {
57 | opacity: 0;
58 | transition: opacity 100ms ease-in-out;
59 | z-index: 10; /* prevents buttons lingering after clicked */
60 | }
61 |
62 | .ReactModal__Overlay--after-open {
63 | opacity: 1;
64 | /* overflow-y: scroll !important; */
65 | }
66 |
67 | .ReactModal__Overlay--before-close {
68 | opacity: 0;
69 | }
70 |
71 | .ReactModal__Content {
72 | min-width: 35vw!important;
73 | }
--------------------------------------------------------------------------------
/assets/css/data-table.css:
--------------------------------------------------------------------------------
1 | div.data-table {
2 | min-height: 768px;
3 | }
4 |
5 | table {
6 | background-color: #001e00 !important;
7 | font-weight: bold;
8 | color: yellow;
9 | }
10 |
11 | table.table-bordered td, table.table-bordered th {
12 | border: 1px solid darkorange !important;
13 | }
14 |
15 | thead th {
16 | padding: 1em !important;
17 | }
18 |
19 | thead th:hover {
20 | color: #001e00 !important;
21 | background-color: yellow !important;
22 | }
23 |
24 | td {
25 | color: yellow !important;
26 | padding: 0.5em !important;
27 | vertical-align: middle !important;
28 | }
29 |
30 | td.action-col, th.action-col {
31 | color: white !important;
32 | }
33 |
34 | table td.table-small-font {
35 | font-size: 0.8em;
36 | }
37 |
38 | caption {
39 | font-size: 0.8em;
40 | text-align: right;
41 | color: orange;
42 | }
43 |
44 | .Modal {
45 | position: absolute;
46 | top: 40px;
47 | left: 40px;
48 | right: 40px;
49 | bottom: 40px;
50 | background-color: papayawhip;
51 | }
52 |
53 | .Overlay {
54 | position: fixed;
55 | top: 0;
56 | left: 0;
57 | right: 0;
58 | bottom: 0;
59 | background-color: rebeccapurple;
60 | }
61 |
62 | button.table-btn {
63 | margin: 0.25em;
64 | }
65 |
66 | div.message-card {
67 | margin: 0 0 1em 0;
68 | text-align: center;
69 | background-color: #001e00;
70 | border: 1px solid yellow;
71 | -webkit-border-radius: 7px;
72 | -moz-border-radius: 7px;
73 | border-radius: 7px;
74 | }
75 |
76 | .alert {
77 | font-weight: bold;
78 | font-size: 0.8em
79 | }
--------------------------------------------------------------------------------
/assets/js/message.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../css/data-table.css';
3 |
4 | class Message extends React.Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | // keep real-time props changes in component state
9 | this.state = {
10 | message: null,
11 | };
12 | }
13 |
14 | componentWillMount() {
15 | }
16 |
17 | componentWillUnmount() {
18 | }
19 |
20 | componentWillReceiveProps(nextProps) {
21 | // generate new table
22 | this.setState({
23 | message: nextProps.message,
24 | messageClass: nextProps.messageClass,
25 | });
26 | }
27 |
28 | renderMessage = () => {
29 | let message = this.state.message ? this.state.message :
30 | this.state.datetimeOfRequest ? `Stock data queried ${this.state.datetimeOfRequest}` : '';
31 | let
32 | messageClass = this.state.messageClass ? this.state.messageClass : 'alert alert-info';
33 | return (
34 |
35 |
36 |
37 |
38 | {message}
39 |
40 |
41 |
42 | )
43 | };
44 |
45 | render() {
46 | return (
47 |
48 | {this.state.message ? this.renderMessage() : null}
49 |
50 | )
51 | }
52 | }
53 |
54 | export default Message;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Dan Bright, dan@uplandsdynamic.com",
3 | "license": "GPLv3",
4 | "name": "simple-stock-management-frontend",
5 | "version": "4.1.8.2",
6 | "private": true,
7 | "description": "React frontend for Simple Stock Management application",
8 | "main": "index.js",
9 | "dependencies": {
10 | "@csstools/normalize.css": "*",
11 | "@fortawesome/fontawesome-svg-core": "*",
12 | "@fortawesome/free-brands-svg-icons": "*",
13 | "@fortawesome/free-regular-svg-icons": "*",
14 | "@fortawesome/free-solid-svg-icons": "*",
15 | "@fortawesome/react-fontawesome": "*",
16 | "acorn": "*",
17 | "axios": "*",
18 | "bootstrap": "*",
19 | "env-cmd": "*",
20 | "jquery": "*",
21 | "js-cookie": "*",
22 | "lodash.clonedeep": "*",
23 | "moment": "*",
24 | "moment-timezone": "*",
25 | "react": "*",
26 | "react-app-polyfill": "*",
27 | "react-dom": "*",
28 | "react-modal": "*",
29 | "react-router-dom": "*",
30 | "react-scripts": "*"
31 | },
32 | "scripts": {
33 | "analyze": "source-map-explorer build/static/js/main.*",
34 | "start": "HTTPS=false PORT=3001 env-cmd -f .env.dev react-scripts start",
35 | "build:dev": "env-cmd -f .env.dev react-scripts build",
36 | "build:production": "env-cmd -f .env.production react-scripts build",
37 | "test": "react-scripts test",
38 | "test:once": "CI=true react-scripts test",
39 | "test:debug": "react-scripts --inspect-brk test --runInBand",
40 | "eject": "react-scripts eject",
41 | "preinstall": "npx npm-force-resolutions"
42 | },
43 | "browserslist": [
44 | ">0.2%",
45 | "not dead",
46 | "not ie <= 11",
47 | "not op_mini all"
48 | ],
49 | "resolutions": {
50 | "glob-parent": "*",
51 | "browserslist": "*",
52 | "immer": "*",
53 | "nth-check": "*",
54 | "ansi-html": "*",
55 | "postcss": "*"
56 | }
57 | }
--------------------------------------------------------------------------------
/src/header.js:
--------------------------------------------------------------------------------
1 | import boxLogo from './img/box.svg'
2 | import './css/header.css';
3 | import AppNameTitle from './app-name-title';
4 | import LoginForm from './loginform';
5 | import React from 'react';
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7 |
8 | const Header = ({
9 | authMeta = {}, apiOptions = null, csrfToken = null, setMessage, getSessionStorage, setSessionStorage,
10 | deleteSessionStorage, setAuthentication, openTruck, accountModes, setAccountMode, accountMode
11 | } = {}) => {
12 |
13 | const storeAccount = accountMode === accountModes.WAREHOUSE
14 |
15 | const buttons = (
16 | {
18 | e.preventDefault();
19 | setAccountMode()
20 | }}>
21 |
22 | {
25 | e.preventDefault();
26 | openTruck({ state: true })
27 | }}>
28 |
29 |
);
30 | return (
31 |
32 |
33 | {authMeta.authenticated ? buttons : ''}
34 |
43 |
44 |
45 |
46 |
47 |
{storeAccount ? 'Warehouse' : "Store Account"}
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default Header;
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import {configure} from "enzyme/build";
2 | import Adapter from "enzyme-adapter-react-16";
3 | import 'jest-enzyme';
4 |
5 | const localStorageMock = {
6 | getItem: jest.fn(),
7 | setItem: jest.fn(),
8 | removeItem: jest.fn(),
9 | clear: jest.fn(),
10 | };
11 |
12 | const sessionStorageMock = {
13 | getItem: jest.fn(),
14 | setItem: jest.fn(),
15 | removeItem: jest.fn(),
16 | clear: jest.fn(),
17 | };
18 |
19 | const mockAPIOptions = {
20 | /* used to define available API options in the api-request component */
21 | GET_STOCK: {name: 'get_stock', method: 'GET', desc: 'request to get stock data'},
22 | PATCH_STOCK: {name: 'patch_stock', method: 'PATCH', desc: 'PATCH request to update stock data'},
23 | ADD_STOCK: {name: 'add_stock', method: 'POST', desc: 'POST request to add stock data'},
24 | DELETE_STOCK_LINE: {name: 'delete_stock_line', method: 'DELETE', desc: 'DELETE request to delete stock line'},
25 | POST_AUTH: {name: 'post_auth', method: 'POST', desc: 'POST request to for authorization'},
26 | PATCH_CHANGE_PW: {name: 'patch_change_pw', method: 'PATCH', desc: 'PATCH request to for changing password'},
27 | };
28 |
29 | const mockProps = {
30 | stockRecord: {
31 | meta: {
32 | page: 1,
33 | limit: 3,
34 | pagerMainSize: 5,
35 | pagerEndSize: 3,
36 | pageOrderBy: '',
37 | pageOrderDir: '',
38 | previous: null,
39 | next: null,
40 | cacheControl: 'no-cache', // no caching by default, so always returns fresh data
41 | search: '',
42 | newRecord: false,
43 | deleteRecord: false
44 | },
45 | data: {
46 | results: [],
47 | updateData: {
48 | id: '',
49 | sku: '',
50 | desc: '',
51 | units_total: 0,
52 | unit_price: 0,
53 | units_to_transfer: 0,
54 | start_units_total: 0
55 | }
56 | }
57 | },
58 | authMeta: {
59 | authenticated: false,
60 | userIsAdmin: false,
61 | },
62 | updateModalOpen: false,
63 | apiMode: mockAPIOptions.GET_STOCK, // will be one of apiOptions when triggered
64 | message: null,
65 | messageClass: '',
66 | greeting: process.env.REACT_APP_GREETING,
67 | csrfToken: null
68 | };
69 |
70 |
71 | global.localStorage = localStorageMock;
72 | global.sessionStorage = sessionStorageMock;
73 | global.mockProps = mockProps;
74 | global.mockAPIOptions = mockAPIOptions;
75 |
76 | // set enzyme adapter
77 | configure({adapter: new Adapter()});
--------------------------------------------------------------------------------
/src/css/data-table.css:
--------------------------------------------------------------------------------
1 | div.data-table {
2 | min-height: 768px;
3 | font-size: 1.7vm!important;
4 | }
5 |
6 | .table-control-btns .btn {
7 | margin-right: 0.1pc;
8 | }
9 |
10 | table {
11 | background-color: #001e00 !important;
12 | font-weight: bold;
13 | color: yellow;
14 | }
15 |
16 | table.table-bordered td, table.table-bordered th {
17 | border: 1px solid darkorange !important;
18 | font-size: 0.8pc;
19 | }
20 |
21 | thead th {
22 | padding: 1em !important;
23 | }
24 |
25 | td {
26 | color: yellow !important;
27 | padding: 0.5em !important;
28 | vertical-align: middle !important;
29 | }
30 |
31 | td.action-col, th.action-col {
32 | color: white !important;
33 | }
34 |
35 | tr {
36 | border: none;
37 | }
38 |
39 | tr.outOfStock td.unitsTotal {
40 | color: orangered !important;
41 | font-size: 0.7em;
42 | }
43 |
44 | tr.outOfStock td.sku {
45 | color: orangered !important;
46 | }
47 |
48 | table td.table-small-font {
49 | font-size: 0.8em;
50 | }
51 |
52 | table.stockTable thead th:hover {
53 | color: #001e00 !important;
54 | background-color: yellow !important;
55 | }
56 |
57 | table.stockTable-disabled, table.stockTable-disabled input {
58 | font-size: 0.8em;
59 | }
60 |
61 | table.stockTable-disabled input {
62 | background-color: lightslategrey !important;
63 | border: none;
64 | }
65 |
66 | table.transferTable td, table.transferTable {
67 | background-color: slateblue;
68 | font-size: 1.1em;
69 | }
70 |
71 | table.transferTable input {
72 | font-size: 1.1em;
73 | }
74 |
75 | caption {
76 | font-size: 0.8em;
77 | text-align: right;
78 | color: orange;
79 | }
80 |
81 | input.search {
82 | background-color: darkorange;
83 | color: #001e00 !important;
84 | font-weight: bold;
85 | }
86 |
87 | input.search::placeholder {
88 | color: #001e00 !important;
89 | font-weight: bold;
90 | }
91 |
92 | input.search {
93 | border: 2px solid darkgoldenrod !important;
94 | }
95 |
96 | .Modal {
97 | position: absolute;
98 | top: 40px;
99 | left: 40px;
100 | right: 40px;
101 | bottom: 40px;
102 | background-color: papayawhip;
103 | }
104 |
105 | .Overlay {
106 | position: fixed;
107 | top: 0;
108 | left: 0;
109 | right: 0;
110 | bottom: 0;
111 | background-color: rebeccapurple;
112 | }
113 |
114 | div.message-card {
115 | margin: 0 0 1em 0;
116 | text-align: center;
117 | background-color: #001e00;
118 | border: 1px solid yellow;
119 | -webkit-border-radius: 7px;
120 | -moz-border-radius: 7px;
121 | border-radius: 7px;
122 | }
123 |
124 | .alert {
125 | font-weight: bold;
126 | font-size: 0.8em
127 | }
--------------------------------------------------------------------------------
/prod.webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const BundleTracker = require('webpack-bundle-tracker');
3 | const statsFileURL = './webpack-stats.json';
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
6 | const webpack = require('webpack');
7 |
8 | module.exports = {
9 |
10 | context: __dirname,
11 |
12 | mode: 'production',
13 |
14 | devtool: "source-map",
15 |
16 | entry: {
17 | main: './assets/js/index.jsx',
18 | data: './assets/js/data-table.jsx',
19 | api: './assets/js/api-request.jsx',
20 | modal: './assets/js/stock-update-modal.jsx'
21 | },
22 |
23 | output: {
24 | path: path.resolve(__dirname, 'assets/bundles/'),
25 | filename: '[name]-[hash].js',
26 | chunkFilename: '[name]-[hash].js'
27 | },
28 |
29 | resolve: {
30 | extensions: [".jsx", ".js"]
31 | },
32 |
33 | optimization: {
34 | namedModules: true, // NamedModulesPlugin()
35 | splitChunks: { // CommonsChunkPlugin()
36 | name: 'vendor',
37 | minChunks: 2
38 | },
39 | noEmitOnErrors: true, // NoEmitOnErrorsPlugin
40 | concatenateModules: true //ModuleConcatenationPlugin
41 | },
42 |
43 | plugins: [
44 | new BundleTracker({filename: statsFileURL}),
45 | new MiniCssExtractPlugin({
46 | // Options similar to the same options in webpackOptions.output
47 | // both options are optional
48 | filename: '[name]-[hash].css',
49 | chunkFilename: '[id].css'
50 | }),
51 | new BundleAnalyzerPlugin({'analyzerMode': 'disabled'}), // server|static|disabled
52 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
53 | ],
54 |
55 | module: {
56 | rules: [
57 | {
58 | test: /\.js$/,
59 | exclude: /node_modules/,
60 | use: {
61 | loader: 'babel-loader',
62 | options: {}
63 | }
64 | }, {
65 | test: /\.jsx$/,
66 | exclude: /node_modules/,
67 | use: {
68 | loader: 'babel-loader',
69 | options: {}
70 | }
71 | },
72 | {
73 | test: /\.(png|woff|woff2|eot|ttf|svg)$/,
74 | use: {
75 | loader: 'url-loader?limit=100000'
76 | }
77 | },
78 | {
79 | test: /\.css$/,
80 | use:
81 | [
82 | {
83 | loader: MiniCssExtractPlugin.loader,
84 | options: {}
85 | },
86 | 'css-loader'
87 | ]
88 | }
89 |
90 | ]
91 | }
92 | };
--------------------------------------------------------------------------------
/src/data-table-nav.js:
--------------------------------------------------------------------------------
1 | import './css/data-table.css';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import Paginate from './paginate.js';
4 | import React from 'react'
5 |
6 | const DataTableNav = ({
7 | stockRecord = null, handleGetRecords, handleAddRecord, handleSearch, handleStockTake, authMeta = null,
8 | accountMode, accountModes, getStyles
9 | } = {}) => {
10 | const { userIsAdmin, authenticated } = authMeta;
11 | const addRecordButton = (
12 | handleAddRecord({ stockRecord })}
13 | className={`btn btn-md btn-warning mr-1 `}>
14 | );
15 |
16 | const stockTakeButton = (
17 | handleStockTake()} className={`btn btn-md btn-warning mr-1 `}>
18 |
19 |
20 | )
21 | if (stockRecord && authenticated) {
22 | return (
23 |
24 |
25 |
39 |
40 |
41 | {
42 | Object.assign(stockRecord.meta, { page: 1 });
43 | handleGetRecords({ stockRecord })
44 | }} className={'btn btn-md btn-warning mr-1 '}>
45 |
46 | {userIsAdmin || accountMode === accountModes.STORE ? addRecordButton : ''}
47 | {accountMode === accountModes.STORE ? stockTakeButton : ''}
48 |
49 |
50 |
53 | handleSearch({ stockRecord, term: e.target.value })
54 | } />
55 |
56 |
57 |
58 |
59 | )
60 | }
61 | return null;
62 | };
63 |
64 | export default DataTableNav;
--------------------------------------------------------------------------------
/assets/js/env.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const _DEPLOYMENT_OPTIONS = {
4 | LOCAL: 'Local development server',
5 | STAGING: 'Staging server',
6 | PROD: 'Production server'
7 | };
8 | const _DEPLOYMENT = _DEPLOYMENT_OPTIONS.LOCAL; // LOCAL, STAGING or PROD
9 | let Env = null;
10 | switch (_DEPLOYMENT) {
11 | case _DEPLOYMENT_OPTIONS.LOCAL:
12 | Env = () => {
13 | return {
14 | /* LOCAL DEV */
15 | apiRoot: 'http://localhost:3000/api',
16 | apiDataRoot: 'http://localhost:3000/api/v1',
17 | appDetails: {
18 | organisationName: 'Aninstance Consultancy',
19 | shortOrgName: 'Aninstance Consultancy',
20 | copyright:
21 | (),
24 | footerText: `This is a demo instance of the Simple Stock Management application.`,
25 | greeting: (Simple Stock Management ),
26 | version: (Version: 1.1.0
)
27 | }
28 | };
29 | };
30 | break;
31 | case _DEPLOYMENT_OPTIONS.STAGING:
32 | Env = () => {
33 | return {
34 | // /* STAGING */
35 | apiRoot: 'https://stockmanagement.staging.aninstance.com/api',
36 | apiDataRoot: 'https://stockmanagement.staging.aninstance.com/api/v1',
37 | appDetails: {
38 | organisationName: 'Aninstance Consultancy',
39 | shortOrgName: 'Aninstance Consultancy',
40 | copyright:
41 | (),
44 | footerText: `This is a demo instance of the Simple Stock Management application.`,
45 | greeting: (Simple Stock Management ),
46 | version: (Version: 1.1.0
)
47 | }
48 | };
49 | };
50 | break;
51 | case _DEPLOYMENT_OPTIONS.PROD:
52 | Env = () => {
53 | return {
54 | /* PRODUCTION */
55 | apiRoot: 'https://stockmanagement.brentlodge.org/api',
56 | apiDataRoot: 'https://stockmanagement.brentlodge.org/api/v1',
57 | appDetails: {
58 | organisationName: 'Brent Lodge Bird & Wildlife Trust',
59 | shortOrgName: 'Brent Lodge',
60 | copyright:
61 | (),
64 | footerText: `Brent Lodge Bird & Wildlife Trust. Registered Charity 276179.`,
65 | greeting: (Simple Stock Management ),
66 | version: (Version: 1.1.0
)
67 | }
68 | };
69 | };
70 | break;
71 | }
72 |
73 | export default Env;
--------------------------------------------------------------------------------
/src/data-table-head.js:
--------------------------------------------------------------------------------
1 | import './css/data-table.css';
2 | import React from 'react'
3 |
4 | const DataTableHead = ({ handleColumnOrderChange, stockRecord, accountMode, accountModes } = {}) => {
5 | if (accountMode === accountModes.STORE) // if store account
6 | return (
7 |
8 | handleColumnOrderChange({ stockRecord, newOrder: 'sku' })}>
10 | SKU
11 |
12 | handleColumnOrderChange({ stockRecord, newOrder: 'desc' })}>
14 | Description
15 |
16 | handleColumnOrderChange({ stockRecord, newOrder: 'units_total' })}>
18 | Units In Stock
19 |
20 | handleColumnOrderChange({ stockRecord, newOrder: 'shrinkage' })}>
22 | Shrinkage Since Stock Take
23 |
24 | handleColumnOrderChange({ stockRecord, newOrder: 'sold_units' })}>
26 | Sold Since Stock Take
27 |
28 | handleColumnOrderChange({ stockRecord, newOrder: 'xfer_price' })}>
30 | Xfer Price
31 |
32 | handleColumnOrderChange({ stockRecord, newOrder: 'selling_price' })}>
34 | Current Selling Price
35 |
36 | handleColumnOrderChange({
37 | stockRecord,
38 | newOrder: 'record_updated'
39 | })}>
40 | Record Updated
41 |
42 |
43 | Action
44 |
45 |
46 | )
47 | else { // warehouse
48 | return (
49 |
50 | handleColumnOrderChange({ stockRecord, newOrder: 'sku' })}>
52 | SKU
53 |
54 | handleColumnOrderChange({ stockRecord, newOrder: 'desc' })}>
56 | Description
57 |
58 | handleColumnOrderChange({ stockRecord, newOrder: 'units_total' })}>
60 | Units
61 |
62 | handleColumnOrderChange({ stockRecord, newOrder: 'unit_price' })}>
64 | Unit Price
65 |
66 | handleColumnOrderChange({
67 | stockRecord,
68 | newOrder: 'record_updated'
69 | })}>
70 | Record Updated
71 |
72 |
73 | Action
74 |
75 |
76 | )
77 | }
78 | }
79 |
80 | export default DataTableHead;
--------------------------------------------------------------------------------
/src/img/box.svg:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
19 |
20 |
21 |
30 |
31 |
32 |
33 |
42 |
43 |
44 |
45 |
46 |
52 |
58 |
64 |
70 |
76 |
82 |
91 |
97 |
105 |
111 |
--------------------------------------------------------------------------------
/src/truck-table.js:
--------------------------------------------------------------------------------
1 | import './css/data-table.css';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import React from 'react';
4 |
5 | const TruckTable = ({ truck, changeUnits }) => {
6 | if (truck.length > 0) {
7 | const tableData = truck.map((consignment, index) => {
8 | const cargo = consignment.cargo;
9 | const editButtonClasses = ['btn', 'btn-warning'];
10 | return (
11 |
12 |
13 | {cargo.sku}
14 |
15 |
16 | {cargo.desc}
17 |
18 |
19 | {cargo.units_to_transfer}
20 |
21 |
22 |
23 | {
24 | e.preventDefault();
25 | changeUnits({ consignmentListIndex: index, func: 'add', cargo });
26 | }} className={editButtonClasses.join(' ')}>
27 |
28 | {
29 | e.preventDefault();
30 | changeUnits({ consignmentListIndex: index, func: 'minus', cargo });
31 | }} className={editButtonClasses.join(' ')}>
32 |
33 | {
34 | e.preventDefault();
35 | changeUnits({ consignmentListIndex: index, func: 'clear', cargo });
36 | }} className={editButtonClasses.join(' ')}>
37 |
38 |
39 |
40 |
41 | );
42 | });
43 | return (
44 |
45 |
46 |
47 |
48 |
50 | Truck
51 |
52 |
53 | SKU
54 | Description
55 | Units to Transfer
56 | Action
57 |
58 |
59 |
60 | {tableData}
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 | return (
70 |
71 |
78 |
79 | )
80 | };
81 | export default TruckTable;
--------------------------------------------------------------------------------
/assets/css/vendor/bootstrap-4.0.0/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
3 | * Copyright 2011-2018 The Bootstrap Authors
4 | * Copyright 2011-2018 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/src/css/vendor/bootstrap-4.0.0/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
3 | * Copyright 2011-2018 The Bootstrap Authors
4 | * Copyright 2011-2018 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/src/stock-update-delete.js:
--------------------------------------------------------------------------------
1 | import './css/data-table.css';
2 | import React from 'react'
3 |
4 | const StockUpdateDelete = ({ stockRecord = null, authMeta = null, handleCloseModal, handleRecordUpdate, deleteRecord = false, accountModes, accountMode } = {}) => {
5 | const { desc, sku, unit_price, units_total, selling_price } = stockRecord.data.updateData;
6 | const userIsAdmin = authMeta.userIsAdmin;
7 | const storeAccount = accountMode === accountModes.STORE;
8 | if (deleteRecord && !storeAccount) {
9 | return (
10 |
11 |
45 |
46 | )
47 | } else if (deleteRecord && storeAccount) {
48 | return (
49 |
50 |
84 |
85 | )
86 | }
87 | return null;
88 | };
89 | export default StockUpdateDelete;
--------------------------------------------------------------------------------
/assets/css/vendor/bootstrap-4.0.0/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
3 | * Copyright 2011-2018 The Bootstrap Authors
4 | * Copyright 2011-2018 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */
8 | *,
9 | *::before,
10 | *::after {
11 | box-sizing: border-box;
12 | }
13 |
14 | html {
15 | font-family: sans-serif;
16 | line-height: 1.15;
17 | -webkit-text-size-adjust: 100%;
18 | -ms-text-size-adjust: 100%;
19 | -ms-overflow-style: scrollbar;
20 | -webkit-tap-highlight-color: transparent;
21 | }
22 |
23 | @-ms-viewport {
24 | width: device-width;
25 | }
26 |
27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
28 | display: block;
29 | }
30 |
31 | body {
32 | margin: 0;
33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
34 | font-size: 1rem;
35 | font-weight: 400;
36 | line-height: 1.5;
37 | color: #212529;
38 | text-align: left;
39 | background-color: #fff;
40 | }
41 |
42 | [tabindex="-1"]:focus {
43 | outline: 0 !important;
44 | }
45 |
46 | hr {
47 | box-sizing: content-box;
48 | height: 0;
49 | overflow: visible;
50 | }
51 |
52 | h1, h2, h3, h4, h5, h6 {
53 | margin-top: 0;
54 | margin-bottom: 0.5rem;
55 | }
56 |
57 | p {
58 | margin-top: 0;
59 | margin-bottom: 1rem;
60 | }
61 |
62 | abbr[title],
63 | abbr[data-original-title] {
64 | text-decoration: underline;
65 | -webkit-text-decoration: underline dotted;
66 | text-decoration: underline dotted;
67 | cursor: help;
68 | border-bottom: 0;
69 | }
70 |
71 | address {
72 | margin-bottom: 1rem;
73 | font-style: normal;
74 | line-height: inherit;
75 | }
76 |
77 | ol,
78 | ul,
79 | dl {
80 | margin-top: 0;
81 | margin-bottom: 1rem;
82 | }
83 |
84 | ol ol,
85 | ul ul,
86 | ol ul,
87 | ul ol {
88 | margin-bottom: 0;
89 | }
90 |
91 | dt {
92 | font-weight: 700;
93 | }
94 |
95 | dd {
96 | margin-bottom: .5rem;
97 | margin-left: 0;
98 | }
99 |
100 | blockquote {
101 | margin: 0 0 1rem;
102 | }
103 |
104 | dfn {
105 | font-style: italic;
106 | }
107 |
108 | b,
109 | strong {
110 | font-weight: bolder;
111 | }
112 |
113 | small {
114 | font-size: 80%;
115 | }
116 |
117 | sub,
118 | sup {
119 | position: relative;
120 | font-size: 75%;
121 | line-height: 0;
122 | vertical-align: baseline;
123 | }
124 |
125 | sub {
126 | bottom: -.25em;
127 | }
128 |
129 | sup {
130 | top: -.5em;
131 | }
132 |
133 | a {
134 | color: #007bff;
135 | text-decoration: none;
136 | background-color: transparent;
137 | -webkit-text-decoration-skip: objects;
138 | }
139 |
140 | a:hover {
141 | color: #0056b3;
142 | text-decoration: underline;
143 | }
144 |
145 | a:not([href]):not([tabindex]) {
146 | color: inherit;
147 | text-decoration: none;
148 | }
149 |
150 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
151 | color: inherit;
152 | text-decoration: none;
153 | }
154 |
155 | a:not([href]):not([tabindex]):focus {
156 | outline: 0;
157 | }
158 |
159 | pre,
160 | code,
161 | kbd,
162 | samp {
163 | font-family: monospace, monospace;
164 | font-size: 1em;
165 | }
166 |
167 | pre {
168 | margin-top: 0;
169 | margin-bottom: 1rem;
170 | overflow: auto;
171 | -ms-overflow-style: scrollbar;
172 | }
173 |
174 | figure {
175 | margin: 0 0 1rem;
176 | }
177 |
178 | img {
179 | vertical-align: middle;
180 | border-style: none;
181 | }
182 |
183 | svg:not(:root) {
184 | overflow: hidden;
185 | }
186 |
187 | table {
188 | border-collapse: collapse;
189 | }
190 |
191 | caption {
192 | padding-top: 0.75rem;
193 | padding-bottom: 0.75rem;
194 | color: #6c757d;
195 | text-align: left;
196 | caption-side: bottom;
197 | }
198 |
199 | th {
200 | text-align: inherit;
201 | }
202 |
203 | label {
204 | display: inline-block;
205 | margin-bottom: .5rem;
206 | }
207 |
208 | button {
209 | border-radius: 0;
210 | }
211 |
212 | button:focus {
213 | outline: 1px dotted;
214 | outline: 5px auto -webkit-focus-ring-color;
215 | }
216 |
217 | input,
218 | button,
219 | select,
220 | optgroup,
221 | textarea {
222 | margin: 0;
223 | font-family: inherit;
224 | font-size: inherit;
225 | line-height: inherit;
226 | }
227 |
228 | button,
229 | input {
230 | overflow: visible;
231 | }
232 |
233 | button,
234 | select {
235 | text-transform: none;
236 | }
237 |
238 | button,
239 | html [type="button"],
240 | [type="reset"],
241 | [type="submit"] {
242 | -webkit-appearance: button;
243 | }
244 |
245 | button::-moz-focus-inner,
246 | [type="button"]::-moz-focus-inner,
247 | [type="reset"]::-moz-focus-inner,
248 | [type="submit"]::-moz-focus-inner {
249 | padding: 0;
250 | border-style: none;
251 | }
252 |
253 | input[type="radio"],
254 | input[type="checkbox"] {
255 | box-sizing: border-box;
256 | padding: 0;
257 | }
258 |
259 | input[type="date"],
260 | input[type="time"],
261 | input[type="datetime-local"],
262 | input[type="month"] {
263 | -webkit-appearance: listbox;
264 | }
265 |
266 | textarea {
267 | overflow: auto;
268 | resize: vertical;
269 | }
270 |
271 | fieldset {
272 | min-width: 0;
273 | padding: 0;
274 | margin: 0;
275 | border: 0;
276 | }
277 |
278 | legend {
279 | display: block;
280 | width: 100%;
281 | max-width: 100%;
282 | padding: 0;
283 | margin-bottom: .5rem;
284 | font-size: 1.5rem;
285 | line-height: inherit;
286 | color: inherit;
287 | white-space: normal;
288 | }
289 |
290 | progress {
291 | vertical-align: baseline;
292 | }
293 |
294 | [type="number"]::-webkit-inner-spin-button,
295 | [type="number"]::-webkit-outer-spin-button {
296 | height: auto;
297 | }
298 |
299 | [type="search"] {
300 | outline-offset: -2px;
301 | -webkit-appearance: none;
302 | }
303 |
304 | [type="search"]::-webkit-search-cancel-button,
305 | [type="search"]::-webkit-search-decoration {
306 | -webkit-appearance: none;
307 | }
308 |
309 | ::-webkit-file-upload-button {
310 | font: inherit;
311 | -webkit-appearance: button;
312 | }
313 |
314 | output {
315 | display: inline-block;
316 | }
317 |
318 | summary {
319 | display: list-item;
320 | cursor: pointer;
321 | }
322 |
323 | template {
324 | display: none;
325 | }
326 |
327 | [hidden] {
328 | display: none !important;
329 | }
330 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/src/css/vendor/bootstrap-4.0.0/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
3 | * Copyright 2011-2018 The Bootstrap Authors
4 | * Copyright 2011-2018 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */
8 | *,
9 | *::before,
10 | *::after {
11 | box-sizing: border-box;
12 | }
13 |
14 | html {
15 | font-family: sans-serif;
16 | line-height: 1.15;
17 | -webkit-text-size-adjust: 100%;
18 | -ms-text-size-adjust: 100%;
19 | -ms-overflow-style: scrollbar;
20 | -webkit-tap-highlight-color: transparent;
21 | }
22 |
23 | @-ms-viewport {
24 | width: device-width;
25 | }
26 |
27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
28 | display: block;
29 | }
30 |
31 | body {
32 | margin: 0;
33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
34 | font-size: 1rem;
35 | font-weight: 400;
36 | line-height: 1.5;
37 | color: #212529;
38 | text-align: left;
39 | background-color: #fff;
40 | }
41 |
42 | [tabindex="-1"]:focus {
43 | outline: 0 !important;
44 | }
45 |
46 | hr {
47 | box-sizing: content-box;
48 | height: 0;
49 | overflow: visible;
50 | }
51 |
52 | h1, h2, h3, h4, h5, h6 {
53 | margin-top: 0;
54 | margin-bottom: 0.5rem;
55 | }
56 |
57 | p {
58 | margin-top: 0;
59 | margin-bottom: 1rem;
60 | }
61 |
62 | abbr[title],
63 | abbr[data-original-title] {
64 | text-decoration: underline;
65 | -webkit-text-decoration: underline dotted;
66 | text-decoration: underline dotted;
67 | cursor: help;
68 | border-bottom: 0;
69 | }
70 |
71 | address {
72 | margin-bottom: 1rem;
73 | font-style: normal;
74 | line-height: inherit;
75 | }
76 |
77 | ol,
78 | ul,
79 | dl {
80 | margin-top: 0;
81 | margin-bottom: 1rem;
82 | }
83 |
84 | ol ol,
85 | ul ul,
86 | ol ul,
87 | ul ol {
88 | margin-bottom: 0;
89 | }
90 |
91 | dt {
92 | font-weight: 700;
93 | }
94 |
95 | dd {
96 | margin-bottom: .5rem;
97 | margin-left: 0;
98 | }
99 |
100 | blockquote {
101 | margin: 0 0 1rem;
102 | }
103 |
104 | dfn {
105 | font-style: italic;
106 | }
107 |
108 | b,
109 | strong {
110 | font-weight: bolder;
111 | }
112 |
113 | small {
114 | font-size: 80%;
115 | }
116 |
117 | sub,
118 | sup {
119 | position: relative;
120 | font-size: 75%;
121 | line-height: 0;
122 | vertical-align: baseline;
123 | }
124 |
125 | sub {
126 | bottom: -.25em;
127 | }
128 |
129 | sup {
130 | top: -.5em;
131 | }
132 |
133 | a {
134 | color: #007bff;
135 | text-decoration: none;
136 | background-color: transparent;
137 | -webkit-text-decoration-skip: objects;
138 | }
139 |
140 | a:hover {
141 | color: #0056b3;
142 | text-decoration: underline;
143 | }
144 |
145 | a:not([href]):not([tabindex]) {
146 | color: inherit;
147 | text-decoration: none;
148 | }
149 |
150 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
151 | color: inherit;
152 | text-decoration: none;
153 | }
154 |
155 | a:not([href]):not([tabindex]):focus {
156 | outline: 0;
157 | }
158 |
159 | pre,
160 | code,
161 | kbd,
162 | samp {
163 | font-family: monospace, monospace;
164 | font-size: 1em;
165 | }
166 |
167 | pre {
168 | margin-top: 0;
169 | margin-bottom: 1rem;
170 | overflow: auto;
171 | -ms-overflow-style: scrollbar;
172 | }
173 |
174 | figure {
175 | margin: 0 0 1rem;
176 | }
177 |
178 | img {
179 | vertical-align: middle;
180 | border-style: none;
181 | }
182 |
183 | svg:not(:root) {
184 | overflow: hidden;
185 | }
186 |
187 | table {
188 | border-collapse: collapse;
189 | }
190 |
191 | caption {
192 | padding-top: 0.75rem;
193 | padding-bottom: 0.75rem;
194 | color: #6c757d;
195 | text-align: left;
196 | caption-side: bottom;
197 | }
198 |
199 | th {
200 | text-align: inherit;
201 | }
202 |
203 | label {
204 | display: inline-block;
205 | margin-bottom: .5rem;
206 | }
207 |
208 | button {
209 | border-radius: 0;
210 | }
211 |
212 | button:focus {
213 | outline: 1px dotted;
214 | outline: 5px auto -webkit-focus-ring-color;
215 | }
216 |
217 | input,
218 | button,
219 | select,
220 | optgroup,
221 | textarea {
222 | margin: 0;
223 | font-family: inherit;
224 | font-size: inherit;
225 | line-height: inherit;
226 | }
227 |
228 | button,
229 | input {
230 | overflow: visible;
231 | }
232 |
233 | button,
234 | select {
235 | text-transform: none;
236 | }
237 |
238 | button,
239 | html [type="button"],
240 | [type="reset"],
241 | [type="submit"] {
242 | -webkit-appearance: button;
243 | }
244 |
245 | button::-moz-focus-inner,
246 | [type="button"]::-moz-focus-inner,
247 | [type="reset"]::-moz-focus-inner,
248 | [type="submit"]::-moz-focus-inner {
249 | padding: 0;
250 | border-style: none;
251 | }
252 |
253 | input[type="radio"],
254 | input[type="checkbox"] {
255 | box-sizing: border-box;
256 | padding: 0;
257 | }
258 |
259 | input[type="date"],
260 | input[type="time"],
261 | input[type="datetime-local"],
262 | input[type="month"] {
263 | -webkit-appearance: listbox;
264 | }
265 |
266 | textarea {
267 | overflow: auto;
268 | resize: vertical;
269 | }
270 |
271 | fieldset {
272 | min-width: 0;
273 | padding: 0;
274 | margin: 0;
275 | border: 0;
276 | }
277 |
278 | legend {
279 | display: block;
280 | width: 100%;
281 | max-width: 100%;
282 | padding: 0;
283 | margin-bottom: .5rem;
284 | font-size: 1.5rem;
285 | line-height: inherit;
286 | color: inherit;
287 | white-space: normal;
288 | }
289 |
290 | progress {
291 | vertical-align: baseline;
292 | }
293 |
294 | [type="number"]::-webkit-inner-spin-button,
295 | [type="number"]::-webkit-outer-spin-button {
296 | height: auto;
297 | }
298 |
299 | [type="search"] {
300 | outline-offset: -2px;
301 | -webkit-appearance: none;
302 | }
303 |
304 | [type="search"]::-webkit-search-cancel-button,
305 | [type="search"]::-webkit-search-decoration {
306 | -webkit-appearance: none;
307 | }
308 |
309 | ::-webkit-file-upload-button {
310 | font: inherit;
311 | -webkit-appearance: button;
312 | }
313 |
314 | output {
315 | display: inline-block;
316 | }
317 |
318 | summary {
319 | display: list-item;
320 | cursor: pointer;
321 | }
322 |
323 | template {
324 | display: none;
325 | }
326 |
327 | [hidden] {
328 | display: none !important;
329 | }
330 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/src/data-table.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import './css/data-table.css';
3 | import 'bootstrap/dist/css/bootstrap.css';
4 | import 'bootstrap/dist/js/bootstrap.js';
5 | import moment from 'moment';
6 | import 'moment/locale/en-gb.js';
7 | import 'moment-timezone';
8 | import DataTableNav from "./data-table-nav";
9 | import DataTableData from "./data-table-data";
10 | import DataTableHead from "./data-table-head";
11 | import DataTableCaption from './data-table-caption';
12 |
13 | const DataTable = ({ stockRecord = {}, setMessage, openStockUpdateModalHandler,
14 | getRecordsHandler, stockTakeHandler, authMeta = {}, setStockRecordState, accountModes,
15 | accountMode, getStyles
16 | } = {}) => {
17 |
18 | const [confirmAction, setConfirmAction] = useState(false);
19 | const { authenticated } = authMeta;
20 |
21 | const _formatUTCDateTime = ({ dateTime = null } = {}) => {
22 | // takes datetime in UTC, formats and returns datetime in user's browser reported timezone
23 | return dateTime ? `${moment.utc(dateTime).local()
24 | .format('DD/MM/YYYY HH:mm:ss')} ${moment.tz(moment.tz.guess()).zoneAbbr()}`
25 | : null;
26 | };
27 |
28 | const _handleColumnOrderChange = ({ stockRecord = {}, newOrder = {} } = {}) => {
29 | let { pageOrderDir, pageOrderBy } = stockRecord.meta;
30 | // set page order direction
31 | pageOrderDir = (!pageOrderBy || pageOrderDir === '-') ? '' : '-'; // *see note 1
32 | Object.assign(stockRecord.meta, { pageOrderBy: newOrder, pageOrderDir, page: 1 }); // maybe page:1 ?
33 | getRecordsHandler({ stockRecord })
34 | };
35 |
36 | const _handleAddRecord = ({ stockRecord = null } = {}) => {
37 | setMessage({ message: null }); // clear old messages
38 | openStockUpdateModalHandler({ stockRecord: stockRecord, newRecord: true }); // open modal
39 | };
40 |
41 | const _handleEditRecord = ({ stockRecord = null, deleteRecord = false } = {}) => {
42 | setMessage({ message: null }); // clear old messages
43 | openStockUpdateModalHandler({ stockRecord, deleteRecord }); // open update modal
44 | };
45 |
46 | const _handleStockTake = ({confirmed = false} = {}) => {
47 | setMessage({message: 'Initiating a stock take resets shrinkage & sales statistics ' +
48 | 'and generates a report. Are you sure you wish to continue?',
49 | messageClass: 'alert alert-warning'
50 | })
51 | confirmed ? stockTakeHandler() : setConfirmAction(true);
52 | };
53 |
54 | const _handleSearch = ({ stockRecord = {}, term = null } = {}) => {
55 | if (stockRecord) {
56 | Object.assign(stockRecord.meta, {
57 | pageOrderBy: 'desc', page: 1,
58 | search: _validateDesc(term)
59 | });
60 | /* set new record state early, even though stockRecord again when API returns,
61 | to ensure search string change keeps pace with user typing speed
62 | */
63 | setStockRecordState({ newStockRecord: stockRecord });
64 | // get the matching records from the API
65 | getRecordsHandler({ stockRecord });
66 | }
67 | };
68 |
69 | const _validateDesc = (value) => {
70 | return (/^[a-zA-Z\d.\- ]*$/.test(value)) ? value : stockRecord.meta.search
71 | };
72 |
73 | const _handleConfirm = ({ cancel = true } = {}) => {
74 | !cancel ? _handleStockTake({ confirmed: true }) : setMessage(
75 | { message: 'Stock take cancelled', messageClass: 'alert alert-info' }
76 | );
77 | setConfirmAction(false);
78 | }
79 |
80 | const _stockTakeConfirmDialog = () => {
81 | return (
82 |
83 | {authenticated ?
84 |
85 | _handleConfirm({ cancel: false })}>Confirm Stock Take Initiation
87 |
88 | Cancel Stock Take
89 | : }
90 |
91 | );
92 | };
93 |
94 | return (
95 |
96 |
107 |
108 |
109 |
110 |
111 |
117 |
118 |
124 |
125 |
126 | {!confirmAction ? : _stockTakeConfirmDialog()}
134 |
135 |
136 |
137 |
138 |
139 |
140 | )
141 | };
142 |
143 | export default DataTable;
144 |
--------------------------------------------------------------------------------
/src/truck.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./css/stock-update-modal.css";
3 | import Modal from "react-modal";
4 | import TruckTable from "./truck-table";
5 | import processRequest from "./api";
6 |
7 | Modal.setAppElement(document.body);
8 |
9 | Modal.defaultStyles.overlay.backgroundColor = "cornflowerblue";
10 | const REGULAR_STYLES = {
11 | content: {
12 | top: "50%",
13 | left: "50%",
14 | right: "auto",
15 | bottom: "auto",
16 | marginRight: "-50%",
17 | transform: "translate(-50%, -50%)",
18 | backgroundColor: "#001e00",
19 | color: "yellow",
20 | border: "1px solid yellow",
21 | borderRadius: "7px 7px 7px 7px",
22 | boxShadow: "-7px -7px 17px 7px #001e00",
23 | maxWidth: "800px",
24 | maxHeight: "95%"
25 | },
26 | overlay: {
27 | backgroundColor: "#2a3517"
28 | }
29 | };
30 |
31 | // const DANGER_STYLES = {
32 | // content: {
33 | // top: '50%',
34 | // left: '50%',
35 | // right: 'auto',
36 | // bottom: 'auto',
37 | // marginRight: '-50%',
38 | // transform: 'translate(-50%, -50%)',
39 | // backgroundColor: 'darkred',
40 | // color: 'white',
41 | // border: '1px solid yellow',
42 | // borderRadius: '7px 7px 7px 7px',
43 | // boxShadow: '-7px -7px 17px 7px black',
44 | // maxWidth: '800px',
45 | // },
46 | // overlay: {
47 | // backgroundColor: 'gold',
48 | // }
49 | // };
50 |
51 | /*
52 | Note: Various ways to style the modal:
53 | - css stylesheets
54 | - inline (as above, set in state (modalStyles))
55 | - default styles (also as above, as used for overlay background color).
56 | For more info. see: https://reactcommunity.org/react-modal/examples/css_classes.html
57 | */
58 |
59 | class TruckModal extends React.Component {
60 | constructor(props) {
61 | super(props);
62 | this.state = {
63 | modalStyles: REGULAR_STYLES,
64 | modalIsOpen: this.props.openTruckModal,
65 | truck: this.props.truck
66 | };
67 | // Remember! This binding is necessary to make `this` work in the callback
68 | this.handleAfterOpenModal = this.handleAfterOpenModal.bind(this);
69 | }
70 |
71 | componentWillUnmount() {
72 | console.log("Unmounting truck modal");
73 | }
74 |
75 | static getDerivedStateFromProps(nextProps, prevState) {
76 | // set modal open/close state (source of truth in parent component - app.js)
77 | let newState = {};
78 | if (nextProps.truck !== prevState.truck) {
79 | newState.truck = nextProps.truck;
80 | }
81 | return { ...newState, modalIsOpen: nextProps.openTruckModal };
82 | }
83 |
84 | componentDidUpdate() {}
85 |
86 | componentDidMount() {}
87 |
88 | handleAfterOpenModal() {
89 | // this.subtitle.style.color = 'yellow'
90 | }
91 |
92 | dispatchTruck() {
93 | /*
94 | method to make API request to transfer stock (PATCH request).
95 | */
96 | // copy truck obj to work on, by converting to string representation, then back (use JSON obj representation)
97 | let truck = JSON.parse(JSON.stringify(this.props.truck));
98 | if (truck.length > 0) {
99 | try {
100 | /* if transferring, delete all superfluous fields from the query to prevent auth issues */
101 | truck.forEach((consignment, index) => {
102 | let cargo = consignment.cargo;
103 | truck[index] = {
104 | id: cargo.id,
105 | units_to_transfer: cargo.units_to_transfer
106 | };
107 | });
108 | // hit the api
109 | const apiRequest = processRequest({
110 | stockRecord: { records: truck },
111 | apiMode: this.props.apiOptions.PATCH_STOCK
112 | });
113 | if (apiRequest) {
114 | apiRequest
115 | .then(response => {
116 | if (response) {
117 | if (response.status === 200) {
118 | this.props.emptyTruck();
119 | // update the main table with the new values
120 | this.props.openTruckModalHandler({
121 | actionCancelled: false,
122 | returnedRecords: response.data
123 | });
124 | }
125 | }
126 | })
127 | .catch(error => {
128 | console.log(`API error: ${error}`);
129 | this.props.setMessage({
130 | message: "Transfer failed! The API rejected the request.",
131 | messageClass: "alert alert-danger"
132 | });
133 | this.props.openTruckModalHandler({ actionCancelled: false });
134 | });
135 | }
136 | } catch (err) {
137 | // allow fall through to return false by default
138 | console.log(`API error: ${err}`);
139 | this.props.openTruckModalHandler({ actionCancelled: false });
140 | }
141 | }
142 | }
143 |
144 | render() {
145 | if (this.props.stockRecord) {
146 | const dispatchButtonClasses = [
147 | "btn",
148 | "btn",
149 | "btn-warning",
150 | "m-2",
151 | "table-btn"
152 | ];
153 | if (!this.state.truck.length > 0) {
154 | dispatchButtonClasses.push("d-none");
155 | }
156 | return (
157 |
165 |
166 |
167 |
168 |
(this.subtitle = subtitle)}>Truck
169 |
170 |
174 |
175 | {
178 | this.dispatchTruck();
179 | }}
180 | >
181 | Request Truck Dispatch!
182 |
183 | {
186 | this.props.openTruckModalHandler({ actionCancelled: true });
187 | }}
188 | >
189 | Cancel
190 |
191 |
192 |
193 |
194 |
195 | );
196 | }
197 | return null;
198 | }
199 | }
200 |
201 | export default TruckModal;
202 |
--------------------------------------------------------------------------------
/src/data-table-data.js:
--------------------------------------------------------------------------------
1 | import './css/data-table.css';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import React from 'react'
4 |
5 | const DataTableData = ({ stockRecord = {}, handleEditRecord, accountMode, accountModes,
6 | formatUTCDateTime, authMeta = {} } = {}) => {
7 | const { authenticated, userIsAdmin } = authMeta;
8 | const storeAccount = accountMode === accountModes.STORE
9 | if (!stockRecord || !authenticated || (stockRecord && stockRecord.data.results.length < 1)) {
10 | return (
11 |
12 |
13 |
14 | There are no records to display!
15 |
16 |
17 |
18 | )
19 | }
20 | if (storeAccount) {
21 | return stockRecord.data.results.map((item, index) => {
22 | let { sku, desc, units_total, xfer_price, record_updated, shrinkage, sold_units, selling_price } = item;
23 | let rowClasses = [units_total <= 0 ? 'outOfStock' : '', 'd-flex', 'dataTableRows'];
24 | let editButtonClasses = ['table-btn', 'btn', 'btn-primary', 'w-100', 'mb-1'];
25 | return (
26 | {/*{item.id} */}
27 | {sku}
28 | {desc}
29 | {units_total > 0 ? units_total : 'Out of Stock'}
30 | {shrinkage}
31 | {sold_units}
32 | {xfer_price}
33 | {selling_price}
34 |
35 | {formatUTCDateTime({ dateTime: record_updated })}
36 |
37 | {
38 | if (userIsAdmin || storeAccount) {
39 | Object.assign(stockRecord.data, {
40 | updateData: {
41 | ...item, // copy vals, don't pass obj. *see note (1)
42 | resultIndex: index,
43 | start_units_total: units_total
44 | }
45 | }); // copy vals, don't pass obj
46 | // reset shrinkage & sold_units in update_data to 0, as want to submit new values in edits here, not increments
47 | stockRecord.data.updateData.shrinkage = 0;
48 | stockRecord.data.updateData.sold_units = 0;
49 | return handleEditRecord({ stockRecord: stockRecord })
50 | } else return null;
51 | }} className={editButtonClasses.join(' ')}>
52 |
53 | {userIsAdmin ? {
54 | // assign record to be deleted to stockRecord.data's updateData value
55 | Object.assign(stockRecord.data, { updateData: { ...item, resultIndex: index } });
56 | return handleEditRecord({ stockRecord: stockRecord, deleteRecord: true })
57 | }}
58 | className={'table-btn btn btn-danger w-100'} id={`deleteButton_${item.id}`}>
59 | : ''}
60 |
61 | )
62 | });
63 | }
64 | else {
65 | return stockRecord.data.results.map((item, index) => {
66 | let { sku, desc, units_total, unit_price, record_updated } = item;
67 | let rowClasses = [units_total <= 0 ? 'outOfStock' : '', 'd-flex', 'dataTableRows'];
68 | let editButtonClasses = [units_total <= 0 && (!userIsAdmin && !storeAccount) ?
69 | 'disabled' : '', 'table-btn', 'btn', 'btn-primary', 'w-100', 'mb-1'];
70 | return (
71 | {/*{item.id} */}
72 | {sku}
73 | {desc}
74 | {units_total > 0 ? units_total : 'Out of Stock'}
75 | {unit_price}
76 |
77 | {formatUTCDateTime({ dateTime: record_updated })}
78 |
79 | {
80 | if (userIsAdmin || storeAccount || (!userIsAdmin && units_total > 0)) {
81 | Object.assign(stockRecord.data, {
82 | updateData: {
83 | ...item, // copy vals, don't pass obj. *see note (1)
84 | resultIndex: index,
85 | start_units_total: units_total
86 | }
87 | }); // copy vals, don't pass obj
88 | return handleEditRecord({ stockRecord: stockRecord })
89 | } else return null;
90 | }} className={editButtonClasses.join(' ')}>
91 |
92 | {userIsAdmin ? {
93 | // assign record to be deleted to stockRecord.data's updateData value
94 | Object.assign(stockRecord.data, { updateData: { ...item, resultIndex: index } });
95 | return handleEditRecord({ stockRecord: stockRecord, deleteRecord: true })
96 | }}
97 | className={'table-btn btn btn-danger w-100'} id={`deleteButton_${item.id}`}>
98 | : ''}
99 |
100 | )
101 | });
102 | }
103 | };
104 |
105 | export default DataTableData;
106 |
107 | /*
108 | Note 1: Be sure to pass values (e.g. {...item}) rather than obj (e.g. {item}),
109 | otherwise the item obj (corresponding to the data results on the main table) will be updated with
110 | values input in the console, as data.updateData would essentially
111 | point to data.results, rather than being a separate, discrete object.
112 | */
--------------------------------------------------------------------------------
/assets/js/paginate.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import '../css/paginate.css'
3 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
4 |
5 | class Paginate extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | records: {},
11 | totalPages: 0,
12 | currentPage: 1
13 | }
14 | }
15 |
16 | componentWillMount() {
17 | this.setState({
18 | records: this.props.records,
19 | totalPages: Math.ceil(this.props.records.data.data.count / this.props.records.meta.limit),
20 | currentPage: this.props.records.meta.page
21 | });
22 | }
23 |
24 | componentWillUnmount() {
25 | }
26 |
27 | componentWillReceiveProps(nextProps) {
28 | let count = nextProps.records.data.data.count || 0;
29 | this.setState({
30 | records: nextProps.records,
31 | totalPages: Math.ceil(count / nextProps.records.meta.limit),
32 | currentPage: nextProps.records.meta.page
33 | });
34 | }
35 |
36 | validatePage(value) {
37 | // if input is a number or space (allowing for backspace), return value (if 0 change to 1), else current page
38 | return /^[\d\s]*$/.test(value) ? parseInt(value) === 0 ? 1 : value : this.state.currentPage;
39 | }
40 |
41 | switchPage = ({linkedPage = 0, dir = 'selected'} = {}) => {
42 | let page, url = null;
43 | switch (dir) {
44 | case 'selected':
45 | page = linkedPage;
46 | break;
47 | case 'previous':
48 | page = this.state.records.meta.page >= 1 ? this.state.records.meta.page - 1 : 1;
49 | url = this.state.records.data.data.previous;
50 | break;
51 | case 'next':
52 | page = this.state.totalPages > this.state.records.meta.page ?
53 | this.state.records.meta.page + 1 : this.state.records.meta.page;
54 | url = this.state.records.data.data.next;
55 | break;
56 | }
57 | this.props.handleGetRecords({stockRecord: {meta: {page: page, url: url, preserveOrder: true}}})
58 | };
59 |
60 | currentPage = () => {
61 | return (
62 |
63 |
64 | Page
65 | e.keyCode === 8 ? this.setState({currentPage: ''}) : null}
66 | onChange={(e) => {
67 | this.switchPage({
68 | linkedPage: parseInt(e.target.value) > 0 ? parseInt(e.target.value)
69 | <= this.state.totalPages ? parseInt(e.target.value) : this.state.totalPages : 1,
70 | dir: 'selected'
71 | });
72 | }} value={this.state.currentPage} type={'text'}
73 | className={'form-control input-sm page-input'}/>
74 |
75 |
76 | );
77 | };
78 |
79 | prevSection = () => {
80 | return (
81 |
82 | this.switchPage({dir: 'previous'})} className="page-link">«
85 | Previous
86 |
87 | )
88 | };
89 |
90 | nextSection = () => {
91 | return (
92 |
93 | this.switchPage({dir: 'next'})} href="#" className="page-link">»
96 | Next
97 |
98 | )
99 | };
100 |
101 | mainSection = () => {
102 | let pageItemClass;
103 | let pagerMainSize = this.state.records.meta.pagerMainSize;
104 | let linkedPage = this.state.records.meta.page;
105 | let totalPages = isNaN(this.state.totalPages) ? 0 : this.state.totalPages;
106 | return (
107 |
108 | {[...Array(totalPages)].map((o, pageIndex) => {
109 | /* if selected (displayed) page is this list item, add 'active' to class to colour it
110 | and remove elements over the max size of displayed list */
111 | pageIndex + 1 > pagerMainSize ? pageItemClass = 'd-none' :
112 | linkedPage === (pageIndex + 1) ? pageItemClass = 'active page-item' :
113 | pageItemClass = 'page-item';
114 | return (
115 | this.switchPage({linkedPage: pageIndex + 1, dir: 'selected'})}
116 | className="page-link">
117 | {pageIndex + 1}
118 |
119 | );
120 | })}
121 |
122 | );
123 | };
124 |
125 | endSection = () => {
126 | let pagerMainSize = this.state.records.meta.pagerMainSize;
127 | let linkedPage = this.state.records.meta.page;
128 | let totalPages = this.state.totalPages;
129 | let numEndEle = this.state.records.meta.pagerEndSize;
130 | let iterArray = Array.apply(null, {length: numEndEle}); // create array as basis for map in frag to iterate
131 | return (
132 |
133 | {iterArray.map((o, c) => {
134 | let p = (iterArray.length - (c + 1)); // flip count order
135 | let page = (totalPages - p) || 0;
136 | return (
137 | pagerMainSize ? linkedPage === page ? 'active page-item' : 'page-item' : 'd-none'}>
139 | this.switchPage({linkedPage: page, dir: 'selected'})} href="#"
140 | className="page-link">{page} )
141 | })}
142 |
143 | );
144 | };
145 |
146 | render() {
147 | return (
148 | {this.currentPage()}
149 |
150 | {this.prevSection()}
151 | {this.mainSection()}
152 |
153 |
154 | {this.endSection()}
155 | {this.nextSection()}
156 |
157 |
158 | );
159 | }
160 | }
161 |
162 | export default Paginate;
--------------------------------------------------------------------------------
/assets/js/loginform.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../css/loginform.css';
3 | import '../css/vendor/bootstrap-4.0.0/bootstrap.min.css';
4 | import './vendor/bootstrap-4.0.0/bootstrap.min.js'
5 | import Greeting from './greeting';
6 |
7 | class LoginForm extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | // Remember! This binding is necessary to make `this` work in the callback
12 | this.loginHandler = this.loginHandler.bind(this);
13 | this.state = {
14 | formToDisplay: null,
15 | authenticated: false,
16 | password: '',
17 | username: '',
18 | oldPassword: '',
19 | newPassword: ''
20 | };
21 | }
22 |
23 | componentWillMount() {
24 | this.setState({authenticated: this.props.authMeta.authenticated}, this.setFormToDisplay(
25 | {auth: this.props.authMeta.authenticated}
26 | ))
27 | }
28 |
29 | componentWillUnmount() {
30 |
31 | }
32 |
33 | componentWillReceiveProps(nextProps) {
34 | if (nextProps.authMeta.authenticated !== this.state.authenticated) {
35 | this.setState({authenticated: nextProps.authMeta.authenticated}, this.setFormToDisplay(
36 | {auth: nextProps.authMeta.authenticated}
37 | ))
38 | }
39 | }
40 |
41 |
42 | setFormToDisplay({form = null, auth = false} = {}) {
43 | if (auth) {
44 | this.setState({formToDisplay: form ? form : 'logout'})
45 | } else {
46 | this.setState({formToDisplay: 'login'})
47 | }
48 | }
49 |
50 | changePasswordFormDisplayHandler() {
51 | this.setFormToDisplay({form: 'changePassword', auth: this.state.authenticated})
52 | }
53 |
54 | loginHandler() {
55 | let username = this.state.username;
56 | let password = this.state.password;
57 | this.setState({username: '', password: ''});
58 | this.props.authenticate({
59 | apiTrigger: this.props.API_OPTIONS.POST_AUTH,
60 | requestData: {
61 | data: {
62 | username: username,
63 | password: password
64 | },
65 | }
66 | });
67 | }
68 |
69 | changePasswordHandler() {
70 | let oldPassword = this.state.oldPassword;
71 | let newPassword = this.state.newPassword;
72 | this.setState({oldPassword: '', newPassword: ''});
73 | this.props.authenticate({
74 | apiTrigger: this.props.API_OPTIONS.PATCH_CHANGE_PW,
75 | requestData: {
76 | data: {
77 | old_password: oldPassword,
78 | new_password: newPassword,
79 | username: this.props.getSessionStorage('username')
80 | },
81 | },
82 | auth: this.state.authenticated
83 | });
84 | }
85 |
86 | handleLogout() {
87 | this.props.deleteSessionStorage(['apiToken', 'username']);
88 | this.props.authenticate({auth: false})
89 | }
90 |
91 | receivePassword(e) {
92 | this.setState({password: e.target.value})
93 | }
94 |
95 | receiveUsername(e) {
96 | this.setState({username: e.target.value})
97 | }
98 |
99 | receiveOldPassword(e) {
100 | this.setState({oldPassword: e.target.value})
101 | }
102 |
103 | receiveNewPassword(e) {
104 | this.setState({newPassword: e.target.value})
105 | }
106 |
107 | render() {
108 | let displayForm;
109 | switch (this.state.formToDisplay) {
110 | case 'login':
111 | displayForm = (
112 |
133 | );
134 | break;
135 | case 'logout':
136 | displayForm = (
137 |
138 | Welcome {this.props.getSessionStorage('username')}!
139 | this.changePasswordFormDisplayHandler()}>Change Password
141 |
142 | this.handleLogout()}>Logout
143 |
144 |
145 | );
146 | break;
147 | case 'changePassword':
148 | displayForm = (
149 |
171 | );
172 | break;
173 | }
174 | return (
175 |
176 |
177 |
178 |
{displayForm}
179 |
180 |
181 | )
182 | }
183 | }
184 |
185 | export default LoginForm;
--------------------------------------------------------------------------------
/src/paginate.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./css/paginate.css";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 |
5 | class Paginate extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | stockRecord: props.stockRecord,
10 | totalPages: Math.ceil(
11 | props.stockRecord.data.count / this.props.stockRecord.meta.limit
12 | ),
13 | currentPage: props.stockRecord.meta.page
14 | };
15 | }
16 |
17 | componentWillUnmount() {}
18 |
19 | static getDerivedStateFromProps(nextProps) {
20 | let count = nextProps.stockRecord.data.count || 0;
21 | return {
22 | stockRecord: nextProps.stockRecord,
23 | totalPages: Math.ceil(count / nextProps.stockRecord.meta.limit),
24 | currentPage: nextProps.stockRecord.meta.page
25 | };
26 | }
27 |
28 | validatePage(value) {
29 | // if input is a number or space (allowing for backspace), return value (if 0 change to 1), else current page
30 | return /^[\d\s]*$/.test(value)
31 | ? parseInt(value) === 0
32 | ? 1
33 | : value
34 | : this.state.currentPage;
35 | }
36 |
37 | switchPage({ linkedPage = 0, dir = "selected" } = {}) {
38 | let { url, page, previous, next } = this.state.stockRecord.meta;
39 | switch (dir) {
40 | case "selected":
41 | page = linkedPage;
42 | url = null; // to be set in DataTable.handleGetRecords()
43 | break;
44 | case "previous":
45 | page = page >= 1 ? page - 1 : 1;
46 | url = previous ? previous : null;
47 | break;
48 | case "next":
49 | page =
50 | this.state.totalPages > this.state.stockRecord.meta.page
51 | ? this.state.stockRecord.meta.page + 1
52 | : this.state.stockRecord.meta.page;
53 | url = next ? next : null;
54 | break;
55 | default:
56 | page = linkedPage;
57 | break;
58 | }
59 | let newStockRecord = JSON.parse(JSON.stringify(this.state.stockRecord));
60 | Object.assign(newStockRecord.meta, {
61 | page: page,
62 | limit: this.state.stockRecord.meta.limit
63 | });
64 | this.props.handleGetRecords({ stockRecord: newStockRecord, url: url });
65 | // locally set page state, to prevent delay when quick typing, pending overwrite by new source-of-truth prop
66 | this.setState({ currentPage: page });
67 | }
68 |
69 | currentPage() {
70 | return (
71 |
72 |
73 | Current page
74 |
76 | e.keyCode === 8 ? this.setState({ currentPage: "" }) : null
77 | }
78 | onChange={e => {
79 | this.switchPage({
80 | linkedPage:
81 | parseInt(e.target.value) > 0
82 | ? parseInt(e.target.value) <= this.state.totalPages
83 | ? parseInt(e.target.value)
84 | : this.state.totalPages
85 | : 1,
86 | dir: "selected"
87 | });
88 | }}
89 | value={this.state.currentPage}
90 | type={"text"}
91 | className={"form-control input-sm page-input"}
92 | />
93 |
94 |
95 | );
96 | }
97 |
98 | prevSection() {
99 | return (
100 |
101 |
102 | this.switchPage({ dir: "previous" })}
106 | className="page-link"
107 | >
108 | «
109 | Previous
110 |
111 |
112 |
113 | );
114 | }
115 |
116 | nextSection() {
117 | return (
118 |
119 |
120 | this.switchPage({ dir: "next" })}
124 | className="page-link"
125 | >
126 | »
127 | Next
128 |
129 |
130 |
131 | );
132 | }
133 |
134 | mainSection() {
135 | let pageItemClass;
136 | let pagerMainSize = this.state.stockRecord.meta.pagerMainSize;
137 | let linkedPage = this.state.stockRecord.meta.page;
138 | let totalPages = isNaN(this.state.totalPages) ? 0 : this.state.totalPages;
139 | let styles = this.props.getStyles();
140 | return (
141 |
142 | {[...Array(totalPages)].map((o, pageIndex) => {
143 | let currentPage = linkedPage === pageIndex + 1;
144 | /* if selected (displayed) page is this list item, add 'active' to class to colour it
145 | and remove elements over the max size of displayed list */
146 | pageIndex + 1 > pagerMainSize
147 | ? (pageItemClass = "d-none")
148 | : currentPage
149 | ? (pageItemClass = "active page-item")
150 | : (pageItemClass = "page-item");
151 | return (
152 |
153 |
156 | this.switchPage({
157 | linkedPage: pageIndex + 1,
158 | dir: "selected"
159 | })
160 | }
161 | className="page-link"
162 | >
163 | {pageIndex + 1}
164 |
165 |
166 | );
167 | })}
168 |
169 | );
170 | }
171 |
172 | endSection() {
173 | let pagerMainSize = this.state.stockRecord.meta.pagerMainSize;
174 | let linkedPage = this.state.stockRecord.meta.page;
175 | let totalPages = this.state.totalPages;
176 | let numEndEle = this.state.stockRecord.meta.pagerEndSize;
177 | let iterArray = Array.apply(null, { length: numEndEle }); // create array as basis for map in frag to iterate
178 | return (
179 |
180 | {iterArray.map((o, c) => {
181 | let p = iterArray.length - (c + 1); // flip count order
182 | let page = totalPages - p || 0;
183 | return (
184 | pagerMainSize
188 | ? linkedPage === page
189 | ? "active page-item"
190 | : "page-item"
191 | : "d-none"
192 | }
193 | >
194 |
196 | this.switchPage({ linkedPage: page, dir: "selected" })
197 | }
198 | href="#"
199 | className="page-link"
200 | >
201 | {page}
202 |
203 |
204 | );
205 | })}
206 |
207 | );
208 | }
209 |
210 | render() {
211 | return (
212 |
213 | {this.currentPage()}
214 |
215 | {this.prevSection()}
216 | {this.mainSection()}
217 |
218 |
219 |
220 | {this.endSection()}
221 | {this.nextSection()}
222 |
223 |
224 | );
225 | }
226 | }
227 |
228 | export default Paginate;
229 |
--------------------------------------------------------------------------------
/src/loginform.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./css/loginform.css";
3 | import "bootstrap/dist/css/bootstrap.css";
4 | import "bootstrap/dist/js/bootstrap.js";
5 | import processRequest from "./api";
6 |
7 | class LoginForm extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | // Remember! This binding is necessary to make `this` work in the callback
11 | this.handleLogin = this.handleLogin.bind(this);
12 | this.initialState = {
13 | formToDisplay: "",
14 | authenticated: this.props.authMeta.authenticated,
15 | password: "",
16 | username: "",
17 | oldPassword: "",
18 | newPassword: "",
19 | messages: {
20 | success: {
21 | authenticated: "Authentication succeeded!",
22 | changedPW: `Password successfully changed. Please log back in with your new credentials!`,
23 | loggedOut: "Successfully logged out!"
24 | },
25 | failure: {
26 | authenticated: "Authentication failed!",
27 | changedPW: "Password change failed!",
28 | loggedOut: "Full server logout failed! Please contact an admin!"
29 | }
30 | }
31 | };
32 | this.state = JSON.parse(JSON.stringify(this.initialState));
33 | }
34 |
35 | resetState() {
36 | this.setState(this.initialState);
37 | }
38 |
39 | componentDidMount() {
40 | this.setFormToDisplay();
41 | }
42 |
43 | componentWillUnmount() {
44 | this.resetState();
45 | }
46 |
47 | componentDidUpdate(prevProps, prevState) {
48 | if (prevState.authenticated !== this.state.authenticated) {
49 | this.setFormToDisplay();
50 | }
51 | }
52 |
53 | static getDerivedStateFromProps(nextProps) {
54 | return {
55 | authenticated: nextProps.authMeta.authenticated,
56 | csrfToken: nextProps.csrfToken
57 | };
58 | }
59 |
60 | setFormToDisplay({ form = null } = {}) {
61 | if (form) {
62 | this.setState({ formToDisplay: form });
63 | } else {
64 | this.setState({
65 | formToDisplay: this.state.authenticated ? "logout" : "login"
66 | });
67 | }
68 | }
69 |
70 | handleLogin() {
71 | let username = this.state.username;
72 | let password = this.state.password;
73 | this.setState({ username: "", password: "" });
74 | this.authenticate({
75 | apiMode: this.props.apiOptions.POST_AUTH,
76 | requestData: {
77 | data: {
78 | username: username,
79 | password: password
80 | }
81 | }
82 | });
83 | this.resetState();
84 | }
85 |
86 | handleLogout() {
87 | // logout of server
88 | this.authenticate({
89 | apiMode: this.props.apiOptions.POST_LOGOUT
90 | });
91 | // delete local session data
92 | this.props.deleteSessionStorage(["token", "username"]);
93 | this.resetState();
94 | this.props.setAuthentication();
95 | }
96 |
97 | handleChangePassword() {
98 | let oldPassword = this.state.oldPassword;
99 | let newPassword = this.state.newPassword;
100 | this.setState({ oldPassword: "", newPassword: "" });
101 | this.authenticate({
102 | apiMode: this.props.apiOptions.PATCH_CHANGE_PW,
103 | requestData: {
104 | data: {
105 | old_password: oldPassword,
106 | new_password: newPassword,
107 | username: this.props.getSessionStorage("username")
108 | }
109 | }
110 | });
111 | this.resetState();
112 | }
113 |
114 | authenticate({ apiMode = null, requestData = {} } = {}) {
115 | // triggers API to get auth token
116 | const apiRequest = processRequest({
117 | apiMode: apiMode,
118 | csrfToken: this.state.csrfToken,
119 | requestData: requestData.data
120 | });
121 | if (apiRequest) {
122 | apiRequest
123 | .then(response => {
124 | if (response && Object.prototype.hasOwnProperty.call(response.data, "token")) {
125 | // logged in
126 | this.props.deleteSessionStorage(["token", "username"]); // ensure any existing token deleted 1st
127 | this.props.setSessionStorage({
128 | key: "token",
129 | value: response.data.token
130 | });
131 | this.props.setSessionStorage({
132 | key: "username",
133 | value: requestData.data.username
134 | });
135 | this.props.setMessage({
136 | // display message
137 | message: this.state.messages.success.authenticated,
138 | messageClass: "alert alert-success"
139 | });
140 | this.props.setAuthentication();
141 | }
142 | if (Object.prototype.hasOwnProperty.call(response.data, "password")) {
143 | // changed password
144 | if (response.data.password === "CHANGED") {
145 | this.props.deleteSessionStorage(["token", "username"]); // delete existing token to enforce re-login
146 | this.props.setMessage({
147 | message: this.state.messages.success.changedPW,
148 | messageClass: "alert alert-success"
149 | });
150 | this.props.setAuthentication();
151 | } else {
152 | this.props.setMessage({
153 | message: this.state.messages.failure.changedPW,
154 | messageClass: "alert alert-success"
155 | });
156 | }
157 | }
158 | if (Object.prototype.hasOwnProperty.call(response.data, "logged_in")) {
159 | // logout
160 | if (!response.data.logged_in) {
161 | this.props.setMessage({
162 | message: this.state.messages.success.loggedOut,
163 | messageClass: "alert alert-success"
164 | });
165 | } else {
166 | this.props.setMessage({
167 | message: this.state.messages.failure.loggedOut,
168 | messageClass: "alert alert-danger"
169 | });
170 | }
171 | }
172 | })
173 | .catch(error => {
174 | let displayError = null;
175 | if (error.response && error.response.data) {
176 | let firstError = Object.keys(error.response.data)[0];
177 | displayError = error.response.data[firstError][0];
178 | }
179 | this.resetState();
180 | this.props.setMessage({
181 | message: `${
182 | displayError
183 | ? displayError
184 | : this.state.messages.failure.authenticated
185 | }`,
186 | messageClass: "alert alert-danger"
187 | });
188 | });
189 | }
190 | }
191 |
192 | receivePassword(e) {
193 | this.setState({ password: e.target.value });
194 | }
195 |
196 | receiveUsername(e) {
197 | this.setState({ username: e.target.value });
198 | }
199 |
200 | receiveOldPassword(e) {
201 | this.setState({ oldPassword: e.target.value });
202 | }
203 |
204 | receiveNewPassword(e) {
205 | this.setState({ newPassword: e.target.value });
206 | }
207 |
208 | render() {
209 | let displayForm;
210 | switch (this.state.formToDisplay) {
211 | case "login":
212 | default:
213 | displayForm = (
214 |
246 | );
247 | break;
248 | case "logout":
249 | displayForm = (
250 |
251 |
252 | Welcome {this.props.getSessionStorage("username")}!
253 |
254 | this.setFormToDisplay({ form: "changePassword" })}
257 | >
258 | Change Password
259 |
260 | this.handleLogout()}
263 | >
264 | Logout
265 |
266 |
267 | );
268 | break;
269 | case "changePassword":
270 | displayForm = (
271 |
306 | );
307 | break;
308 | }
309 | return (
310 |
311 | {displayForm}
312 |
313 | );
314 | }
315 | }
316 |
317 | export default LoginForm;
318 |
--------------------------------------------------------------------------------
/src/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios/index";
2 |
3 | const processRequest = ({
4 | stockRecord = null,
5 | requestData = null,
6 | apiMode = null,
7 | csrfToken = null,
8 | url = null
9 | } = {}) => {
10 | const { requestType, method } = apiMode;
11 | if (apiMode && requestType) {
12 | if (requestType === "get_stock") {
13 | return _getStock({ stockRecord, csrfToken, requestMethod: method, url }); // returns a promise
14 | } else if (requestType === "get_account_stock") {
15 | return _getAccountStock({
16 | stockRecord,
17 | csrfToken,
18 | requestMethod: method,
19 | url
20 | }); // returns a promise
21 | } else if (requestType === "get_take_stock") {
22 | return _getTakeStock({
23 | stockRecord,
24 | csrfToken,
25 | requestMethod: method,
26 | url
27 | }); // returns a promise
28 | } else if (requestType === "patch_stock") {
29 | return _updateStock({
30 | stockRecord,
31 | csrfToken,
32 | requestMethod: method,
33 | url
34 | }); // returns a promise
35 | } else if (requestType === "patch_account_stock") {
36 | return _updateAccountStock({
37 | stockRecord,
38 | csrfToken,
39 | requestMethod: method,
40 | url
41 | }); // returns a promise
42 | } else if (requestType === "add_stock") {
43 | return _addStock({ stockRecord, csrfToken, requestMethod: method, url }); // returns a promise
44 | } else if (requestType === "add_account_stock") {
45 | return _addAccountStock({
46 | stockRecord,
47 | csrfToken,
48 | requestMethod: method,
49 | url
50 | }); // returns a promise
51 | } else if (requestType === "delete_stock_line") {
52 | return _deleteStock({ stockRecord, csrfToken, requestMethod: method });
53 | } else if (requestType === "delete_account_stock_line") {
54 | return _deleteAccountStock({
55 | stockRecord,
56 | csrfToken,
57 | requestMethod: method
58 | });
59 | } else if (
60 | requestType === "post_auth" ||
61 | requestType === "patch_change_pw" ||
62 | requestType === "post_logout"
63 | ) {
64 | return _auth({ csrfToken, requestData, apiMode, requestMethod: method });
65 | }
66 | }
67 | console.log(`
68 | No API mode, or stock record configuration set. API requested failed.
69 | Pertinent variable values: requestType=${requestType}; method=${method}`);
70 | return false;
71 | };
72 |
73 | const _getSessionStorage = key => {
74 | //return JSON.parse(localStorage.getItem(key));
75 | return sessionStorage.getItem(key);
76 | };
77 |
78 | const _makeRequest = ({
79 | stockRecord = null,
80 | requestMethod = null,
81 | csrfToken = null,
82 | requestData = null,
83 | url = null,
84 | } = {}) => {
85 | const CancelToken = axios.CancelToken;
86 | let cancel;
87 | // if no requestData passed, see if update data in stock record data.
88 | if (!requestData && stockRecord !== null) {
89 | requestData = stockRecord.data.updateData;
90 | }
91 | if (url && requestMethod) {
92 | // make request
93 | if (cancel !== undefined) {
94 | cancel();
95 | console.log(
96 | "API request cancelled because an existing request was already underway!"
97 | );
98 | }
99 | return axios({
100 | cancelToken: new CancelToken(c => (cancel = c)),
101 | method: requestMethod,
102 | url: url,
103 | responseType: "json",
104 | data: requestData,
105 | //auth: {},
106 | headers: {
107 | Authorization: _getSessionStorage("token")
108 | ? `Token ${_getSessionStorage("token")}`
109 | : null,
110 | //'cache-control': 'no-cache',
111 | "Content-Type": "application/json",
112 | "X-CSRFToken": csrfToken
113 | } // additional headers here
114 | });
115 | }
116 | console.log("API did not send a request");
117 | };
118 |
119 | const _getStock = ({
120 | stockRecord = null,
121 | csrfToken = null,
122 | requestMethod = null,
123 | url = null
124 | } = {}) => {
125 | // get stock for warehouse
126 | if (stockRecord) {
127 | if (!url) {
128 | let { pageOrderBy, pageOrderDir, search, limit, page } = stockRecord.meta;
129 | if (!url) {
130 | // constructs request URL, unless pre-defined in paginate.js through api 'next' or 'previous'.
131 | // build url
132 | url =
133 | `${process.env.REACT_APP_API_DATA_ROUTE}/stock/?limit=${limit}` +
134 | `&offset=${page * limit - limit}` +
135 | `&order_by=${pageOrderDir}${pageOrderBy}&desc=${search}`; // update URL
136 | }
137 | }
138 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url }); // returns a promise
139 | }
140 | return false;
141 | };
142 |
143 | const _getAccountStock = ({
144 | stockRecord = null,
145 | csrfToken = null,
146 | requestMethod = null,
147 | url = null
148 | } = {}) => {
149 | // get stock for store account
150 | if (stockRecord) {
151 | if (!url) {
152 | let { pageOrderBy, pageOrderDir, search, limit, page } = stockRecord.meta;
153 | if (!url) {
154 | // constructs request URL, unless pre-defined in paginate.js through api 'next' or 'previous'.
155 | // build url
156 | url =
157 | `${
158 | process.env.REACT_APP_API_DATA_ROUTE
159 | }/accounts/stock/?limit=${limit}` +
160 | `&offset=${page * limit - limit}` +
161 | `&order_by=${pageOrderDir}${pageOrderBy}&desc=${search}`; // update URL
162 | }
163 | }
164 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url }); // returns a promise
165 | }
166 | return false;
167 | };
168 |
169 | const _getTakeStock = ({
170 | stockRecord = null,
171 | csrfToken = null,
172 | requestMethod = null,
173 | url = null
174 | } = {}) => {
175 | // initiate stock take
176 | url = url
177 | ? url
178 | : `${process.env.REACT_APP_API_DATA_ROUTE}/accounts/take-stock/`;
179 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url });
180 | };
181 |
182 | const _updateStock = ({
183 | stockRecord = null,
184 | csrfToken = null,
185 | requestMethod = null,
186 | url = null
187 | } = {}) => {
188 | // manager's transfer updates - updates a truck load!
189 | if ("records" in stockRecord) {
190 | url = `${process.env.REACT_APP_API_DATA_ROUTE}/stock/`;
191 | return _makeRequest({
192 | requestData: { ...stockRecord },
193 | csrfToken,
194 | requestMethod,
195 | url
196 | });
197 | } else if (stockRecord) {
198 | // admin's stock record updates - updates single line (not truck)
199 | // build url
200 | url = url
201 | ? url
202 | : `${process.env.REACT_APP_API_DATA_ROUTE}/stock/${
203 | stockRecord.data.updateData.id
204 | }/`;
205 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url });
206 | }
207 | return false;
208 | };
209 |
210 | const _updateAccountStock = ({
211 | stockRecord = null,
212 | csrfToken = null,
213 | requestMethod = null,
214 | url = null
215 | } = {}) => {
216 | // store account updates
217 | if (stockRecord) {
218 | // admin's stock record updates
219 | // build url
220 | url = url
221 | ? url
222 | : `${process.env.REACT_APP_API_DATA_ROUTE}/accounts/stock/${
223 | stockRecord.data.updateData.id
224 | }/`;
225 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url });
226 | }
227 | return false;
228 | };
229 |
230 | const _addStock = ({
231 | stockRecord = null,
232 | csrfToken = null,
233 | requestMethod = null,
234 | url = null
235 | } = {}) => {
236 | if (stockRecord) {
237 | // build url
238 | url = url ? url : `${process.env.REACT_APP_API_DATA_ROUTE}/stock/`;
239 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url });
240 | }
241 | return false;
242 | };
243 |
244 | const _addAccountStock = ({
245 | stockRecord = null,
246 | csrfToken = null,
247 | requestMethod = null,
248 | url = null
249 | } = {}) => {
250 | if (stockRecord) {
251 | // build url
252 | url = url ? url : `${process.env.REACT_APP_API_DATA_ROUTE}/accounts/stock/`;
253 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url });
254 | }
255 | return false;
256 | };
257 |
258 | const _deleteStock = ({
259 | stockRecord = null,
260 | csrfToken = null,
261 | requestMethod = null,
262 | url = null
263 | } = {}) => {
264 | if (stockRecord) {
265 | // build url
266 | url = url
267 | ? url
268 | : `${process.env.REACT_APP_API_DATA_ROUTE}/stock/${
269 | stockRecord.data.updateData.id
270 | }/`;
271 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url });
272 | }
273 | return false;
274 | };
275 |
276 | const _deleteAccountStock = ({
277 | stockRecord = null,
278 | csrfToken = null,
279 | requestMethod = null,
280 | url = null
281 | } = {}) => {
282 | if (stockRecord) {
283 | // build url
284 | url = url
285 | ? url
286 | : `${process.env.REACT_APP_API_DATA_ROUTE}/accounts/stock/${
287 | stockRecord.data.updateData.id
288 | }/`;
289 | return _makeRequest({ stockRecord, csrfToken, requestMethod, url });
290 | }
291 | return false;
292 | };
293 |
294 | const _auth = ({
295 | requestMethod = null,
296 | csrfToken = null,
297 | requestData = null,
298 | apiMode = null
299 | } = {}) => {
300 | const apiRoute = process.env.REACT_APP_API_ROUTE;
301 | const loginURL = `${apiRoute}/api-token-auth/`;
302 | const logoutURL = `${apiRoute}/v2/logout/`;
303 | const changePWURL = `${apiRoute}/v2/change-password/${_getSessionStorage(
304 | "username"
305 | )}/`;
306 | let url = null;
307 | switch (apiMode.requestType) {
308 | case "patch_change_pw":
309 | url = changePWURL;
310 | break;
311 | case "post_logout":
312 | url = logoutURL;
313 | break;
314 | default:
315 | url = loginURL;
316 | }
317 | const cacheControl = "no-cache";
318 | return _makeRequest({
319 | requestMethod,
320 | requestData,
321 | csrfToken,
322 | cacheControl,
323 | url: url
324 | });
325 | };
326 |
327 | export default processRequest;
328 |
329 | /*
330 | Note 1:
331 | Method to generate the request ordering. Returns '' or '-'.
332 | Reverses ordering if column clicked when already ordered on the same column. Otherwise, defaults to ascending order.
333 | Note: page always reverts to page 1 IF column clicked to change order on any page other than 1, as new ordering
334 | is requested from API. Needs to return to page 1 to display the newly requested (differently sorted) pages -
335 | otherwise the current page would display any data that happened to correspond to that page of newly received data,
336 | and that wouldn't be expected behaviour.
337 | Could also add method (& UI link/button/widget) for columns to LOCALLY order/sort presently displayed page data
338 | (without an API request) if required.
339 | */
340 |
--------------------------------------------------------------------------------
/assets/js/data-table.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Paginate from './paginate.jsx';
3 | import '../css/data-table.css';
4 | import '../css/vendor/bootstrap-4.0.0/bootstrap.min.css';
5 | import './vendor/bootstrap-4.0.0/bootstrap.min.js'
6 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
7 | import moment from 'moment'
8 | import 'moment/locale/en-gb.js'
9 | import 'moment-timezone'
10 |
11 | class DataTable extends React.Component {
12 |
13 | constructor(props) {
14 | super(props);
15 | // keep real-time props changes in component state
16 | this.state = {
17 | modalIsOpen: false,
18 | dataTable: null,
19 | message: null,
20 | messageClass: '',
21 | datetimeOfRequest: null,
22 | search: ''
23 | };
24 | }
25 |
26 | static formatUTCDateTime({dateTime = null} = {}) {
27 | // takes datetime in UTC, formats and returns datetime in user's browser reported timezone
28 | return dateTime ? `${moment.utc(dateTime).local()
29 | .format('DD/MM/YYYY HH:mm:ss')} ${moment.tz(moment.tz.guess()).zoneAbbr()}`
30 | : null;
31 | }
32 |
33 | componentWillMount() {
34 | // kick it off, make a request for stock data ...
35 | this.handleGetRecords({stockRecord: {meta: this.props.stockRecordMeta}});
36 | }
37 |
38 | componentWillUnmount() {
39 | }
40 |
41 | componentWillReceiveProps(nextProps) {
42 | // generate new table
43 | this.setState({
44 | message: nextProps.message,
45 | messageClass: nextProps.messageClass,
46 | datetimeOfRequest: DataTable.formatUTCDateTime(
47 | {dateTime: nextProps.stockRecordMeta.datetime_of_request}),
48 | search: nextProps.stockRecordMeta.search,
49 | dataTable:
50 | this.generateTable({
51 | stockRecordData: nextProps.stockRecordData,
52 | stockRecordMeta: nextProps.stockRecordMeta
53 | }),
54 | });
55 | }
56 |
57 | handleGetRecords({stockRecord = {}} = {}) {
58 | this.props.setMessage({message: null}); // clear old messages
59 | this.props.setStockRecordState({
60 | stockRecord: stockRecord,
61 | apiTrigger: this.props.API_OPTIONS.GET_STOCK
62 | }); // set record request state
63 | }
64 |
65 | handleAddRecord() {
66 | this.props.setMessage({message: null}); // clear old messages
67 | this.props.openStockUpdateModalHandler({record: {meta: {newRecord: true}}}); // open modal
68 | }
69 |
70 | handleEditRecord({record = null} = {}) {
71 | this.props.setMessage({message: null}); // clear old messages
72 | this.props.openStockUpdateModalHandler({record: record}); // open update modal
73 | }
74 |
75 | handleDeleteLine({record = null} = {}) {
76 | this.props.setMessage({message: null}); // clear old messages
77 | this.props.openStockUpdateModalHandler({record: {data: record, meta: {deleteRecord: true}}})
78 | }
79 |
80 | handleSearch(e) {
81 | this.handleGetRecords({
82 | stockRecord: {
83 | meta: {
84 | orderBy: 'desc', page: 1,
85 | search: this.validateDesc(e.target.value)
86 | }
87 | }
88 | });
89 | }
90 |
91 | validateDesc(value) {
92 | return (/^[a-zA-Z\d.\- ]*$/.test(value)) ? value : this.state.search
93 | }
94 |
95 | generateTable({stockRecordData = {}, stockRecordMeta = {}} = {}) {
96 | let navBar = () => {
97 | return (
98 |
99 |
100 |
101 | this.handleGetRecords({
102 | stockRecord: {meta: {page: 1, preserveOrder: true}}
103 | })
104 | } className={'btn btn-warning'}>
105 |
106 |
107 |
108 | this.handleAddRecord()}
109 | className={'btn btn-warning'}>
110 |
111 |
112 |
113 |
114 |
115 | this.handleSearch(e)}/>
118 |
119 |
120 |
127 |
128 |
129 | )
130 | };
131 | let noData = () => {
132 | return (
133 |
134 | {navBar()}
135 |
Loading data ...
136 |
137 | );
138 | };
139 | let resultData = ({stockData = null} = {}) => {
140 | return stockData.data.results.length ?
141 | stockData.data.results.map((item) => {
142 | return (
143 | {/*{item.id} */}
144 | {item.sku}
145 | {item.desc}
146 | {item.units_total}
147 | {item.unit_price}
148 | {DataTable.formatUTCDateTime({dateTime: item.record_updated})}
150 |
151 | this.handleEditRecord({record: {data: item}})}
152 | className={'table-btn btn btn-primary'}>
153 |
154 | {stockRecordMeta.userIsAdmin ?
155 | this.handleDeleteLine({record: item})}
156 | className={'table-btn btn btn-danger'}>
157 | : ''}
158 |
159 | )
160 | }) : null;
161 | };
162 | let table = () => {
163 | let data = resultData({stockData: stockRecordData});
164 | return data ? (
165 |
166 | {navBar()}
167 |
168 |
169 |
170 |
171 | {this.props.APP_DETAILS.shortOrgName} Stock
172 | Data {stockRecordMeta.datetime_of_request ?
173 | `[Request returned: ${this.state.datetimeOfRequest}]` : ''}
174 |
175 |
176 |
177 | this.handleGetRecords({
178 | stockRecord: {meta: {orderBy: 'sku', page: 1}}
179 | })}>
180 | SKU
181 |
182 | this.handleGetRecords({
183 | stockRecord: {meta: {orderBy: 'desc', page: 1}}
184 | })}>
185 | Description
186 |
187 | this.handleGetRecords({
188 | stockRecord: {meta: {orderBy: 'units_total', page: 1}}
189 | })}>
190 | Units
191 |
192 | this.handleGetRecords({
193 | stockRecord: {meta: {orderBy: 'unit_price', page: 1}}
194 | })}>
195 | Unit Price
196 |
197 | this.handleGetRecords({
198 | stockRecord: {meta: {orderBy: 'record_updated', page: 1}}
199 | })}>
200 | Record Updated
201 |
202 |
203 | Action
204 |
205 |
206 |
207 |
208 | {data}
209 |
210 |
211 |
212 |
213 |
214 |
215 | ) : null;
216 | };
217 | return table() ? table() : noData();
218 | }
219 |
220 | render() {
221 | return (
222 |
223 | {this.state.dataTable}
224 |
225 | )
226 | }
227 | }
228 |
229 | export default DataTable;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Stock Management - Frontend Client Component
2 |
3 | ## Security Advisory
4 |
5 | Please note, this application has not been audited for security and *probably does* contain vulnerabilities that could expose data contained on the host system to unauthorized manipulation or disclosure, both during the building process and deployment. Use at your own risk.
6 |
7 | ## About
8 |
9 | This a demo/prototype repository for the frontend web client for the Simple Stock Management stock and inventory system. It is built using web technologies, with a client/server architecture. The repository for the server component is at: https://github.com/Aninstance/simple-stock-management
10 |
11 | The system allows "stores" to request transfers of stock ("order") from a central stock repository ("warehouse"). Stock is adjusted for the "Warehouse Account" and the "Store Account" as stock transfers are "ordered". Email notifications are sent to the "warehouse" administrator(s) and the ordering "store manager".
12 |
13 | This project - available to subscribers and clients as a regularly maintained application-as-a-service - offers a web frontend that connects to a RESTful API backend. Data is stored in either a SQLite, mySQL or PostgreSQL (recommended) database.
14 |
15 | ## Support & Project Status
16 |
17 | A regularly patched, proprietary licensed application-as-a-service version, fully maintained for subscribers and clients, is available upon request (limited availability) and is currently priced at £10.00/month.
18 |
19 | A one-off installation service for this GPL open source version is also available.
20 |
21 | The GPL licensed version of this project offered here is *not guaranteed* to be regularly maintained. It is made available here for demo/prototype purposes only, and should not be used in production (i.e. a "live" working environment) unless the administrator regularly patches project dependencies (i.e. PYPI & npm packages) with upstream security updates as and when released by vendors.
22 |
23 | If you would like to avail of the proprietary subscription to the application-as-a-service, or request other bespoke work on this project, please email to discuss: ssm@uplandsdynamic.com.
24 |
25 |
26 | ## Key Technologies for Client Component
27 |
28 | Key technologies include: Javascript (ReactJS); HTML5; CSS3; BootStrap 4.
29 |
30 | ## Live Demo
31 |
32 | There is a live demo, available here:
33 |
34 | https://frontend.ssm.webapps.uplandsdynamic.com
35 |
36 | There are two test users - one for the warehouse administrator, the other for a 'store manager'. Credentials are:
37 |
38 | Adminstrator:
39 | Username: test_admin
40 | Password: jduejHje(89K
41 |
42 | Manager:
43 | Username: test_manager
44 | Password: jduejHje(89K
45 |
46 | ## Screenshots
47 |
48 | 
49 | 
50 | 
51 |
52 | 
53 | 
54 | 
55 | 
56 | 
57 | 
58 |
59 | ## Key features
60 |
61 | - Administrator may add, edit and delete stock from database.
62 | - Store managers may request transfers ("order") stock from the "warehouse".
63 | - Dynamic search of stock lines (SKU and description).
64 | - Configurable pagination of results table.
65 | - Transfer requests of stock lines are loaded to a "truck" (i.e. like "adding to a basket/cart" in an e-commerce system), before the request is submitted.
66 | - The "truck" retains the transfer data until the "Request truck dispatch" button is clicked. The truck data is retained across sessions (meaning the data remains in the truck even if the user logs out, then resumes their transfer at a later time).
67 | - Once the "Request truck dispatch" button is clicked, the transfer request process will complete. The truck empties and a single email containing a summary of the successful transfers - and any failures - is dispatched to both the requesting user and the warehouse administrator. Warehouse quantities are immediately adjusted accordingly, both in the "Warehouse" and "Store" accounts.
68 | - A "Stock take" feature compiles and emails detailed reports, consisting of:
69 |
70 | - For every unique stock line in a "Store Account" (see screenshot #10, below, for an example report):
71 |
72 | - SKU
73 | - Stock description
74 | - Units of opening stock
75 | - Units of closing stock
76 | - Change in stock units since last stock take
77 | - Number of units transferred since last stock take
78 | - Number of units recorded sold since last stock take
79 | - Number of units recorded as shrinkage since last stock take
80 | - Differential for units of unrecorded history since last stock take (i.e. unrecorded sales, unrecorded transfers, unrecorded loss)
81 | - Current transfer value of a unit
82 | - Current retail price of a unit
83 | - Total value of units recorded sold since last stock take
84 | - Total value of units recorded as shrinkage since last stock take
85 | - Total value of units transferred since last stock take
86 | - Total value differential of units with unrecorded history since last stock take, at present xfer price
87 | - Total value differential of units with unrecorded history since last stock take, at present retail price (i.e. unrecorded sales, unrecorded transfers, shrinkage)
88 | - Current total held stock transfer value at present xfer price
89 | - Current total held stock retail value at present retail price
90 |
91 | - Overview of the entire "Store Account" (see screenshot #10, below, for an example report):
92 |
93 | - Units of opening stock
94 | - Units of closing stock
95 | - Units of stock transferred since last stock take
96 | - Units of stock recorded sold since last stock take
97 | - Units of stock recorded as shrinkage since last stock take
98 | - Change in stock holding owing to unrecorded unit history since last stock take (i.e. unrecorded sales, unrecorded transfers, unrecorded loss)
99 | - Value of stock recorded sold since last stock take
100 | - Value of stock recorded as shrinkage since last stock take
101 | - Total value differential of units with unrecorded history since last stock take at current transfer value
102 | - Total value differential of units with unrecorded history since last stock take at current retail value (i.e. unrecorded sales, unrecorded transfers, unrecorded loss)
103 | - Total value of transfers since last stock take (at actual xfer prices)
104 | - All time total value of transfers (at actual xfer prices)
105 | - Value of held stock at current transfer price
106 | - Value of held stock at current retail price
107 |
108 | - Automated removal of obsolete stock line records (lines with zero units of held stock) from the Store accounts following a successful stock take process
109 | - Historical retention of previous stock take data (not currently exposed on the UI)
110 |
111 | ## Brief UI instructions
112 |
113 | Warehouse administrators:
114 |
115 | - Plus sign button allows adding new stock lines
116 | - Circular arrows button refreshes records from the database
117 | - Pencil icon button in `Action` column allows editing of stock line
118 | - Dustbin icon button in `Action` column allows deletion of a stock line
119 |
120 | Store account managers:
121 |
122 | - Head-&-shoulders icon (right of top header bar) switches between `Warehouse` account (from where transfers are requested) and the user's `Store` account
123 | - Truck icon (right of top header bar) opens the user's "transfer truck"
124 | - Circular arrows button refreshes records from the database
125 | - Plus sign button allows manual addition of new lines to the `Store` account
126 | - Pencil icon button in `Action` column allows editing of stock line data (e.g. change stock level, record a sale or shrinkage, etc)
127 | - `New shrinkage` & `New recorded sold` update fields are ***disabled** during a stock line edit if the `stock quantity` field is changed. This is to prevent user error by inadvertent duplication of submitted data (i.e. user manually decrementing the `stock quantity` field whilst also recording the same data as `New recorded sold`). Likewise, the `stock quantity` field is disabled if the `New shrinkage` and/or `New recorded sold` fields are edited, for the same reason
128 | - Eye icon button initiates a stock take
129 |
130 | ## Installation & Usage (on Linux systems)
131 |
132 | __Below are basic steps to install and run a demonstration of the app on an Linux Ubuntu 20.04 server. They do not provide for a secure installation, such as would be required if the app was publicly available. Steps should be taken to security harden the environment if using in production.__
133 |
134 | ### Brief Installation Instructions
135 |
136 | - Clone repository to an app source directory of your choice, e.g. `git clone https://github.com/Aninstance/simple-stock-management-frontend.git`.
137 | - Make web root directory, e.g. `mkdir -p /var/www/html/ssm-frontend`.
138 | - Move into the cloned directory and install the required node packages. Ensure a current version of NPM is installed and active on your system - it's recommended to install NVM (Node Version Manager) - which allows multiple node versions to be installed - to avoid changing a pre-installed version that may be required by other software or packages on your system.
139 | - Install npx on your system if not already installed, e.g.: `npm install -g npx`.
140 | - Install the packages by running `npm install`.
141 | - In the event of any vulnerabilities being flagged up, run `npm audit fix`.
142 | - Configure the app environment by coping the `.env.production_DEFAULT` file to `.env.production` and editing according to your requirements.
143 | - Build the app, e.g.: `npm run build:production`.
144 | - Copy the built app into its web directory, e.g. `cp -a build/. /var/www/html/ssm-frontend/;` `cp -a /var/www/html/ssm-frontend/static. /var/www/html/ssm-frontend/;`.
145 | - Recursively change ownership of the `ssm-frontend` directory to your web server user.
146 | - Configure your web server to serve the app from your web directory (this is straight forward, but outwith the scope of this document. If you need further help, please check your web server's documentation).
147 |
148 | ### Update Instructions
149 |
150 | - Ensure you backup a copy of changes to your environment configuration - make a copy of your `.env.production` file *outside* of your cloned directory.
151 | - From the build directory, run `git pull`.
152 | - Remove existing node modules, e.g.: `rm -rf node_modules`.
153 | - Remove the package lock file, e.g.: `rm -rf package-lock.json`.
154 | - Install updated modules, e.g.: `npm install`.
155 | - In the event of any vulnerabilities being flagged up, run `npm audit fix`.
156 | - Rebuild the project, e.g.: `npm run build:production`.
157 | - Once the app has been built, copy to the web directory: `cp -a build/. /var/www/html/ssm-frontend/;` `cp -a /var/www/html/ssm-frontend/static/. /var/www/html/ssm-frontend/;`.
158 | - Recursively change ownership of the `ssm-frontend` directory to your web server user.
159 | - Restart your web server.
160 |
161 | Note: The above guide is not definitive and is intended for users who know their way around Ubuntu server and Django.
162 |
163 | *Users would need to arrange database backups and to secure the application appropriately when used in a production environment.*
164 |
165 | ## Development Roadmap
166 |
167 | - No new features planned at present. To request a change or additional functionality, or to file a bug, please open a github issue and/or contact dan@uplandsdynamic.com.
168 |
169 | ## Authors
170 |
171 | - Dan Bright (Uplands Dynamic), dan@uplandsdynamic.com
172 |
--------------------------------------------------------------------------------
/src/stock-update-modal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./css/stock-update-modal.css";
3 | import Modal from "react-modal";
4 | import StockUpdateTable from "./stock-update-table.js";
5 | import StockUpdateDelete from "./stock-update-delete.js";
6 | import processRequest from "./api.js";
7 |
8 | Modal.setAppElement(document.body);
9 |
10 | Modal.defaultStyles.overlay.backgroundColor = "cornflowerblue";
11 | const REGULAR_STYLES = {
12 | content: {
13 | top: "50%",
14 | left: "50%",
15 | right: "auto",
16 | bottom: "auto",
17 | marginRight: "-50%",
18 | transform: "translate(-50%, -50%)",
19 | backgroundColor: "#001e00",
20 | color: "yellow",
21 | border: "1px solid yellow",
22 | borderRadius: "7px 7px 7px 7px",
23 | boxShadow: "-7px -7px 17px 7px #001e00",
24 | maxWidth: "800px",
25 | maxHeight: "95%"
26 | },
27 | overlay: {
28 | backgroundColor: "#2a3517"
29 | }
30 | };
31 |
32 | const REGULAR_STYLES_ACCOUNT = {
33 | content: {
34 | top: "50%",
35 | left: "50%",
36 | right: "auto",
37 | bottom: "auto",
38 | marginRight: "-50%",
39 | transform: "translate(-50%, -50%)",
40 | backgroundColor: "#000033",
41 | color: "yellow",
42 | border: "1px solid yellow",
43 | borderRadius: "7px 7px 7px 7px",
44 | boxShadow: "-7px -7px 17px 7px 000033",
45 | maxWidth: "800px",
46 | maxHeight: "95%"
47 | },
48 | overlay: {
49 | backgroundColor: "#000033"
50 | }
51 | };
52 |
53 | const DANGER_STYLES = {
54 | content: {
55 | top: "50%",
56 | left: "50%",
57 | right: "auto",
58 | bottom: "auto",
59 | marginRight: "-50%",
60 | transform: "translate(-50%, -50%)",
61 | backgroundColor: "darkred",
62 | color: "white",
63 | border: "1px solid yellow",
64 | borderRadius: "7px 7px 7px 7px",
65 | boxShadow: "-7px -7px 17px 7px black",
66 | maxWidth: "800px"
67 | },
68 | overlay: {
69 | backgroundColor: "gold"
70 | }
71 | };
72 |
73 | /*
74 | Note: Various ways to style the modal:
75 | - css stylesheets
76 | - inline (as above, set in state (modalStyles))
77 | - default styles (also as above, as used for overlay background color).
78 | For more info. see: https://reactcommunity.org/react-modal/examples/css_classes.html
79 | */
80 |
81 | class StockUpdateModal extends React.Component {
82 | constructor(props) {
83 | super(props);
84 | this.state = {
85 | modalStyles: REGULAR_STYLES,
86 | modalIsOpen: this.props.openStockUpdateModal.state,
87 | deleteRecord: this.props.openStockUpdateModal.deleteRecord,
88 | newRecord: this.props.openStockUpdateModal.newRecord
89 | };
90 | // Remember! This binding is necessary to make `this` work in the callback
91 | this.handleAfterOpenModal = this.handleAfterOpenModal.bind(this);
92 | this.handleCloseModal = this.handleCloseModal.bind(this);
93 | }
94 |
95 | componentWillUnmount() {
96 | console.log("Unmounting stock update modal");
97 | }
98 |
99 | static getDerivedStateFromProps(nextProps) {
100 | let newStyles =
101 | nextProps.accountMode === nextProps.accountModes.STORE
102 | ? REGULAR_STYLES_ACCOUNT
103 | : REGULAR_STYLES;
104 | if (nextProps.openStockUpdateModal.deleteRecord) {
105 | newStyles = DANGER_STYLES;
106 | }
107 | // set modal open/close state (source of truth in parent component - app.js)
108 | return {
109 | modalIsOpen: nextProps.openStockUpdateModal.state,
110 | deleteRecord: nextProps.openStockUpdateModal.deleteRecord,
111 | newRecord: nextProps.openStockUpdateModal.newRecord,
112 | modalStyles: newStyles
113 | };
114 | }
115 |
116 | componentDidUpdate() {}
117 |
118 | handleAfterOpenModal() {}
119 |
120 | handleCloseModal({
121 | stockRecord = this.props.stockRecord,
122 | actionCancelled = false
123 | } = {}) {
124 | // set modal state in parent component (app,js) back to closed
125 | this.props.setStockUpdateModalState({
126 | stockRecord,
127 | state: false,
128 | actionCancelled: actionCancelled
129 | });
130 | }
131 |
132 | dataUpdate({
133 | stockRecord = this.props.stockRecord,
134 | updated = {},
135 | commit = false
136 | } = {}) {
137 | if (commit) {
138 | // close modal (makes new api request for updated data following update)
139 | this.handleCloseModal({ stockRecord, actionCancelled: false });
140 | } else {
141 | // called from update modal table, so update just the updateData values, pending eventual api call
142 | Object.assign(stockRecord.data.updateData, { ...updated });
143 | this.props.setStockRecordState({ newStockRecord: stockRecord });
144 | }
145 | }
146 |
147 | handleRecordUpdate({
148 | adminUpdate = false,
149 | accountMode = this.props.accountModes.WAREHOUSE,
150 | accountModes = this.props.accountModes,
151 | newRecord = false,
152 | deleteRecord = false
153 | } = {}) {
154 | /*
155 | method to update or delete record data (cancel if desc or sku fields were empty)
156 | */
157 | const { desc, sku } = this.props.stockRecord.data.updateData;
158 | const storeAccount = accountMode === accountModes.STORE;
159 | // if deleteRecord was passed in props (i.e. delete button on data table was clicked), prioritse that
160 | deleteRecord = this.state.deleteRecord
161 | ? this.state.deleteRecord
162 | : deleteRecord;
163 | /* if not admin, load the truck for transfer, rather than submit an API request */
164 | if (!adminUpdate && !storeAccount) {
165 | this.props.loadTruck({ cargo: this.props.stockRecord.data.updateData });
166 | this.handleCloseModal();
167 | } else {
168 | /*Administrative update */
169 | let stockRecord = JSON.parse(JSON.stringify(this.props.stockRecord)); // make a clone in case pre-request changes made
170 | /* these fields required to have a value unless deleting record (or just transferring) */
171 | let reqFieldsComplete = !!(
172 | (desc && sku) ||
173 | this.props.stockRecord.meta.deleteRecord
174 | );
175 | // set API mode
176 | let apiMode = null;
177 | if (!storeAccount) {
178 | // set API mode for warehouse admin
179 | if (newRecord) {
180 | apiMode = this.props.apiOptions.ADD_STOCK;
181 | } else if (deleteRecord && adminUpdate) {
182 | apiMode = this.props.apiOptions.DELETE_STOCK_LINE;
183 | } else {
184 | apiMode = this.props.apiOptions.PATCH_STOCK;
185 | }
186 | } else {
187 | // st API mode for store accounts admin
188 | if (newRecord) {
189 | apiMode = this.props.apiOptions.ADD_ACCOUNT_STOCK;
190 | } else if (deleteRecord && adminUpdate) {
191 | apiMode = this.props.apiOptions.DELETE_ACCOUNT_STOCK_LINE;
192 | } else {
193 | apiMode = this.props.apiOptions.PATCH_ACCOUNT_STOCK;
194 | }
195 | }
196 | // Hit the api if required fields are complete
197 | if (reqFieldsComplete) {
198 | const apiRequest = processRequest({
199 | stockRecord: stockRecord,
200 | apiMode: apiMode
201 | });
202 | stockRecord = null; // delete the clone from memory
203 | if (apiRequest) {
204 | apiRequest
205 | .then(response => {
206 | if (response) {
207 | if (response.status === 200) {
208 | this.props.setMessage({
209 | message: "Records successfully updated!",
210 | messageClass: "alert alert-success"
211 | });
212 | } else if (response.status === 201) {
213 | this.props.setMessage({
214 | message: "Records successfully added!",
215 | messageClass: "alert alert-success"
216 | });
217 | } else if (response.status === 204) {
218 | this.props.setMessage({
219 | message: "Record deleted!",
220 | messageClass: "alert alert-success"
221 | });
222 | }
223 | }
224 | // update the main table with the new values
225 | this.dataUpdate({ updated: response.data, commit: true });
226 | })
227 | .catch(error => {
228 | console.log(error);
229 | this.props.setMessage({
230 | message: "An API error has occurred",
231 | messageClass: "alert alert-danger"
232 | });
233 | this.handleCloseModal({ actionCancelled: !reqFieldsComplete }); // close modal
234 | });
235 | }
236 | } else {
237 | console.log(
238 | "Record update was not committed and no API call was made!"
239 | );
240 | }
241 | }
242 | }
243 |
244 | deletion() {
245 | return (
246 |
255 | );
256 | }
257 |
258 | tables() {
259 | if (this.props.accountMode === this.props.accountModes.WAREHOUSE) {
260 | return (
261 |
262 |
273 |
274 | );
275 | } else if (this.props.accountMode === this.props.accountModes.STORE) {
276 | return (
277 |
278 |
289 |
290 | );
291 | }
292 | }
293 |
294 | render() {
295 | return (
296 |
304 |
305 |
306 |
307 |
(this.subtitle = subtitle)}>Manage Stock
308 |
309 |
310 | {
313 | this.handleCloseModal({ actionCancelled: true });
314 | }}
315 | >
316 | Cancel
317 |
318 |
319 |
320 |
321 | {this.state.deleteRecord ? this.deletion() : this.tables()}
322 |
323 |
324 |
325 | );
326 | }
327 | }
328 |
329 | export default StockUpdateModal;
330 |
--------------------------------------------------------------------------------
/assets/js/index.jsx:
--------------------------------------------------------------------------------
1 | /* react */
2 | import React, {Component} from 'react';
3 | import ReactDOM from 'react-dom';
4 | /* environment settings */
5 | import Env from './env.jsx';
6 | /* app components */
7 | import LoginForm from './loginform.jsx';
8 | import DataTable from './data-table.jsx';
9 | import StockUpdateModal from './stock-update-modal.jsx';
10 | import ApiRequest from './api-request.jsx';
11 | import Message from './message.jsx';
12 | import Footer from './footer.jsx';
13 | /* cookies */
14 | import Cookies from 'js-cookie';
15 |
16 | /* css */
17 | import '../css/index.css';
18 | /* font awesome icons */
19 | import {library} from '@fortawesome/fontawesome-svg-core'
20 | import {faSyncAlt, faEllipsisH, faPlus, faTrashAlt, faEdit} from '@fortawesome/free-solid-svg-icons'
21 |
22 | library.add(faSyncAlt, faEllipsisH, faPlus, faTrashAlt, faEdit);
23 | /* axios */
24 | import axios from 'axios/index';
25 |
26 | axios.defaults.withCredentials = true;
27 |
28 | /* App root */
29 | class App extends React.Component {
30 |
31 | static APP_DETAILS = Env().appDetails;
32 |
33 | static API_OPTIONS =
34 | {
35 | /* used to define available API options in the api-request component */
36 | GET_STOCK: {method: 'GET', desc: 'request to get stock data'},
37 | PATCH_STOCK: {method: 'PATCH', desc: 'PATCH request to update stock data'},
38 | ADD_STOCK: {method: 'POST', desc: 'POST request to add stock data'},
39 | DELETE_STOCK_LINE: {method: 'DELETE', desc: 'DELETE request to delete stock line'},
40 | POST_AUTH: {method: 'POST', desc: 'POST request to for authorization'},
41 | PATCH_CHANGE_PW: {method: 'PATCH', desc: 'PATCH request to for changing password'},
42 | };
43 |
44 | static defaultStockRecordData = () => {
45 | return {data: {results: []}}
46 | };
47 |
48 | static defaultStockRecordMeta = () => {
49 | return {
50 | page: 1,
51 | limit: 25,
52 | pagerMainSize: 5,
53 | pagerEndSize: 3,
54 | orderBy: 'sku',
55 | orderDir: '-',
56 | preserveOrder: false,
57 | cacheControl: 'no-cache', // no caching by default, so always returns fresh data
58 | url: null,
59 | userIsAdmin: false,
60 | search: ''
61 | }
62 | };
63 |
64 | static defaultStockUpdateData = () => {
65 | return {
66 | units_total: 0,
67 | unit_price: 0.00,
68 | desc: '',
69 | sku: ''
70 | }
71 | };
72 |
73 | static defaultStockUpdateMeta = () => {
74 | return {
75 | cacheControl: 'no-cache', // no caching by default, so always returns fresh data
76 | url: null,
77 | userIsAdmin: false,
78 | newRecord: false,
79 | deleteRecord: false,
80 | }
81 | };
82 |
83 | static defaultAuthMeta = () => {
84 | return {
85 | url: `${Env().apiRoot}/api-token-auth/`,
86 | change_pw_url: `${Env().apiRoot}/v1/change-password/`,
87 | authenticated: Boolean(sessionStorage.getItem('apiToken')),
88 | cacheControl: 'no-cache',
89 | requestData: null,
90 | }
91 | };
92 |
93 |
94 | // constructor fires before component mounted
95 | constructor(props) {
96 | super(props); // makes 'this' refer to component (i.e. like python self)
97 | // set local state
98 | this.state = {
99 | // only initialise stockData with required defaults for 1st request and
100 | // BEFORE data returned. Rest all added in call to stockDataHandler called after response received
101 | stockRecordData: App.defaultStockRecordData(),
102 | stockRecordMeta: App.defaultStockRecordMeta(),
103 | stockUpdateData: App.defaultStockUpdateData(),
104 | stockUpdateMeta: App.defaultStockUpdateMeta(),
105 | authMeta: App.defaultAuthMeta(),
106 | stockUpdateModalOpen: false,
107 | apiTrigger: null, // will be one of API_OPTIONS when triggered
108 | message: null,
109 | messageClass: '',
110 | greeting: App.APP_DETAILS.greeting,
111 | csrfToken: this.getCSRFToken()
112 | };
113 | }
114 |
115 | getCSRFToken = () => {
116 | return Cookies.get('csrftoken')
117 | };
118 |
119 | setSessionStorage = ({key, value}) => {
120 | sessionStorage.setItem(key, value);
121 | };
122 |
123 | getSessionStorage = (key) => {
124 | //return JSON.parse(localStorage.getItem(key));
125 | return sessionStorage.getItem(key);
126 | };
127 |
128 | deleteSessionStorage = (keys = []) => {
129 | if (keys.length > 0) {
130 | keys.forEach((k) => {
131 | sessionStorage.removeItem(k)
132 | })
133 | }
134 | return true;
135 | };
136 |
137 |
138 | authenticate = ({apiTrigger = null, requestData = null, auth = false} = {}) => {
139 | // ensure token deleted if auth false
140 | !auth ? this.deleteSessionStorage(['apiToken', 'username']) : null;
141 | // triggers API to get auth token
142 | this.setState({
143 | apiTrigger: apiTrigger,
144 | authMeta: {
145 | url: this.state.authMeta.url,
146 | change_pw_url: this.state.authMeta.change_pw_url,
147 | authenticated: auth,
148 | requestData: requestData
149 | }
150 | })
151 | };
152 |
153 | resetUpdateDataState = () => {
154 | this.setState({
155 | stockUpdateData: App.defaultStockUpdateData(),
156 | stockUpdateMeta: App.defaultStockUpdateMeta(),
157 | })
158 | };
159 |
160 | setStockRecordState = ({stockRecord, updatedData, apiTrigger = null} = {}) => {
161 | /*
162 | method to update state for record being retrieved (GET request)
163 | */
164 | let metaState = this.state.stockRecordMeta;
165 | let recordState = stockRecord && stockRecord.data ? stockRecord.data : this.state.stockRecordData;
166 | //let metaState = this.state.stockRecordMeta;
167 | // record response data
168 | if (updatedData) { // update just fields of the record being updated
169 | Object.entries(recordState.data.results).forEach((r, index) => {
170 | if (r[1]['id'] === updatedData['id']) {
171 | recordState['data']['results'][index] = updatedData
172 | }
173 | });
174 | metaState['datetime_of_request'] = updatedData['datetime_of_request'];
175 | metaState['userIsAdmin'] = updatedData['user_is_admin'];
176 | this.setMessage({message: 'Record updated!', messageClass: 'alert alert-success'})
177 | }
178 | if (stockRecord) {
179 | /*update record request meta (sets new request URLs, etc) */
180 | Object.entries(stockRecord.meta ? stockRecord.meta : metaState).forEach((kvArray) => {
181 | metaState[kvArray[0]] = kvArray[1];
182 | });
183 | if (recordState['data']['results'].length) {
184 | // add time of request from recordState to metaState
185 | metaState['datetime_of_request'] = recordState['data']['results'][0]['datetime_of_request'];
186 | // add user admin status from recordState to metaState
187 | metaState['userIsAdmin'] = recordState['data']['results'][0]['user_is_admin'];
188 | }
189 | }
190 | // ensure page never falls below 1!
191 | metaState.page < 1 ? metaState.page = 1 : metaState.page;
192 | // set new state, then once done (as callback) reset state data to defaults
193 | this.setState({
194 | stockRecordData: recordState, stockRecordMeta: metaState,
195 | apiTrigger: apiTrigger,
196 | }, this.resetUpdateDataState());
197 | };
198 |
199 | setStockUpdateRecordState = ({record = null, apiTrigger = null, clearData = false} = {}) => {
200 | /*
201 | method to update the state for the record being edited (PATCH request), or add
202 | new record
203 | */
204 | clearData ? this.setState({stockUpdateData: App.defaultStockUpdateData()}) : null; // clear updateData if requested
205 | let recordState = this.state.stockUpdateData;
206 | let metaState = this.state.stockUpdateMeta;
207 | let admin = false;
208 | Object.entries(record && record.data ? record.data : recordState).forEach((kvArray) => {
209 | /*add data to record (removing unnecessary elements & assigning
210 | user admin status to stockUpdateMeta instead */
211 | kvArray[0] === 'user_is_admin' ? admin = kvArray[1] : null;
212 | recordState[kvArray[0]] = kvArray[1];
213 | });
214 | Object.entries(record && record.meta ? record.meta : metaState).forEach((kvArray) => {
215 | // add new meta to record
216 | metaState[kvArray[0]] = kvArray[1];
217 | });
218 | metaState['userIsAdmin'] = admin;
219 | this.setState({
220 | stockUpdateMeta: metaState,
221 | stockUpdateData: recordState,
222 | apiTrigger: apiTrigger
223 | });
224 | };
225 |
226 |
227 | openStockUpdateModalHandler = ({record = this.state.stockUpdateData} = {}) => {
228 | /*
229 | method to open the stock addition/update modal & send incoming record-to-be-updated
230 | to the setStockUpdateRecordState method (to update the state!)
231 | */
232 | this.setStockUpdateRecordState({record: record});
233 | this.setStockUpdateModalState({state: true});
234 | };
235 |
236 | setStockUpdateModalState = ({state = false, actionCancelled = false} = {}) => {
237 | /*
238 | method to update the state for the modal open/close state and
239 | if action was cancelled, RESET stockUpdateData & stockUpdateMeta to default values
240 | */
241 | this.setState({stockUpdateModalOpen: state});
242 | actionCancelled ? this.resetUpdateDataState() : null;
243 | };
244 |
245 | disarmAPI = () => {
246 | // disables API active state
247 | this.setState({apiTrigger: null})
248 | };
249 |
250 | setMessage = ({message = null, messageClass = ''} = {}) => {
251 | this.setState({message: message, messageClass: messageClass});
252 | };
253 |
254 | render() {
255 | let dataTable;
256 | if (this.state.authMeta.authenticated) {
257 | dataTable = (
258 |
268 | )
269 | }
270 | return (
271 |
272 |
273 |
274 |
275 |
283 |
286 | {dataTable}
287 |
291 |
300 |
316 |
317 |
318 |
319 |
320 | );
321 | };
322 | }
323 |
324 | ReactDOM.render(
325 | , document.getElementById('react-app'));
--------------------------------------------------------------------------------
/assets/js/stock-update-modal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../css/stock-update-modal.css';
3 | import Modal from "react-modal";
4 | import Env from './env.jsx';
5 |
6 | Modal.setAppElement('#react-app');
7 |
8 | const REGULAR_STYLES = {
9 | top: '50%',
10 | left: '50%',
11 | right: 'auto',
12 | bottom: 'auto',
13 | marginRight: '-50%',
14 | transform: 'translate(-50%, -50%)',
15 | backgroundColor: '#001e00',
16 | color: 'yellow',
17 | border: '1px solid yellow',
18 | borderRadius: '7px 7px 7px 7px',
19 | boxShadow: '-7px -7px 17px 7px #001e00',
20 | maxWidth: '800px'
21 | };
22 |
23 | const DANGER_STYLES = {
24 | top: '50%',
25 | left: '50%',
26 | right: 'auto',
27 | bottom: 'auto',
28 | marginRight: '-50%',
29 | transform: 'translate(-50%, -50%)',
30 | backgroundColor: 'darkred',
31 | color: 'white',
32 | border: '1px solid yellow',
33 | borderRadius: '7px 7px 7px 7px',
34 | boxShadow: '-7px -7px 17px 7px #001e00',
35 | maxWidth: '800px'
36 | };
37 |
38 | class StockUpdateModal extends React.Component {
39 |
40 | constructor(props) {
41 | super(props);
42 | // Remember! This binding is necessary to make `this` work in the callback
43 | // keep real-time props changes in component state
44 | this.state = {
45 | stockUpdateMeta: this.props.stockUpdateMeta,
46 | modalIsOpen: false,
47 | id: null,
48 | units_total: 0,
49 | unit_price: 0.00,
50 | desc: '',
51 | sku: '',
52 | unitsToTransfer: 0,
53 | newRecord: false,
54 | deleteRecord: false,
55 | modalStyles: {content: REGULAR_STYLES}
56 | };
57 | this.handleOpenModal = this.handleOpenModal.bind(this);
58 | this.handleAfterOpenModal = this.handleAfterOpenModal.bind(this);
59 | this.handleCloseModal = this.handleCloseModal.bind(this);
60 | }
61 |
62 | componentWillMount() {
63 | }
64 |
65 | componentWillUnmount() {
66 | }
67 |
68 |
69 | componentWillReceiveProps(nextProps) {
70 | this.setState({
71 | stockUpdateMeta: nextProps.stockUpdateMeta,
72 | id: nextProps.stockUpdateData.id,
73 | units_total: nextProps.stockUpdateData.units_total,
74 | unit_price: nextProps.stockUpdateData.unit_price,
75 | sku: nextProps.stockUpdateData.sku,
76 | desc: nextProps.stockUpdateData.desc,
77 | newRecord: nextProps.stockUpdateMeta.newRecord,
78 | deleteRecord: nextProps.stockUpdateMeta.deleteRecord
79 | });
80 | nextProps.stockUpdateMeta.deleteRecord ? this.state.modalStyles.content = DANGER_STYLES :
81 | this.state.modalStyles.content = REGULAR_STYLES;
82 | // if signal to open modal, open it
83 | if (nextProps.openStockUpdateModal) {
84 | this.handleOpenModal();
85 | }
86 | }
87 |
88 | handleOpenModal() {
89 | this.setState({modalIsOpen: true});
90 | }
91 |
92 | handleAfterOpenModal() {
93 | // this.subtitle.style.color = 'yellow';
94 | }
95 |
96 | handleCloseModal({actionCancelled = false} = {}) {
97 | // first clear temp values from component state
98 | this.setState({
99 | id: null,
100 | units_total: 0,
101 | unit_price: 0.00,
102 | sku: '',
103 | desc: '',
104 | unitsToTransfer: 0,
105 | stockUpdateMeta: null,
106 | newRecord: false,
107 | deleteRecord: false,
108 | modalIsOpen: false // close modal,
109 | });
110 | // set modal state in index.jsx back to closed
111 | this.props.setStockUpdateModalState({state: false, actionCancelled: actionCancelled});
112 | }
113 |
114 | handleRecordUpdate() {
115 | /*
116 | method to update record data (cancel if desc or sku fields were empty)
117 | */
118 | let allFieldsComplete = !!(this.state.desc && this.state.sku);
119 | if (allFieldsComplete) {
120 | this.props.setStockUpdateRecordState({
121 | record: {
122 | data: {
123 | units_total: this.state.units_total,
124 | unit_price: this.state.unit_price,
125 | sku: this.state.sku,
126 | desc: this.state.desc
127 | },
128 | meta: {
129 | url: `${Env().apiDataRoot}/stock/`
130 | }
131 | },
132 | apiTrigger: this.state.newRecord ?
133 | this.props.API_OPTIONS.ADD_STOCK : this.props.API_OPTIONS.PATCH_STOCK
134 | });
135 | }
136 | this.handleCloseModal({actionCancelled: !allFieldsComplete });
137 | }
138 |
139 | handleRecordDelete() {
140 | /*
141 | method to handle deletion of a stock line (record)
142 | */
143 | this.props.setStockUpdateRecordState({
144 | record: {
145 | meta: {
146 | url: `${Env().apiDataRoot}/stock/${this.state.id}`
147 | }
148 | },
149 | apiTrigger: this.props.API_OPTIONS.DELETE_STOCK_LINE
150 | });
151 | this.handleCloseModal();
152 | }
153 |
154 |
155 | validatePrice(value) {
156 | return (/^[\d]*[\.]?[\d]{0,2}$/.test(value)) ? value : this.state.unit_price
157 | }
158 |
159 | validateDesc(value) {
160 | return (/^[a-zA-Z\d.\- ]*$/.test(value)) ? value : this.state.desc
161 | }
162 |
163 | generateEditForm() {
164 | if (this.state.stockUpdateMeta.deleteRecord) {
165 | return (
166 |
167 |
168 |
Delete "{this.state.desc}"
169 |
187 |
188 |
189 | )
190 | } else if (this.state.stockUpdateMeta.userIsAdmin || this.state.stockUpdateMeta.newRecord) {
191 | // note: if newRecord, no userIsAdmin state has been set, hence need to test for newRecord separately
192 | return (
193 |
194 |
251 |
252 |
253 |
254 |
255 | {
257 | e.preventDefault();
258 | this.handleRecordUpdate()
259 | }}
260 | className={'btn btn-warning'}>
261 | {this.state.newRecord ? 'New Stock Item' : 'Edit Stock Record'}
262 |
263 |
264 |
265 |
266 |
267 |
268 | )
269 | }
270 | else {
271 | return (
272 |
300 | )
301 | }
302 | }
303 |
304 | render() {
305 | if (this.state.stockUpdateMeta) {
306 | return (
307 |
314 |
315 |
316 |
317 |
this.subtitle = subtitle}>Manage Stock
318 |
319 |
320 | {
322 | this.handleCloseModal({actionCancelled: true})
323 | }}>Cancel
324 |
325 |
326 |
327 |
328 |
329 | {this.generateEditForm()}
330 |
331 |
332 |
333 |
334 | );
335 | }
336 | else {
337 | return (
338 |
339 | )
340 | }
341 | }
342 | }
343 |
344 | export default StockUpdateModal;
--------------------------------------------------------------------------------
/src/api-request.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import axios from 'axios/index';
3 |
4 | class ApiRequest extends React.Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | // Remember! This binding is necessary to make `this` work in the callback
9 | // keep real-time props changes in component state
10 | this.setFormApi = this.setFormApi.bind(this);
11 | this.state = {
12 | activeRequest: false,
13 | currentOrderBy: '',
14 | currentOrderDir: '',
15 | currentPage: 1,
16 | };
17 | }
18 |
19 | componentWillMount() {
20 | }
21 |
22 | componentWillUnmount() {
23 | }
24 |
25 | componentWillReceiveProps(nextProps) {
26 | let orderDir, url;
27 | let getStockMeta = nextProps.stockRecordMeta;
28 | let updateStockMeta = nextProps.stockUpdateMeta;
29 | let updateStockData = nextProps.stockUpdateData;
30 | let authMeta = nextProps.authMeta;
31 | let csrfToken = nextProps.csrfToken;
32 | switch (nextProps.apiTrigger) {
33 | case this.props.apiOptions.GET_STOCK: // get stock data
34 | orderDir = this.generateRecordRequestOrder(getStockMeta);
35 | url = getStockMeta.url ? getStockMeta.url :
36 | `${process.env.REACT_APP_API_DATA_ROUTE}/stock/?limit=${getStockMeta.limit}` +
37 | `&offset=${(getStockMeta.page * getStockMeta.limit) - getStockMeta.limit}` +
38 | `&order_by=${orderDir}${getStockMeta.orderBy}&desc=${getStockMeta.search}`;
39 | this.props.setStockRecordState({
40 | stockRecord: {meta: {orderDir: orderDir, url: null}}
41 | });// set new order dir state
42 | this.sendRequest({
43 | url: url,
44 | csrfToken: csrfToken,
45 | requestType: this.props.apiOptions.GET_STOCK,
46 | requestMeta: getStockMeta
47 | });
48 | break;
49 | case this.props.apiOptions.PATCH_STOCK: // update stock data
50 | url = updateStockData ?
51 | `${process.env.REACT_APP_API_DATA_ROUTE}/stock/${updateStockData.id}/` :
52 | updateStockMeta.url;
53 | this.sendRequest({
54 | url: url,
55 | csrfToken: csrfToken,
56 | requestData: updateStockData,
57 | requestType: this.props.apiOptions.PATCH_STOCK,
58 | requestMeta: updateStockMeta
59 | });
60 | break;
61 | case this.props.apiOptions.ADD_STOCK: // update stock data
62 | url = updateStockMeta.url;
63 | this.sendRequest({
64 | url: url,
65 | csrfToken: csrfToken,
66 | requestData: updateStockData,
67 | requestType: this.props.apiOptions.ADD_STOCK,
68 | requestMeta: updateStockMeta
69 | });
70 | break;
71 | case this.props.apiOptions.DELETE_STOCK_LINE: // delete stock line
72 | url = updateStockData ? `${process.env.REACT_APP_API_DATA_ROUTE}/stock/${updateStockData.id}/` :
73 | null;
74 | this.sendRequest({
75 | url: url,
76 | csrfToken: csrfToken,
77 | requestData: updateStockData,
78 | requestType: this.props.apiOptions.DELETE_STOCK_LINE,
79 | requestMeta: updateStockMeta
80 | });
81 | break;
82 | case this.props.apiOptions.POST_AUTH: // get auth token
83 | url = authMeta.url;
84 | this.sendRequest({
85 | url: url,
86 | csrfToken: csrfToken,
87 | requestType: this.props.apiOptions.POST_AUTH,
88 | requestData: {
89 | username: authMeta.requestData.data.username,
90 | password: authMeta.requestData.data.password
91 | }
92 | });
93 | break;
94 | case this.props.apiOptions.PATCH_CHANGE_PW: // change password
95 | url = `${authMeta.change_pw_url}${authMeta.requestData.data.username}/`;
96 | this.sendRequest({
97 | url: url,
98 | csrfToken: csrfToken,
99 | requestType: this.props.apiOptions.PATCH_CHANGE_PW,
100 | requestData: {
101 | old_password: authMeta.requestData.data.old_password,
102 | new_password: authMeta.requestData.data.new_password
103 | }
104 | });
105 | break;
106 | default:
107 | // console.log('Not calling API, nothing to do ...')
108 | }
109 | }
110 |
111 | setFormApi(formApi) {
112 | this.formApi = formApi;
113 | }
114 |
115 | generateRecordRequestOrder(requestMeta) {
116 | /*
117 | Method to generate the request ordering.
118 | Reverses ordering if column clicked when
119 | already ordered on the same column.
120 | Otherwise, defaults to ascending order.
121 |
122 | Note: page always reverts to page 1 IF column clicked to change order on any
123 | page other than 1, as new ordering is requested from API. Needs to return to page 1 to
124 | display the newly requested (differently sorted) pages -
125 | otherwise the current page would display any data that happened to correspond to that
126 | page of newly received data, and that wouldn't be expected behaviour.
127 | Could also add method (& UI link/button/widget) for columns to LOCALLY order/sort presently
128 | displayed page data (without an API request) if required.
129 | */
130 | if (requestMeta.preserveOrder) return requestMeta.orderDir; // end here if current order to be preserved
131 | if (this.state.currentOrderBy !== requestMeta.orderBy) {
132 | this.setState({
133 | currentOrderDir: '',
134 | currentOrderBy: requestMeta.orderBy,
135 | });
136 | return ''
137 | }
138 | if (this.state.currentPage === requestMeta.page) {
139 | let newOrderDir = this.state.currentOrderDir === '-' ? '' : "-";
140 | this.setState({
141 | currentOrderDir: newOrderDir
142 | });
143 | return newOrderDir;
144 | } else {
145 | this.setState({
146 | currentPage: requestMeta.page
147 | });
148 | return this.state.currentOrderDir
149 | }
150 | }
151 |
152 | sendRequest({url = null, requestData = null, requestMeta = null, requestType = null, csrfToken = null} = {}) {
153 | this.props.disarmAPI(); // disarm the API now the command has been actioned
154 | if (!this.state.activeRequest && url && requestType) {
155 | this.setState({
156 | activeRequest: true,
157 | });
158 | // make request
159 | //console.log('Sending request to API');
160 | axios({
161 | method: requestType.method,
162 | url: url,
163 | responseType: 'json',
164 | data: requestData ? requestData : {},
165 | //auth: {},
166 | headers: {
167 | Authorization: this.props.getSessionStorage('apiToken') ?
168 | `Token ${this.props.getSessionStorage('apiToken')}` : null,
169 | // 'cache-control': requestMeta ? requestMeta.cacheControl : 'no-cache',
170 | 'Content-Type': 'application/json',
171 | 'X-CSRFToken': csrfToken,
172 | } // additional headers here
173 | }).then((response) => {
174 | this.setState({activeRequest: false});
175 | this.handleAPIResponse({
176 | requestData: requestData, requestType: requestType,
177 | response: response
178 | });
179 | }).catch(error => {
180 | this.setState({activeRequest: false});
181 | this.handleAPIResponse({error: error, requestType: requestType});
182 | });
183 | } else {
184 | //console.log('Request already active or no URL set!')
185 | }
186 | }
187 |
188 | handleAPIResponse({requestData = null, requestType = null, response = null, error = null} = {}) {
189 | if (error) {
190 | switch (requestType) {
191 | case this.props.apiOptions.GET_STOCK:
192 | this.props.setMessage({
193 | message: error.response ? String(error.response.data[0]) : String(error),
194 | messageClass: 'alert alert-danger'
195 | });
196 | break;
197 | case this.props.apiOptions.PATCH_STOCK:
198 | case this.props.apiOptions.ADD_STOCK:
199 | case this.props.apiOptions.DELETE_STOCK_LINE:
200 | let msgArray = [];
201 | this.props.setStockUpdateRecordState({clearData: true}); // clear data for next time
202 | if (error.response && error.response.data) {
203 | Object.keys(error.response.data).forEach((key) => msgArray.push(error.response.data[key]));
204 | this.props.setMessage({
205 | message: msgArray.length >= 0 ? msgArray.join(', ') : error,
206 | messageClass: 'alert alert-danger'
207 | });
208 | } else {
209 | this.props.setMessage({
210 | message: 'An api error occurred!',
211 | messageClass: 'alert alert-danger'
212 | });
213 | }
214 | break;
215 | case this.props.apiOptions.POST_AUTH:
216 | case this.props.apiOptions.PATCH_CHANGE_PW:
217 | let message = 'An error occurred! ';
218 | if (error.response) {
219 | if (error.response.data) {
220 | if (error.response.data.old_password) {
221 | message += `Old password error: ${error.response.data.old_password} `;
222 | }
223 | if (error.response.data.new_password) {
224 | message += `New password error: ${error.response.data.new_password} `;
225 | }
226 | } else if (error.response.detail) {
227 | message += `Error detail: ${error.response.detail} `;
228 | }
229 | }
230 | else {
231 | message += error
232 | }
233 | this.props.setMessage({
234 | message: message,
235 | messageClass: 'alert alert-danger'
236 | });
237 | this.props.authenticate({auth: false}); // ensure unauthenticated status is set
238 | break;
239 | default:
240 | this.props.setMessage({
241 | message: 'An api error occurred!',
242 | messageClass: 'alert alert-danger'
243 | });
244 | }
245 | } else {
246 | switch (requestType) {
247 | case this.props.apiOptions.GET_STOCK:
248 | this.props.setStockRecordState({
249 | stockRecord: {data: response, meta: {preserveOrder: false}}
250 | });
251 | break;
252 | case this.props.apiOptions.PATCH_STOCK:
253 | this.props.setStockRecordState({
254 | updatedData: response.data
255 | });
256 | this.props.setStockUpdateRecordState({clearData: true}); // clear data for next time
257 | this.props.setMessage({
258 | message: 'Stock action successful!',
259 | messageClass: 'alert alert-success'
260 | });
261 | break;
262 | case this.props.apiOptions.ADD_STOCK:
263 | case this.props.apiOptions.DELETE_STOCK_LINE:
264 | this.props.setStockUpdateRecordState({clearData: true}); // clear data for next time
265 | this.props.setStockRecordState({
266 | stockRecord: {meta: {preserveOrder: true}},
267 | apiTrigger: this.props.apiOptions.GET_STOCK
268 | }); // sets data to be refreshed & rebuild of table
269 | this.props.setMessage({
270 | message: 'Stock action successful!',
271 | messageClass: 'alert alert-success'
272 | });
273 | break;
274 | case this.props.apiOptions.POST_AUTH:
275 | if (response.data) {
276 | let token = response.data.token;
277 | this.props.setSessionStorage({key: 'apiToken', value: token}); // stick token into local browser storage
278 | this.props.setSessionStorage({key: 'username', value: requestData.username}); // store username for good measure
279 | this.props.authenticate({auth: true}); // set authenticated status
280 | this.props.setMessage({
281 | message: 'Authentication successful',
282 | messageClass: 'alert alert-success'
283 | });
284 | }
285 | else {
286 | this.props.setMessage({
287 | message: 'An error occurred whilst connecting to the API.',
288 | messageClass: 'alert alert-danger'
289 | })
290 | }
291 | break;
292 | case this.props.apiOptions.PATCH_CHANGE_PW:
293 | if (response.data) {
294 | this.props.authenticate({auth: false}); // log out
295 | this.props.setMessage({
296 | message: 'Password changed! Please login using your new credentials.',
297 | messageClass: 'alert alert-success'
298 | })
299 | } else {
300 | this.props.setMessage({
301 | message: 'An error occurred whilst connecting to the API.',
302 | messageClass: 'alert alert-danger'
303 | })
304 | }
305 | break;
306 | default:
307 | this.props.setMessage({
308 | message: 'API action has completed ...',
309 | messageClass: 'alert alert-success'
310 | })
311 | }
312 | }
313 | }
314 |
315 | /*
316 | render stuff ...
317 | */
318 |
319 | renderDefault() {
320 | return (
321 |
322 | )
323 | }
324 |
325 | render() {
326 | return (
327 | {this.renderDefault()}
328 | );
329 | }
330 | }
331 |
332 | export default ApiRequest;
--------------------------------------------------------------------------------