├── .env-example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── index.html ├── manifest.json └── robots.txt └── src ├── App.js ├── actions ├── discountActions.js ├── itemActions.js ├── kiloActions.js ├── pendingItemsActions.js ├── priceActions.js ├── quantityActions.js ├── sacksActions.js ├── searchResultsActions.js └── suppliersActions.js ├── api ├── Api.js └── models │ ├── ModelStocks.js │ ├── ModelTransactionItem.js │ ├── ModelTransactions.js │ └── index.js ├── assets ├── css │ └── global-style.css ├── icons │ ├── close.svg │ ├── delete.svg │ ├── exclamation-mark.svg │ ├── inventory.svg │ ├── left-arrow.svg │ ├── loop.svg │ ├── magnify.svg │ ├── money.svg │ └── tick.svg └── images │ ├── mockup.png │ ├── profile_female.jpg │ └── profile_male.png ├── components ├── Card │ ├── Card.css │ ├── Card.js │ ├── CardHover.css │ ├── CardProfile.js │ └── index.js ├── Form │ ├── Form.spec.js │ ├── FormButton.css │ ├── FormButton.js │ ├── FormDetailText.js │ ├── FormGroup.css │ ├── FormGroupInventoryQuantity.js │ ├── FormGroupInventorySack.js │ ├── FormGroupSalesKilo.js │ ├── FormGroupSalesQuantity.js │ ├── FormInput.js │ ├── FormSelect.js │ ├── FormStaticText.js │ └── index.js ├── Modal │ ├── Modal.spec.js │ ├── ModalConfirm.css │ ├── ModalConfirm.js │ ├── ModalFailed.js │ ├── ModalLoading.css │ ├── ModalLoading.js │ ├── ModalLogin.css │ ├── ModalLogin.js │ ├── ModalSuccess.js │ └── index.js ├── Nav │ ├── TopLeftNav.css │ ├── TopLeftNav.js │ ├── TopRightNav.css │ ├── TopRightNav.js │ └── index.js ├── PendingItem.css ├── PendingItem.js ├── SearchBar │ ├── SearchBarGroup.css │ ├── SearchBarGroup.js │ ├── SearchBarItem.css │ ├── SearchBarItem.js │ └── index.js └── index.js ├── containers ├── MainFormLayoutInventory.js ├── MainFormLayoutSales.js ├── MainLayout.css ├── MainLayout.js ├── MainLayoutNav.js ├── ModalLayout.css ├── ModalLayout.js └── PendingItemsLayout.js ├── context ├── AppContext.js ├── InventoryContext.js └── SalesContext.js ├── enums ├── enumFormTypes.js ├── enumKiloType.js ├── enumPendingItemTypes.js └── enumSubmitConfirmTypes.js ├── index.js ├── pages ├── AbstractPage.js ├── HomePage.js ├── InventoryPage.js ├── SalesPage.js ├── SelectionPage.js └── index.js ├── reducers ├── discountReducers.js ├── itemReducers.js ├── kiloReducers.js ├── pendingItemsReducers.js ├── priceReducers.js ├── quantityReducers.js ├── sacksReducers.js ├── searchResultsReducers.js └── suppliersReducers.js ├── routeConfig.js ├── setupTests.js ├── store.js ├── types.js └── util.js /.env-example: -------------------------------------------------------------------------------- 1 | API_URL="http://localhost:1337" 2 | # development, production 3 | APP_ENV="development" 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | public 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "plugin:react/recommended", 9 | "google" 10 | ], 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react", 20 | "react-hooks" 21 | ], 22 | "rules": { 23 | "react-hooks/rules-of-hooks": "error", 24 | "react-hooks/exhaustive-deps": "warn", 25 | "linebreak-style": "off", 26 | "max-len": "off", 27 | "require-jsdoc": "off", 28 | "no-invalid-this": "off", 29 | "react/prop-types": "off" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea 26 | .eslintcache 27 | .env 28 | debug.log 29 | /public/env.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # POS (Point-of-sales) Inventory System UI 2 | 3 | > POS Inventory System created using ReactJS 4 | 5 | > UI/UX Designed from scratch, curated for non-technical people 6 | 7 | --- 8 | 9 | ![ScreenShot](src/assets/images/mockup.png) 10 | 11 | ## Features 12 | 13 | ### Inventory: 14 | 15 | - Add inventory items 16 | - Add supplier 17 | 18 | ### POS: 19 | 20 | - Add sales items by quantity 21 | - Add sales items by kilo 22 | - Add sales items by sack 23 | - Auto calculate discount and total 24 | 25 | ## Built using: 26 | - ReactJS :heart: 27 | 28 | ## Author 29 | - [Simperfy](https://github.com/Simperfy) :dog: 30 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "C:\\Users\\James\\AppData\\Local\\Temp\\jest", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: 'coverage', 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "\\\\node_modules\\\\" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: "babel", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | testEnvironment: 'node', 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "\\\\node_modules\\\\" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jasmine2", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "\\\\node_modules\\\\", 180 | // "\\.pnp\\.[^\\\\]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | verbose: true, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory-system", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.6", 7 | "@testing-library/react": "^11.2.2", 8 | "@testing-library/user-event": "^12.6.0", 9 | "axios": "^0.21.1", 10 | "bootstrap": "^4.5.3", 11 | "react": "^17.0.1", 12 | "react-bootstrap": "^1.4.0", 13 | "react-dom": "^17.0.1", 14 | "react-dotenv": "^0.1.3", 15 | "react-redux": "^7.2.2", 16 | "react-router-dom": "^5.2.0", 17 | "react-scripts": "4.0.1", 18 | "redux": "^4.0.5", 19 | "redux-thunk": "^2.3.0", 20 | "uuid": "^8.3.2", 21 | "web-vitals": "^0.2.4" 22 | }, 23 | "scripts": { 24 | "start": "react-dotenv && react-scripts start", 25 | "build": "react-dotenv && react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "react-dotenv": { 48 | "whitelist": [ 49 | "API_URL", 50 | "APP_ENV" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "@babel/preset-env": "^7.12.11", 55 | "@babel/preset-react": "^7.12.10", 56 | "@types/react-router-dom": "^5.1.7", 57 | "@wojtekmaj/enzyme-adapter-react-17": "^0.4.1", 58 | "babel-eslint": "^10.1.0", 59 | "babel-jest": "^26.6.3", 60 | "enzyme": "^3.11.0", 61 | "enzyme-adapter-react-16": "^1.15.5", 62 | "eslint": "^7.17.0", 63 | "eslint-config-google": "^0.14.0", 64 | "eslint-plugin-react": "^7.22.0", 65 | "eslint-plugin-react-hooks": "^4.2.0", 66 | "react-test-renderer": "^17.0.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simperfy/pos-inventory-system-ui/4d71e694be24beda0bf526f4609c79004019ff2f/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | PetShop - Home 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "PetShop", 3 | "name": "PetShop POS Inventory System", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'bootstrap/dist/css/bootstrap.min.css'; 4 | import Container from 'react-bootstrap/Container'; 5 | import {BrowserRouter as Router, Route, Switch, Redirect} from 'react-router-dom'; 6 | import {HomePage, InventoryPage, SelectionPage, SalesPage} from './pages'; 7 | import {AppContext} from './context/AppContext'; 8 | import {getRoute} from './routeConfig'; 9 | import Api from './api/Api'; 10 | import env from 'react-dotenv'; 11 | import store from './store'; 12 | import {Provider} from 'react-redux'; 13 | 14 | class App extends React.Component { 15 | _isMounted = false; 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | isReady: false, 21 | isLoggedIn: false, 22 | user: null, 23 | jwt: null, 24 | isDisconnected: false, 25 | }; 26 | 27 | this.style = { 28 | noInternetDiv: {backgroundColor: '#ff8100', width: '100vw', position: 'fixed'}, 29 | noInternetP: {textAlign: 'center', fontSize: '1.5rem', color: 'white', margin: 'auto'}, 30 | }; 31 | } 32 | 33 | componentDidMount() { 34 | this._isMounted = true; 35 | const jwt = localStorage.getItem('jwt'); 36 | if (jwt) this.autoSignIn(jwt); 37 | else this.setState({isReady: true}); 38 | 39 | // @TODO: Don't remove, useful if server is in the cloud 40 | // Handle internet disconnection 41 | /* this.handleConnectionChange(); 42 | window.addEventListener('online', this.handleConnectionChange); 43 | window.addEventListener('offline', this.handleConnectionChange);*/ 44 | 45 | if (env.APP_ENV) { 46 | if (env.APP_ENV === 'development') return; 47 | console.log('pinging'); 48 | console.log(env.APP_ENV); 49 | this.handleLocalServerConnectionPings(); 50 | } 51 | } 52 | 53 | handleLocalServerConnectionPings = () => { 54 | this.serverPing = setInterval(() => { 55 | fetch(env.API_URL) 56 | .then(() => this.setState({isDisconnected: false})) 57 | .catch(() => this.setState({isDisconnected: true})); 58 | }, 3000); 59 | } 60 | 61 | componentWillUnmount() { 62 | this._isMounted = false; 63 | clearInterval(this.serverPing); 64 | 65 | // @TODO: Don't remove, useful if server is in the cloud 66 | // Handle internet disconnection 67 | /* window.removeEventListener('online', this.handleConnectionChange); 68 | window.removeEventListener('offline', this.handleConnectionChange);*/ 69 | } 70 | 71 | // @TODO: Don't remove, useful if server is in the cloud 72 | /* handleConnectionChange = () => { 73 | const condition = navigator.onLine ? 'online' : 'offline'; 74 | if (condition === 'online') { 75 | const webPing = setInterval( 76 | () => { 77 | fetch('//google.com', { 78 | mode: 'no-cors', 79 | }) 80 | .then(() => { 81 | this.setState({ isDisconnected: false }, () => { 82 | return clearInterval(webPing) 83 | }); 84 | }).catch(() => this.setState({ isDisconnected: true }) ) 85 | }, 2000); 86 | return; 87 | } 88 | 89 | return this.setState({ isDisconnected: true }); 90 | }*/ 91 | 92 | autoSignIn = (jwt) => { 93 | Api.getCurrentUser(jwt).then( 94 | ({data}) => { 95 | this._isMounted && 96 | this.setState({ 97 | isReady: true, 98 | isLoggedIn: true, 99 | user: data, 100 | jwt: jwt, 101 | }); 102 | }, 103 | (err) => { 104 | this._isMounted && 105 | this.setState({ 106 | isReady: true, 107 | }); 108 | }, 109 | ); 110 | } 111 | 112 | login = (user, jwt) => { 113 | this.setState({isLoggedIn: true, user: user, jwt: jwt}); 114 | localStorage.setItem('jwt', jwt); 115 | }; 116 | 117 | logout = () => { 118 | this.setState({isLoggedIn: false, user: null, jwt: null}); 119 | localStorage.clear(); 120 | } 121 | 122 | PrivateRoute = ({children, ...rest}) => this.state.isLoggedIn ? children : } />; 123 | 124 | render() { 125 | return this.state.isReady && ( 126 | 127 | 137 | 138 |
139 | { this.state.isDisconnected && ( 140 |
141 |

Server connection lost

142 |
) 143 | } 144 | 145 | 146 | 147 | { !this.state.isLoggedIn ? : } 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |

Invalid link

162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | ); 170 | } 171 | } 172 | 173 | export default App; 174 | -------------------------------------------------------------------------------- /src/actions/discountActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | export const updateDiscountOnInput = (e) => (dispatch, getState) => { 4 | let discount = parseFloat(e.target.value); 5 | if (isNaN(discount)) discount = 0; 6 | 7 | const price = getState().price; 8 | 9 | if (discount >= price) dispatch({type: types.UPDATE_DISCOUNT, payload: {discount: price}}); 10 | 11 | if (price > 0 && discount <= price) dispatch({type: types.UPDATE_DISCOUNT, payload: {discount}}); 12 | }; 13 | 14 | export const resetDiscount = () => (dispatch, getState) => { 15 | dispatch({type: types.RESET_DISCOUNT}); 16 | }; 17 | -------------------------------------------------------------------------------- /src/actions/itemActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | export const updateItemText = (text) => (dispatch, getState) => { 4 | dispatch({type: types.UPDATE_ITEM_TEXT, payload: {text}}); 5 | }; 6 | 7 | export const updateItemBarcode = (barcode) => (dispatch, getState) => { 8 | dispatch({type: types.UPDATE_ITEM_BARCODE, payload: {barcode}}); 9 | }; 10 | 11 | export const updateItemPrice = (price) => (dispatch, getState) => { 12 | dispatch({type: types.UPDATE_ITEM_PRICE, payload: {price}}); 13 | }; 14 | 15 | export const updateItemRemaining = (remaining) => (dispatch, getState) => { 16 | dispatch({type: types.UPDATE_ITEM_REMAINING, payload: {remaining}}); 17 | }; 18 | -------------------------------------------------------------------------------- /src/actions/kiloActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | export const updateKiloOnInput = (e) => (dispatch, getState) => { 4 | const kilo = parseFloat(e.target.value); 5 | 6 | dispatch({type: types.UPDATE_KILO, payload: {kilo}}); 7 | }; 8 | 9 | export const updateKilo = (kilo) => (dispatch, getState) => { 10 | dispatch({type: types.UPDATE_KILO, payload: {kilo}}); 11 | }; 12 | 13 | export const updateKiloBySackId = (sackId) => (dispatch, getState) => { 14 | const kilo = getState().sacksStore.sacks.find((s) => s.id === sackId).value; 15 | 16 | dispatch({type: types.UPDATE_KILO, payload: {kilo}}); 17 | }; 18 | 19 | export const updateKiloOnInputAndPrice = (e) => (dispatch, getState) => { 20 | const kilo = parseFloat(e.target.value); 21 | let price = getState().item.price * kilo; 22 | 23 | if (isNaN(price)) price = 0; 24 | 25 | dispatch({type: types.UPDATE_PRICE, payload: {price}}); 26 | dispatch({type: types.UPDATE_KILO, payload: {kilo}}); 27 | }; 28 | -------------------------------------------------------------------------------- /src/actions/pendingItemsActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | import {v4 as uuidv4} from 'uuid'; 3 | 4 | // for inventory 5 | export const addPendingInventoryItem = (item) => (dispatch, getState) => { 6 | const pendingItems = getState().pending.pendingItems?.slice() || []; 7 | const supplierSelectedId = getState().suppliersStore.supplierSelectedId; 8 | const supplierName = getState().suppliersStore.suppliers.find((s) => s.id === supplierSelectedId).name; 9 | 10 | const isAlreadyAdded = pendingItems.some((pi) => pi.barcode === item.barcode && pi.supplierId === supplierSelectedId); 11 | 12 | if (isAlreadyAdded) return alert(`${item.name} item with same supplier already exist. Please remove duplicate first.`); 13 | 14 | pendingItems.push({ 15 | id: uuidv4(), 16 | name: item.name, 17 | supplierName: supplierName, 18 | supplierId: supplierSelectedId, 19 | quantity: item.quantity, 20 | barcode: item.barcode, 21 | kilo: item.kilo, 22 | }); 23 | 24 | dispatch({type: types.ADD_TO_PENDING_ITEMS, payload: {pendingItems}}); 25 | }; 26 | 27 | // for sales 28 | export const addPendingSalesItem = (item) => (dispatch, getState) => { 29 | const pendingSalesItems = getState().pending.pendingItems?.slice() || []; 30 | 31 | pendingSalesItems.push({ 32 | id: uuidv4(), 33 | name: item.name, 34 | quantity: item.quantity, 35 | barcode: item.barcode, 36 | kilo: item.kilo, 37 | discount: item.discount, 38 | price: item.price, 39 | }); 40 | 41 | dispatch({type: types.ADD_TO_PENDING_SALES_ITEMS, payload: {pendingItems: pendingSalesItems}}); 42 | }; 43 | 44 | export const removeSinglePendingItem = (id) => (dispatch, getState) => { 45 | const pendingItems = getState().pending.pendingItems?.slice().filter((pi) => pi.id !== id); 46 | 47 | dispatch({type: types.REMOVE_SINGLE_PENDING_ITEMS, payload: {pendingItems}}); 48 | }; 49 | 50 | export const removeAllPendingItems = (warn = true) => (dispatch, getState) => { 51 | let confirm = true; 52 | if (warn) confirm = window.confirm('Do you want to remove all items?'); 53 | 54 | if (confirm) dispatch({type: types.REMOVE_ALL_PENDING_ITEMS, payload: {pendingItems: []}}); 55 | }; 56 | -------------------------------------------------------------------------------- /src/actions/priceActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | export const updatePriceOnInput = (e) => (dispatch, getState) => { 4 | const price = parseInt(e.target.value); 5 | 6 | dispatch({type: types.UPDATE_PRICE, payload: {price}}); 7 | }; 8 | 9 | export const updatePrice = (price) => (dispatch, getState) => { 10 | dispatch({type: types.UPDATE_PRICE, payload: {price}}); 11 | }; 12 | 13 | export const updatePriceBySackId = (sackId) => (dispatch, getState) => { 14 | const price = getState().sacksStore.sacks.find((s) => s.id === sackId).price; 15 | 16 | dispatch({type: types.UPDATE_PRICE, payload: {price}}); 17 | }; 18 | -------------------------------------------------------------------------------- /src/actions/quantityActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | import {isValidQuantity} from '../util'; 3 | 4 | export const updateQuantityOnInput = (e, shouldCheckRemaining = true) => (dispatch, getState) => { 5 | const quantity = parseInt(e.target.value); 6 | if (!shouldCheckRemaining) return dispatch({type: types.UPDATE_QUANTITY, payload: {quantity}}); 7 | 8 | const remaining = getState().item.remaining; 9 | 10 | if (isValidQuantity(remaining, quantity)) { 11 | dispatch({type: types.UPDATE_QUANTITY, payload: {quantity}}); 12 | } else { 13 | window.alert('out of stock'); 14 | } 15 | }; 16 | 17 | export const updateQuantity = (quantity) => (dispatch, getState) => { 18 | const remaining = getState().item.remaining; 19 | 20 | if (isValidQuantity(remaining, quantity)) { 21 | dispatch({type: types.UPDATE_QUANTITY, payload: {quantity}}); 22 | } else { 23 | window.alert('out of stock'); 24 | } 25 | }; 26 | 27 | export const resetQuantity = () => (dispatch, getState) => { 28 | dispatch({type: types.RESET_QUANTITY}); 29 | }; 30 | -------------------------------------------------------------------------------- /src/actions/sacksActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | import {mapSacks} from '../util'; 3 | 4 | export const updateSacks = (sacks) => (dispatch, getState) => { 5 | sacks = sacks.map(mapSacks); 6 | 7 | dispatch({type: types.UPDATE_SACKS, payload: {sacks}}); 8 | }; 9 | 10 | export const updateSacksAndKilo = (sacks) => (dispatch, getState) => { 11 | sacks = sacks.map(mapSacks); 12 | const kilo = sacks[0]?.value || 0; 13 | 14 | dispatch({type: types.UPDATE_KILO, payload: {kilo}}); 15 | dispatch({type: types.UPDATE_SACKS, payload: {sacks}}); 16 | }; 17 | 18 | export const updateSackSelectedId = (sackId) => (dispatch, getState) => { 19 | dispatch({type: types.UPDATE_SACK_SELECTED_VALUE, payload: {sackId}}); 20 | }; 21 | 22 | export const updateSackSelectedIdAndKilo = (sackId) => (dispatch, getState) => { 23 | const kilo = getState().sacksStore.sacks.find((s) => s.id === sackId).value; 24 | 25 | dispatch({type: types.UPDATE_KILO, payload: {kilo}}); 26 | dispatch({type: types.UPDATE_SACK_SELECTED_VALUE, payload: {sackId}}); 27 | }; 28 | 29 | export const updateSackSelectedIdWith = (sackId, params) => (dispatch, getState) => { 30 | const sack = getState().sacksStore.sacks.find((s) => s.id === sackId); 31 | 32 | if (params.some((p) => p === 'kilo')) { 33 | const kilo = sack.value; 34 | dispatch({type: types.UPDATE_KILO, payload: {kilo}}); 35 | } 36 | 37 | if (params.some((p) => p === 'price')) { 38 | const price = sack.price; 39 | dispatch({type: types.UPDATE_PRICE, payload: {price}}); 40 | } 41 | 42 | dispatch({type: types.UPDATE_SACK_SELECTED_VALUE, payload: {sackId}}); 43 | }; 44 | -------------------------------------------------------------------------------- /src/actions/searchResultsActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | import {mapSearchResults} from '../util'; 3 | 4 | export const updateSearchResults = (data) => (dispatch, getState) => { 5 | const searchResults = data.length > 0 ? data.slice().map(mapSearchResults) : []; 6 | 7 | dispatch({type: types.UPDATE_SEARCH_RESULTS, payload: {searchResults}}); 8 | }; 9 | -------------------------------------------------------------------------------- /src/actions/suppliersActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | export const updateSuppliers = (suppliers) => (dispatch, getState) => { 4 | suppliers = suppliers.map((s) => ({id: s.id, name: s.supplierName})); 5 | dispatch({type: types.UPDATE_SUPPLIERS, payload: {suppliers}}); 6 | }; 7 | 8 | export const updateSuppliersSelectedIdOnInput = (e) => (dispatch, getState) => { 9 | const supplierSelectedId = e.target.value; 10 | dispatch({type: types.UPDATE_SUPPLIER_SELECTED_ID, payload: {supplierSelectedId}}); 11 | }; 12 | 13 | export const updateSuppliersSelectedId = (supplierSelectedId) => (dispatch, getState) => { 14 | dispatch({type: types.UPDATE_SUPPLIER_SELECTED_ID, payload: {supplierSelectedId: supplierSelectedId.toString()}}); 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/Api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import env from 'react-dotenv'; 3 | 4 | export default class Api { 5 | static async getCurrentUser(jwt) { 6 | return axios.get(`${env.API_URL}/users/me`, { 7 | headers: {Authorization: `Bearer ${jwt}`}, 8 | }); 9 | } 10 | 11 | static async getUsers() { 12 | return axios.get(`${env.API_URL}/users`); 13 | } 14 | 15 | static async getItems(jwt, val) { 16 | const url = `${env.API_URL}/items/find-by-id-or-name?item_name=${val}&id_like=${val}`; 17 | 18 | return axios.get(url, { 19 | headers: {Authorization: `Bearer ${jwt}`}, 20 | }); 21 | } 22 | 23 | /* 24 | static async getItems(jwt, params = {}) { 25 | const url = this.setParams(`${env.API_URL}/items`, params); 26 | 27 | return axios.get(url, { 28 | headers: {Authorization: `Bearer ${jwt}`}, 29 | }); 30 | } 31 | */ 32 | 33 | // static async getItemsByNameOrId(jwt, query) { 34 | // const url = `${env.API_URL}/items?_where[_or][0][item_name_contains]=${query}&_where[_or][1][id_in]=${query}`; 35 | // 36 | // return axios.get(url, { 37 | // headers: { Authorization: `Bearer ${jwt}` }, 38 | // }); 39 | // } 40 | 41 | // static async getSuppliers(jwt) { 42 | // return axios.get(`${env.API_URL}/suppliers`, { 43 | // headers: { Authorization: `Bearer ${jwt}` }, 44 | // }); 45 | // } 46 | 47 | static async get(jwt, url, params) { 48 | const newUrl = this.setParams(`${env.API_URL}${url}`, params); 49 | 50 | return axios.get(newUrl, { 51 | headers: {Authorization: `Bearer ${jwt}`}, 52 | }); 53 | } 54 | 55 | static async create(jwt, url, params) { 56 | const baseURL = env.API_URL; 57 | 58 | return axios.post(baseURL + url, {...params}, { 59 | headers: {Authorization: `Bearer ${jwt}`}, 60 | }); 61 | } 62 | 63 | static async createBatch(jwt, batchData) { 64 | const instance = axios.create({ 65 | baseURL: env.API_URL, 66 | headers: {Authorization: `Bearer ${jwt}`}, 67 | }); 68 | 69 | const instances = [ 70 | // instance.post(data.url, {...data.data}) 71 | ]; 72 | 73 | for (const data of batchData) { 74 | instances.push(instance.post(data.url, {...data.data})); 75 | } 76 | 77 | return axios.all([ 78 | ...instances, 79 | ]); 80 | } 81 | 82 | static setParams = (url, params) => { 83 | url = new URL(url); 84 | // eslint-disable-next-line camelcase 85 | const search_params = url.searchParams; 86 | 87 | for (const key of Object.keys(params)) search_params.set(key, params[key]); 88 | 89 | url.search = search_params.toString(); 90 | 91 | return url.toString(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/api/models/ModelStocks.js: -------------------------------------------------------------------------------- 1 | import Api from '../Api'; 2 | 3 | export default class ModelStocks { 4 | static async getRemaining(jwt, itemNameQuery) { 5 | const params = { 6 | search: itemNameQuery, 7 | }; 8 | 9 | return Api.get(jwt, '/items/remaining', params); 10 | } 11 | 12 | static async createBatch(jwt, userId, pendingItems) { 13 | const batchData = [ 14 | // {url: '/stocks', data: {}} 15 | ]; 16 | 17 | for (const pi of pendingItems) { 18 | batchData.push({ 19 | url: '/stocks', 20 | data: { 21 | // 'kilo': pi.kilo <= 0 ? null : pi.kilo, 22 | 'kilo': null, 23 | 'stock_quantity': pi.quantity, 24 | 'item': pi.barcode, 25 | 'supplier': pi.supplierId, 26 | 'users_permissions_user': userId, 27 | }, 28 | }); 29 | } 30 | 31 | return Api.createBatch(jwt, batchData); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/api/models/ModelTransactionItem.js: -------------------------------------------------------------------------------- 1 | import Api from '../Api'; 2 | 3 | export default class ModelTransactionItem { 4 | static async createBatch(jwt, transactionId, pendingSalesItems) { 5 | const batchData = [ 6 | // {url: '/transaction-items', data: {}} 7 | ]; 8 | 9 | for (const pi of pendingSalesItems) { 10 | batchData.push({ 11 | url: '/transaction-items', 12 | data: { 13 | 'quantity': pi.quantity, 14 | 'discount': pi.discount, 15 | 'subtotal': (pi.price - pi.discount) * pi.quantity, 16 | 'item': pi.barcode, 17 | 'price': pi.price, 18 | 'transaction': transactionId, 19 | }, 20 | }); 21 | } 22 | 23 | return Api.createBatch(jwt, batchData); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/api/models/ModelTransactions.js: -------------------------------------------------------------------------------- 1 | import Api from '../Api'; 2 | 3 | export class ModelTransactions { 4 | static async create(jwt, id) { 5 | const url = '/transactions'; 6 | const params = { 7 | 'subtotal': 0, 8 | 'users_permissions_user': id, 9 | }; 10 | 11 | return Api.create(jwt, url, params); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/api/models/index.js: -------------------------------------------------------------------------------- 1 | import ModelStocks from './ModelStocks'; 2 | 3 | export {ModelStocks}; 4 | -------------------------------------------------------------------------------- /src/assets/css/global-style.css: -------------------------------------------------------------------------------- 1 | /* width */ 2 | ::-webkit-scrollbar { 3 | width: 5px; 4 | } 5 | 6 | /* Track */ 7 | ::-webkit-scrollbar-track { 8 | background: #f1f1f1; 9 | border-radius: 5px; 10 | } 11 | 12 | /* Handle */ 13 | ::-webkit-scrollbar-thumb { 14 | background: #888; 15 | border-radius: 5px; 16 | } 17 | 18 | /* Handle on hover */ 19 | ::-webkit-scrollbar-thumb:hover { 20 | background: #555; 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/exclamation-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/assets/icons/inventory.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/assets/icons/loop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/assets/icons/magnify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/assets/icons/money.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 33 | 36 | 39 | 42 | 45 | 46 | 48 | 51 | 52 | 53 | 55 | 57 | 58 | 62 | 64 | 67 | 68 | 71 | 74 | 77 | 80 | 83 | 86 | 88 | 89 | 93 | 94 | 95 | 98 | 101 | 104 | 107 | 110 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/assets/icons/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/assets/images/mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simperfy/pos-inventory-system-ui/4d71e694be24beda0bf526f4609c79004019ff2f/src/assets/images/mockup.png -------------------------------------------------------------------------------- /src/assets/images/profile_female.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simperfy/pos-inventory-system-ui/4d71e694be24beda0bf526f4609c79004019ff2f/src/assets/images/profile_female.jpg -------------------------------------------------------------------------------- /src/assets/images/profile_male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simperfy/pos-inventory-system-ui/4d71e694be24beda0bf526f4609c79004019ff2f/src/assets/images/profile_male.png -------------------------------------------------------------------------------- /src/components/Card/Card.css: -------------------------------------------------------------------------------- 1 | .m-default { 2 | margin: 1.875rem; 3 | } 4 | 5 | .mt-default { 6 | margin-top: 1.875rem; 7 | } 8 | 9 | .mb-default { 10 | margin-bottom: 1.875rem; 11 | } 12 | 13 | .ml-default { 14 | margin-left: 1.875rem; 15 | } 16 | 17 | .mr-default { 18 | margin-right: 1.875rem; 19 | } 20 | 21 | .mx-default { 22 | margin: 0 1.875rem; 23 | } 24 | 25 | .my-default { 26 | margin: 1.875rem 0; 27 | } 28 | 29 | .card-container { 30 | cursor: pointer; 31 | height: 16.25rem; 32 | width: 18.75rem; 33 | margin-bottom: 3.75rem; 34 | } 35 | 36 | .card-container > a { 37 | display: block; 38 | } 39 | 40 | .card-container img { 41 | height: 9.375rem; 42 | width: 9.375rem; 43 | display: block; 44 | margin: auto; 45 | } 46 | 47 | .card-bg { 48 | padding-top: 1.5625rem; 49 | width: 18.75rem; 50 | height: 12.5rem; 51 | border-radius: 5px; 52 | background-color: #00b2ff; 53 | } 54 | 55 | .card-link { 56 | font-size: 2.25rem; 57 | text-align: center; 58 | text-decoration: underline; 59 | color: #001aff; 60 | } 61 | 62 | .card-link:hover { 63 | text-decoration: underline; 64 | } -------------------------------------------------------------------------------- /src/components/Card/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Card.css'; 4 | 5 | // TODO: This component violates single responsibility rule (handles both login card and selection) 6 | function Card({img, label, email, gender, noBotMargin, className, style, noLink, notAllowed, handleClick = () => null}) { 7 | return ( 8 |
handleClick(label, email, gender)} 12 | > 13 |
14 | card 15 |
16 |

17 | {label} 18 |

19 |
20 | ); 21 | } 22 | 23 | export default Card; 24 | -------------------------------------------------------------------------------- /src/components/Card/CardHover.css: -------------------------------------------------------------------------------- 1 | .hover-enlarge:hover { 2 | transform: scale(1.02); 3 | } 4 | 5 | .hover-enlarge { 6 | transition: all .2s ease-in-out; 7 | } -------------------------------------------------------------------------------- /src/components/Card/CardProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Card.css'; 4 | import './CardHover.css'; 5 | 6 | // TODO: This component violates single responsibility rule (handles both login card and selection) 7 | const CardProfile = ({img, label, email, gender, noBotMargin, className, style, noLink, notAllowed, handleClick = () => null}) => { 8 | return ( 9 |
handleClick(label, email, gender)} 13 | > 14 |
15 | card 16 |
17 |

18 | {label} 19 |

20 |
21 | ); 22 | }; 23 | 24 | export default CardProfile; 25 | -------------------------------------------------------------------------------- /src/components/Card/index.js: -------------------------------------------------------------------------------- 1 | import CardProfile from './CardProfile'; 2 | import Card from './Card'; 3 | 4 | export default {Card, CardProfile}; 5 | -------------------------------------------------------------------------------- /src/components/Form/Form.spec.js: -------------------------------------------------------------------------------- 1 | import {shallow} from 'enzyme'; 2 | 3 | import React from 'react'; 4 | import FormButton from './FormButton'; 5 | import {FormDetailText} from './FormDetailText'; 6 | 7 | describe('FormButton Test', () => { 8 | let wrapper; 9 | let mockHandleClick; 10 | 11 | beforeEach(() => { 12 | mockHandleClick = jest.fn(() => null); 13 | wrapper = shallow(); 14 | }); 15 | 16 | it('should find "lorem ipsum" when rendered', () => { 17 | expect(wrapper.find('button').text()).toContain('lorem ipsum'); 18 | }); 19 | 20 | it('should call click function when clicked', () => { 21 | wrapper.find('button').simulate('click'); 22 | expect(mockHandleClick.mock.calls.length).toBe(1); 23 | }); 24 | 25 | it('should find red-btn-solid class when rendered', () => { 26 | expect(wrapper.find('button').hasClass('red-btn-solid')).toEqual(true); 27 | }); 28 | }); 29 | 30 | describe('FormDetailText Test', () => { 31 | let wrapper; 32 | const price = 100; 33 | const discount = 50; 34 | const quantity = 10; 35 | 36 | beforeEach(() => { 37 | wrapper = shallow(); 38 | }); 39 | 40 | it('should find price when rendered', () => { 41 | expect(wrapper.find('p').someWhere((n) => n.text().includes('Price: ₱100'))).toEqual(true); 42 | }); 43 | 44 | it('should find discount when rendered', () => { 45 | expect(wrapper.find('p').someWhere((n) => n.text().includes('Discount: -₱50'))).toEqual(true); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/Form/FormButton.css: -------------------------------------------------------------------------------- 1 | .red-btn, 2 | .green-btn, 3 | .blue-btn { 4 | border: 0; 5 | outline: none; 6 | font-size: 1.5rem; 7 | text-decoration: underline; 8 | background-color: inherit; 9 | } 10 | 11 | .red-btn { 12 | color: rgba(255, 0, 0, 0.8); 13 | } 14 | .green-btn { 15 | color: #3BB54A 16 | } 17 | .blue-btn { 18 | color: #00B2FF; 19 | } 20 | 21 | .red-btn-solid, 22 | .green-btn-solid, 23 | .blue-btn-solid { 24 | border: none; 25 | outline: none; 26 | border-radius: 5px; 27 | padding: 0.625rem 3.75rem; 28 | margin-right: 1.875rem; 29 | color: white; 30 | font-size: 2.25rem; 31 | font-weight: bold; 32 | } 33 | 34 | .red-btn-solid { 35 | background-color: rgba(255, 0, 0, 0.8); 36 | } 37 | 38 | .green-btn-solid { 39 | background-color: #3BB54A 40 | } 41 | 42 | .blue-btn-solid { 43 | background-color:#00B2FF; 44 | } 45 | 46 | .red-btn:focus, 47 | .red-btn-solid:focus, 48 | .green-btn-solid:focus .blue-btn-solid:focus { 49 | outline: none; 50 | opacity: 0.8; 51 | } 52 | 53 | button, button:focus { 54 | outline: none 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/components/Form/FormButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './FormButton.css'; 4 | 5 | function FormButton({text, handleClick, style, color, solid}) { 6 | return ( 7 | 10 | ); 11 | } 12 | 13 | export default FormButton; 14 | -------------------------------------------------------------------------------- /src/components/Form/FormDetailText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | 4 | export const FormDetailText = ({price, discount, quantity}) => { 5 | const subTotal = quantity * price; 6 | const discountTotal = quantity * (isNaN(discount) ? 0 : discount); 7 | const total = subTotal - discountTotal; 8 | 9 | return ( 10 |
11 |

12 | Price: ₱{price} (per item) 13 |

14 |

15 | Discount: -₱{discount} (per item) 16 |

17 |
18 |

Subtotal: ₱{subTotal}

19 |

Discount: -₱{discountTotal}

20 |

Total: ₱{total}

21 |
22 | ); 23 | }; 24 | 25 | export default connect((state) => ({ 26 | price: state.price, 27 | discount: state.discount, 28 | quantity: state.quantity, 29 | }), {})(FormDetailText); 30 | -------------------------------------------------------------------------------- /src/components/Form/FormGroup.css: -------------------------------------------------------------------------------- 1 | @keyframes fadein { 2 | from { opacity: 0; } 3 | to { opacity: 1; } 4 | } 5 | 6 | .form-group-container { 7 | animation: 0.5s ease-out 0s 1 fadein; 8 | } 9 | 10 | span.item-barcode, 11 | .form-item select { 12 | font-size: 1.125rem; 13 | color: rgba(0, 0, 0, 0.5); 14 | } 15 | 16 | .form-item input { 17 | border: 0; 18 | outline: none; 19 | max-width: 6.25rem; 20 | text-align: center; 21 | text-decoration: underline; 22 | text-decoration-color: rgba(0, 0, 0, 0.5); 23 | } 24 | 25 | .form-item select { 26 | border: 0; 27 | outline: none; 28 | } 29 | 30 | .form-item option { 31 | font-size: 1.2rem; 32 | } 33 | 34 | .form-item p, 35 | .form-item label, 36 | .form-item input, 37 | .form-item input::placeholder, 38 | .form-item select { 39 | font-size: 1.5rem; 40 | } 41 | 42 | .form-item label { 43 | margin-right: 0.9375rem; 44 | } 45 | 46 | .form-item p, 47 | .details-form p { 48 | margin: 0; 49 | } 50 | 51 | .form-item { 52 | margin: 0 0 1.875rem; 53 | } 54 | 55 | .main-form, 56 | .details-form { 57 | margin-top: 1.875rem; 58 | } 59 | 60 | .form-btn-group { 61 | margin-top: 2.8125rem; 62 | } 63 | 64 | .form-btn-group button { 65 | border-radius: 5px; 66 | } 67 | 68 | .details-form p { 69 | font-size: 1.5rem; 70 | } 71 | 72 | .details-form span { 73 | font-size: 0.75rem; 74 | color: rgba(0, 0, 0, 0.5); 75 | vertical-align: super; 76 | } 77 | 78 | .saturate-red { 79 | color: #460000; 80 | } 81 | 82 | .saturate-green { 83 | color: #004600; 84 | } 85 | -------------------------------------------------------------------------------- /src/components/Form/FormGroupInventoryQuantity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Form} from '../index'; 4 | 5 | import {InventoryContext} from '../../context/InventoryContext'; 6 | 7 | import './FormGroup.css'; 8 | import {connect} from 'react-redux'; 9 | import {updateQuantityOnInput} from '../../actions/quantityActions'; 10 | import {updateSuppliersSelectedId, updateSuppliersSelectedIdOnInput} from '../../actions/suppliersActions'; 11 | 12 | class FormGroupInventoryQuantity extends React.Component { 13 | static contextType = InventoryContext; 14 | 15 | componentDidMount() { 16 | this.props.updateSuppliersSelectedId(this.props.suppliers[0].id); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 |
23 |
24 | 25 | this.props.updateQuantityOnInput(e, false)} 28 | label={'Qty'} 29 | placeHolder={'1 pcs'} 30 | value={this.props.quantity} 31 | min="1" 32 | hideZero 33 | /> 34 | 40 |
41 | 47 | 52 |
53 |
54 |
55 | 56 |
57 | ); 58 | } 59 | } 60 | 61 | export default connect((state) => ({ 62 | quantity: state.quantity, 63 | suppliers: state.suppliersStore.suppliers, 64 | itemText: state.item.text, 65 | itemBarcode: state.item.barcode, 66 | supplierSelectedId: state.suppliersStore.supplierSelectedId, 67 | }), {updateQuantityOnInput, updateSuppliersSelectedIdOnInput, updateSuppliersSelectedId}, 68 | )(FormGroupInventoryQuantity); 69 | -------------------------------------------------------------------------------- /src/components/Form/FormGroupInventorySack.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Form} from '../index'; 4 | 5 | import {InventoryContext} from '../../context/InventoryContext'; 6 | 7 | import './FormGroup.css'; 8 | import {connect} from 'react-redux'; 9 | import {updateQuantityOnInput} from '../../actions/quantityActions'; 10 | import {updateSuppliersSelectedId, updateSuppliersSelectedIdOnInput} from '../../actions/suppliersActions'; 11 | 12 | class FormGroupInventorySack extends React.Component { 13 | static contextType = InventoryContext; 14 | 15 | componentDidMount() { 16 | this.props.updateSuppliersSelectedId(this.props.suppliers[0].id); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 |
23 |
24 | 25 | 34 | 40 | 46 |
47 | 53 | 58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | export default connect((state) => ({ 67 | quantity: state.quantity, 68 | suppliers: state.suppliersStore.suppliers, 69 | sacks: state.sacksStore.sacks, 70 | selectedSackId: state.sacksStore.selectedSackId, 71 | itemText: state.item.text, 72 | itemBarcode: state.item.barcode, 73 | supplierSelectedId: state.suppliersStore.supplierSelectedId, 74 | }), {updateQuantityOnInput, updateSuppliersSelectedIdOnInput, updateSuppliersSelectedId}, 75 | )(FormGroupInventorySack); 76 | -------------------------------------------------------------------------------- /src/components/Form/FormGroupSalesKilo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Form} from '../index'; 4 | 5 | import {SalesContext} from '../../context/SalesContext'; 6 | 7 | import './FormGroup.css'; 8 | import enumKiloType from '../../enums/enumKiloType'; 9 | import {connect} from 'react-redux'; 10 | import {updateDiscountOnInput} from '../../actions/discountActions'; 11 | import {updateQuantity, updateQuantityOnInput} from '../../actions/quantityActions'; 12 | import {updateSackSelectedIdAndKilo, updateSackSelectedIdWith} from '../../actions/sacksActions'; 13 | import {updateKilo, updateKiloOnInputAndPrice} from '../../actions/kiloActions'; 14 | // import {updatePrice} from '../../actions/priceActions'; 15 | 16 | class FormGroupSalesKilo extends React.Component { 17 | static contextType = SalesContext; 18 | 19 | componentDidMount() { 20 | this.props.updateQuantity(1); 21 | this.props.updateKilo(1); 22 | } 23 | 24 | render() { 25 | const { 26 | kiloType, 27 | kiloTypes, 28 | } = this.context.state.mainForm; 29 | 30 | return ( 31 |
32 |
33 |
34 | 35 | ({id: s.value, name: s.name}))} 40 | /> 41 | { kiloType === enumKiloType.sack && 42 | <> 43 | {/* QUANTITY */} 44 | 53 | {/* SACK */} 54 | this.props.updateSackSelectedIdAndKilo(e.target.value)} 57 | onChange={(e) => this.props.updateSackSelectedIdWith(e.target.value, ['kilo', 'price'])} 58 | label={'Sack'} 59 | options={this.props.sacks} 60 | /> 61 | } 62 | 63 | { kiloType === enumKiloType.kilo && 64 | <> 65 | {/* KILO */} 66 | 75 | } 76 | 85 |
86 | 92 | 97 |
98 |
99 |
100 | 101 | {this.props.quantity > 0 && ( 102 |
103 | 104 |
105 | )} 106 |
107 | ); 108 | } 109 | } 110 | 111 | export default connect((state) => ({ 112 | kilo: state.kilo, 113 | quantity: state.quantity, 114 | discount: state.discount, 115 | sacks: state.sacksStore.sacks, 116 | selectedSackId: state.sacksStore.selectedSackId, 117 | itemText: state.item.text, 118 | itemBarcode: state.item.barcode, 119 | }), { 120 | updateQuantity, 121 | updateDiscountOnInput, 122 | updateQuantityOnInput, 123 | updateSackSelectedIdAndKilo, 124 | updateKiloOnInputAndPrice, 125 | updateKilo, 126 | updateSackSelectedIdWith, 127 | })(FormGroupSalesKilo); 128 | -------------------------------------------------------------------------------- /src/components/Form/FormGroupSalesQuantity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Form} from '../index'; 4 | 5 | import {SalesContext} from '../../context/SalesContext'; 6 | 7 | import './FormGroup.css'; 8 | import {connect} from 'react-redux'; 9 | import {updateQuantityOnInput} from '../../actions/quantityActions'; 10 | import {updateDiscountOnInput} from '../../actions/discountActions'; 11 | 12 | class FormGroupSalesQuantity extends React.Component { 13 | static contextType = SalesContext; 14 | 15 | render() { 16 | return ( 17 |
18 |
19 |
20 | 21 | 30 | 39 |
40 | 46 | 51 |
52 |
53 |
54 | 55 | {this.props.quantity > 0 && ( 56 |
57 | 58 |
59 | )} 60 |
61 | ); 62 | } 63 | } 64 | 65 | 66 | export default connect((state) => ({ 67 | quantity: state.quantity, 68 | discount: state.discount, 69 | itemText: state.item.text, 70 | itemBarcode: state.item.barcode, 71 | }), {updateQuantityOnInput, updateDiscountOnInput}, 72 | )(FormGroupSalesQuantity); 73 | -------------------------------------------------------------------------------- /src/components/Form/FormInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function FormInput({formType, label, placeHolder, onChange, max, min, value, hideZero}) { 4 | return ( 5 |
6 | 7 | 17 |
18 | ); 19 | } 20 | 21 | export default FormInput; 22 | -------------------------------------------------------------------------------- /src/components/Form/FormSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function FormSelect({label, options, value, onChange}) { 4 | return ( 5 |
6 | 7 | 12 |
13 | ); 14 | } 15 | 16 | export default FormSelect; 17 | -------------------------------------------------------------------------------- /src/components/Form/FormStaticText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function FormStaticText({text, textBelow}) { 4 | return ( 5 |
6 |

Item: {text}

7 | {textBelow} 8 |
9 | ); 10 | } 11 | 12 | export default FormStaticText; 13 | -------------------------------------------------------------------------------- /src/components/Form/index.js: -------------------------------------------------------------------------------- 1 | import FormButton from './FormButton'; 2 | import FormDetailText from './FormDetailText'; 3 | import FormGroupSalesQuantity from './FormGroupSalesQuantity'; 4 | import FormGroupSalesKilo from './FormGroupSalesKilo'; 5 | import FormGroupInventoryQuantity from './FormGroupInventoryQuantity'; 6 | import FormGroupInventorySack from './FormGroupInventorySack'; 7 | import FormInput from './FormInput'; 8 | import FormSelect from './FormSelect'; 9 | import FormStaticText from './FormStaticText'; 10 | 11 | export default { 12 | FormButton, 13 | FormDetailText, 14 | FormGroupInventoryQuantity, 15 | FormGroupInventorySack, 16 | FormInput, 17 | FormSelect, 18 | FormStaticText, 19 | FormGroupSalesQuantity, 20 | FormGroupSalesKilo, 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.spec.js: -------------------------------------------------------------------------------- 1 | import {mount, shallow} from 'enzyme'; 2 | 3 | import React from 'react'; 4 | import {enumSubmitConfirmTypes} from '../../enums/enumSubmitConfirmTypes'; 5 | import ModalConfirm from './ModalConfirm'; 6 | import ModalFailed from './ModalFailed'; 7 | import ModalLoading from './ModalLoading'; 8 | import ModalSuccess from './ModalSuccess'; 9 | import ModalLogin from './ModalLogin'; 10 | 11 | describe('ModalConfirm Test', () => { 12 | let wrapper; 13 | const confirmItems = [ 14 | {id: 1, leftText: 'sample item', rightText: 100}, 15 | ]; 16 | 17 | it('should find "Adding Items to inventory:" when rendered', () => { 18 | wrapper = shallow(); 19 | 20 | expect(wrapper.find('h4').text()).toBe('Adding Items to inventory:'); 21 | }); 22 | 23 | it('should find "Adding Items to sales:" when rendered', () => { 24 | wrapper = shallow(); 25 | 26 | expect(wrapper.find('h4').text()).toBe('Adding Items to sales:'); 27 | }); 28 | 29 | it('should find "sample item" when rendered', () => { 30 | wrapper = shallow(); 31 | 32 | expect(wrapper.find('.confirm-item-text').text()).toBe('sample item'); 33 | }); 34 | 35 | it('should find "100" when rendered', () => { 36 | wrapper = shallow(); 37 | 38 | expect(wrapper.find('.confirm-item-info').text()).toBe('100'); 39 | }); 40 | }); 41 | 42 | describe('ModalFailed Test', () => { 43 | let wrapper; 44 | let mockHandleClose; 45 | // let mockHandleClick; 46 | 47 | beforeEach(() => { 48 | mockHandleClose = jest.fn(() => null); // no need to test this as this is already tested 49 | // mockHandleClick = jest.fn(() => null); 50 | // wrapper = render(); 51 | wrapper = mount(); 52 | }); 53 | 54 | it('should find "Failed" when rendered', () => { 55 | expect(wrapper.find('h2').text()).toBe('Failed'); 56 | }); 57 | 58 | it('should call function from ".modal-close class" when clicked', () => { 59 | wrapper.find('.modal-close').simulate('click'); 60 | 61 | expect(mockHandleClose.mock.calls.length).toBe(1); 62 | }); 63 | }); 64 | 65 | describe('ModalLoading Test', () => { 66 | let wrapper; 67 | beforeEach(() => { 68 | jest.useFakeTimers(); 69 | wrapper = shallow(); 70 | }); 71 | 72 | it('should animate when rendered', () => { 73 | expect(wrapper.find('h2').text()).toBe('Please wait'); 74 | expect(wrapper.state('counter')).toBe(0); 75 | 76 | jest.advanceTimersByTime(1100); 77 | expect(wrapper.find('h2').text()).toBe('Please wait.'); 78 | 79 | jest.advanceTimersByTime(1100); 80 | expect(wrapper.find('h2').text()).toBe('Please wait..'); 81 | 82 | jest.advanceTimersByTime(1100); 83 | expect(wrapper.find('h2').text()).toBe('Please wait...'); 84 | expect(wrapper.state('counter')).toBe(3); 85 | }); 86 | }); 87 | 88 | describe('ModalSuccess Test', () => { 89 | let wrapper; 90 | 91 | beforeEach(() => { 92 | wrapper = shallow(); 93 | }); 94 | 95 | it('should find "Success" when rendered', () => { 96 | expect(wrapper.find('h2').text()).toBe('Success'); 97 | }); 98 | }); 99 | 100 | describe('ModalLogin Test', () => { 101 | let wrapper; 102 | let mockHandleClick; 103 | 104 | beforeEach(() => { 105 | mockHandleClick = jest.fn(() => null); 106 | wrapper = mount(); 107 | }); 108 | 109 | it('should call "mockHandleClick" when clicked', () => { 110 | wrapper.find('button').simulate('click'); 111 | 112 | expect(mockHandleClick.mock.calls.length).toBe(1); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/components/Modal/ModalConfirm.css: -------------------------------------------------------------------------------- 1 | .confirm-item-container { 2 | } 3 | 4 | .confirm-item-container h4 { 5 | margin-bottom: 1.875rem; 6 | } 7 | 8 | .confirm-item { 9 | } 10 | 11 | .confirm-item-text { 12 | font-size: 1.5rem; 13 | } 14 | 15 | .confirm-item-info { 16 | line-height: 2.5rem; 17 | font-size: 1.125rem; 18 | color: rgba(0,0,0,0.5); 19 | } 20 | 21 | .confirm-item-scrollable { 22 | max-height: 25rem; 23 | overflow: auto; 24 | } -------------------------------------------------------------------------------- /src/components/Modal/ModalConfirm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ModalLayout from '../../containers/ModalLayout'; 3 | 4 | import './ModalConfirm.css'; 5 | import FormButton from '../Form/FormButton'; 6 | import {enumSubmitConfirmTypes} from '../../enums/enumSubmitConfirmTypes'; 7 | 8 | function ModalConfirm({confirmItems, handleSubmitConfirm, setState, submitConfirmType}) { 9 | return ( 10 | <> 11 | 12 |
13 |
14 |

Adding Items to {submitConfirmType === enumSubmitConfirmTypes.INVENTORY_SUBMIT ? 'inventory' : 'sales'}:

15 |
16 | {confirmItems.map((ci) => ( 17 |
21 | 22 | {ci.leftText} 23 | 24 | 25 | {ci.rightText} 26 | 27 |
28 | ))} 29 |
30 |
31 | 32 |
33 | 40 | setState({isConfirming: false})} 45 | style={{fontSize: '1rem', margin: 0}} 46 | /> 47 |
48 |
49 |
50 | 51 | ); 52 | } 53 | 54 | export default ModalConfirm; 55 | -------------------------------------------------------------------------------- /src/components/Modal/ModalFailed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ModalLayout from '../../containers/ModalLayout'; 3 | import FormButton from '../Form/FormButton'; 4 | import {ReactComponent as FailedIcon} from '../../assets/icons/exclamation-mark.svg'; 5 | 6 | function ModalFailed({handleClose, handleClick}) { 7 | return ( 8 | <> 9 | 10 |
11 |
12 | 13 |

Failed

14 |
15 | 16 |
17 | 24 |
25 |
26 |
27 | 28 | ); 29 | } 30 | 31 | export default ModalFailed; 32 | -------------------------------------------------------------------------------- /src/components/Modal/ModalLoading.css: -------------------------------------------------------------------------------- 1 | .spin-img { 2 | -webkit-animation: spin 4s linear infinite; 3 | -moz-animation: spin 4s linear infinite; 4 | animation: spin 4s linear infinite; 5 | } 6 | 7 | @-moz-keyframes spin { 8 | 100% { 9 | -moz-transform: rotate(-360deg); 10 | } 11 | } 12 | @-webkit-keyframes spin { 13 | 100% { 14 | -webkit-transform: rotate(-360deg); 15 | } 16 | } 17 | @keyframes spin { 18 | 100% { 19 | -webkit-transform: rotate(-360deg); 20 | transform: rotate(-360deg); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Modal/ModalLoading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ModalLayout from '../../containers/ModalLayout'; 4 | import {InventoryContext} from '../../context/InventoryContext'; 5 | import {ReactComponent as LoopIcon} from '../../assets/icons/loop.svg'; 6 | 7 | import './ModalLoading.css'; 8 | 9 | class ModalLoading extends React.Component { 10 | static contextType = InventoryContext; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | texts: ['Please wait', 'Please wait.', 'Please wait..', 'Please wait...'], 16 | counter: 0, 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | this.interval = setInterval(() => { 22 | this.setState((prevState, props) => { 23 | if (prevState.counter++ > 2) prevState.counter = 0; 24 | return {counter: prevState.counter}; 25 | }); 26 | }, 1000); 27 | } 28 | 29 | componentWillUnmount() { 30 | clearInterval(this.interval); 31 | } 32 | 33 | render() { 34 | return ( 35 | <> 36 | 37 |
38 |
39 | 40 |

{this.state.texts[this.state.counter]}

41 |
42 |
43 |
44 | 45 | ); 46 | } 47 | } 48 | 49 | export default ModalLoading; 50 | -------------------------------------------------------------------------------- /src/components/Modal/ModalLogin.css: -------------------------------------------------------------------------------- 1 | .login-input { 2 | font-size: 1.875rem; 3 | text-align: center; 4 | height: 3.125rem; 5 | width: 18.75rem; 6 | border-radius: 5px; 7 | border-width: 1px; 8 | background-color: rgba(196, 196, 196, 0.73); 9 | } 10 | 11 | .login-input::placeholder { 12 | color: white; 13 | font-size: 1.875rem; 14 | } 15 | 16 | .login-btn { 17 | margin: 0 5%; 18 | width: 90%; 19 | font-size: 2.25rem; 20 | color: white; 21 | background-color: #0085ff; 22 | border-radius: 5px; 23 | font-weight: bold; 24 | } -------------------------------------------------------------------------------- /src/components/Modal/ModalLogin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from 'react-bootstrap/Button'; 4 | import ModalLayout from '../../containers/ModalLayout'; 5 | import Card from '../Card/Card'; 6 | 7 | import './ModalLogin.css'; 8 | 9 | class ModalLogin extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = {value: ''}; 13 | } 14 | 15 | validatePassword = (e) => { 16 | const regex = /^[0-9\b]+$/; 17 | 18 | if (e.target.value === '' || regex.test(e.target.value)) { 19 | this.setState({value: e.target.value}); 20 | } 21 | 22 | this.props.onChange(e); 23 | }; 24 | 25 | render() { 26 | return ( 27 | <> 28 | 29 | 30 |
31 | {this.props.incorrectPassword &&

Incorrect pin

} 32 | 43 |
44 | 47 |
48 | 49 | ); 50 | } 51 | } 52 | 53 | export default ModalLogin; 54 | -------------------------------------------------------------------------------- /src/components/Modal/ModalSuccess.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ModalLayout from '../../containers/ModalLayout'; 3 | import FormButton from '../Form/FormButton'; 4 | import {ReactComponent as SuccessIcon} from '../../assets/icons/tick.svg'; 5 | 6 | function ModalSuccess({handleClick}) { 7 | return ( 8 | <> 9 | 10 |
11 |
12 | 13 |

Success

14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 | ); 23 | } 24 | 25 | export default ModalSuccess; 26 | -------------------------------------------------------------------------------- /src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import ModalConfirm from './ModalConfirm'; 2 | import ModalFailed from './ModalFailed'; 3 | import ModalLoading from './ModalLoading'; 4 | import ModalLogin from './ModalLogin'; 5 | import ModalSuccess from './ModalSuccess'; 6 | 7 | export default { 8 | ModalConfirm, 9 | ModalFailed, 10 | ModalLoading, 11 | ModalLogin, 12 | ModalSuccess, 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Nav/TopLeftNav.css: -------------------------------------------------------------------------------- 1 | .top-left-nav-icon { 2 | display: inline-block; 3 | vertical-align: baseline; 4 | margin-left: 1.875rem; 5 | } 6 | 7 | .top-left-nav { 8 | margin-top: 1rem; 9 | } 10 | 11 | .top-left-nav .back-btn { 12 | cursor: pointer; 13 | width: 2rem; 14 | fill: #C4C4C4; 15 | display: inline-block; 16 | vertical-align: baseline; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Nav/TopLeftNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {useHistory} from 'react-router-dom'; 4 | 5 | import {getRoute} from '../../routeConfig'; 6 | 7 | import {ReactComponent as LeftArrowIcon} from '../../assets/icons/left-arrow.svg'; 8 | import {ReactComponent as InventoryIcon} from '../../assets/icons/inventory.svg'; 9 | import {ReactComponent as MoneyIcon} from '../../assets/icons/money.svg'; 10 | 11 | import './TopLeftNav.css'; 12 | 13 | const TopLeftNav = ({type}) => { 14 | const history = useHistory(); 15 | 16 | return ( 17 |
18 | history.push(getRoute('selection'))} 20 | className="ml-3 back-btn" 21 | /> 22 | {type === 'inventory' && } 23 | {type === 'sales' && } 24 | {type === 'inventory' ? 'Inventory' : 'Sales'} 25 |
26 | ); 27 | }; 28 | 29 | export default TopLeftNav; 30 | -------------------------------------------------------------------------------- /src/components/Nav/TopRightNav.css: -------------------------------------------------------------------------------- 1 | a.top-right-nav, 2 | p.top-right-nav { 3 | vertical-align: baseline; 4 | display: inline-block; 5 | padding-top: 1rem; 6 | } 7 | 8 | p.top-right-nav { 9 | font-size: 1.5rem; 10 | margin: 0; 11 | line-height: 2.625rem; 12 | } 13 | 14 | a.top-right-nav { 15 | font-size: 1.75rem; 16 | margin: 0 1.875rem; 17 | text-decoration: underline; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Nav/TopRightNav.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | 3 | import {useHistory} from 'react-router-dom'; 4 | import {getRoute} from '../../routeConfig'; 5 | 6 | import {AppContext} from '../../context/AppContext'; 7 | 8 | import './TopRightNav.css'; 9 | 10 | const TopRightNav = ({username, hasBackBtn}) => { 11 | const history = useHistory(); 12 | const {logout} = useContext(AppContext); 13 | 14 | return ( 15 | <> 16 |

{username}

17 | { 21 | logout(); 22 | history.push(getRoute('home')); 23 | }} 24 | > 25 | Logout 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default TopRightNav; 32 | -------------------------------------------------------------------------------- /src/components/Nav/index.js: -------------------------------------------------------------------------------- 1 | import TopLeftNav from './TopLeftNav'; 2 | import TopRightNav from './TopRightNav'; 3 | 4 | export default {TopLeftNav, TopRightNav}; 5 | -------------------------------------------------------------------------------- /src/components/PendingItem.css: -------------------------------------------------------------------------------- 1 | /* .pending-item { 2 | margin-bottom: 1.875rem; 3 | } */ 4 | 5 | @keyframes slideInFromLeft { 6 | 0% { 7 | transform: translateX(-100%); 8 | } 9 | 100% { 10 | transform: translateX(0); 11 | } 12 | } 13 | 14 | .pending-item { 15 | animation: 0.5s ease-out 0s 1 slideInFromLeft; 16 | } 17 | /* Decrease max-height for sales because of Total Sales text */ 18 | .pending-item-scrollable-inventory { 19 | overflow-y: auto; 20 | max-height: 30rem; 21 | } 22 | 23 | .pending-item span, .pending-item p { 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | } 27 | 28 | .pending-item p { 29 | font-size: 1.5rem; 30 | margin: 0; 31 | padding: 0; 32 | } 33 | 34 | .pending-item button { 35 | color: red; 36 | outline: none; 37 | border: 0; 38 | background-color: inherit; 39 | } 40 | 41 | .pending-item span { 42 | vertical-align: super; 43 | font-size: 0.8rem; 44 | color: rgba(0, 0, 0, 0.5); 45 | } 46 | 47 | .delete-icon { 48 | height: 1.5rem; 49 | width: 1.5rem; 50 | fill: rgba(255, 0, 0, 0.8); 51 | } 52 | 53 | .pending-item .text-right { 54 | font-size: 0.8rem; 55 | display: inline-block; 56 | color: rgba(0, 0, 0, 0.7); 57 | } 58 | 59 | .pending-item button { 60 | display: inline-block; 61 | color: rgba(0, 0, 0, 0.7); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/PendingItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {ReactComponent as DeleteIcon} from '../assets/icons/delete.svg'; 4 | 5 | import './PendingItem.css'; 6 | 7 | function PendingItem({id, quantity, name, textBelow, textBelow2, textRight, textRightStyle, textRightBelow, removePendingItem}) { 8 | const style = { 9 | textRightBelow: { 10 | lineHeight: 1, 11 | overflow: 'hidden', 12 | maxWidth: '5rem', 13 | display: 'inline-block', 14 | textOverflow: 'ellipsis', 15 | textDecoration: 'line-through', 16 | }, 17 | }; 18 | 19 | return ( 20 | <> 21 |
22 |
23 |

24 | {quantity} x {name} 25 |
26 | {textBelow} {textBelow2 &&
} {textBelow2}
27 |

28 |
29 |
30 |

31 | { textRight } 32 |
33 | {textRightBelow} 34 |

35 |
36 | 39 |
40 |
41 | {/* {textBelow} {textBelow2 &&
} {textBelow2}
*/} 42 |
43 | 44 | ); 45 | } 46 | 47 | export default PendingItem; 48 | -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBarGroup.css: -------------------------------------------------------------------------------- 1 | .search-icon { 2 | width: 2rem; 3 | fill: rgba(0, 0, 0, 0.5); 4 | } 5 | 6 | .search-bar-input { 7 | height: 5rem; 8 | width: 90%; 9 | border: 0; 10 | outline: none; 11 | } 12 | 13 | .search-bar { 14 | background-color: white; 15 | display: flex; 16 | justify-content: space-between; 17 | border: 1px solid rgba(0, 0, 0, 0.2); 18 | border-radius: 5px; 19 | } 20 | 21 | .search-bar, 22 | .search-group { 23 | width: 100%; 24 | } 25 | 26 | .search-bar-input, 27 | .search-bar-input::placeholder { 28 | font-size: 2.25rem; 29 | margin-left: 1.875rem; 30 | } 31 | 32 | .search-bar > img { 33 | width: 2rem; 34 | } 35 | 36 | .search-btn { 37 | margin-right: 1.875rem; 38 | outline: none !important; 39 | border: 0px solid black; 40 | background-color: white; 41 | } 42 | 43 | .search-items-container { 44 | position: fixed; 45 | width: 65%; 46 | overflow-y: auto; 47 | max-height: 25rem; 48 | z-index: 1; 49 | } -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBarGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchBarItem from './SearchBarItem'; 3 | 4 | import {ReactComponent as Magnify} from '../../assets/icons/magnify.svg'; 5 | 6 | import './SearchBarGroup.css'; 7 | import '../../assets/css/global-style.css'; 8 | import {connect} from 'react-redux'; 9 | 10 | class SearchBarGroup extends React.Component { 11 | render() { 12 | return ( 13 |
14 |
15 |
16 | 24 | 27 |
28 | 29 | {this.props.showSearchResults && ( 30 |
31 | {this.props.searchResults.map((res) => ( 32 | this.props.handleSearchBarItemClick(res)} 38 | checkIfOutOfStock={this.props.checkIfOutOfStock} 39 | /> 40 | ))} 41 |
42 | )} 43 |
44 |
45 | ); 46 | } 47 | } 48 | 49 | export default connect((state) => ({ 50 | searchResults: state.searchResults, 51 | }), {})(SearchBarGroup); 52 | -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBarItem.css: -------------------------------------------------------------------------------- 1 | .search-bar-item { 2 | height: 2.5rem; 3 | background-color: white; 4 | border-left: 1px solid rgba(0, 0, 0, 0.2); 5 | border-right: 1px solid rgba(0, 0, 0, 0.2); 6 | border-bottom: 1px solid rgba(0, 0, 0, 0.2); 7 | } 8 | 9 | .search-bar-item:hover { 10 | background-color: #EAF3FF; 11 | cursor: pointer; 12 | } 13 | 14 | .sb-label, .sb-id { 15 | margin: 0; 16 | padding: 0; 17 | line-height: 2.5rem; 18 | } 19 | 20 | .sb-label { 21 | font-size: 1.5rem; 22 | margin-left: 1.875rem; 23 | } 24 | 25 | .sb-id { 26 | margin-right: 1.875rem; 27 | color: rgba(0, 0, 0, 0.5) 28 | } -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBarItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './SearchBarItem.css'; 4 | 5 | function SearchBarItem({name, barcode, remaining, onClick, checkIfOutOfStock = true}) { 6 | let labelStyle = {}; 7 | let divStyle = {}; 8 | const outOfStock = (remaining <= 0) && checkIfOutOfStock; 9 | 10 | const spanStyle = { 11 | color: 'red', 12 | }; 13 | 14 | if (outOfStock) { 15 | divStyle = { 16 | cursor: 'not-allowed', 17 | }; 18 | 19 | labelStyle = { 20 | color: 'red', 21 | textDecoration: 'line-through', 22 | }; 23 | } 24 | 25 | return ( 26 |
27 |
28 | {name} 29 | {outOfStock &&    Out of stock} 30 |
31 |

{barcode}

32 |
33 | ); 34 | } 35 | 36 | export default SearchBarItem; 37 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | import SearchBarGroup from './SearchBarGroup'; 2 | import SearchBarItem from './SearchBarItem'; 3 | 4 | export default {SearchBarGroup, SearchBarItem}; 5 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Card from './Card'; 2 | import Form from './Form'; 3 | import Modal from './Modal'; 4 | import Nav from './Nav'; 5 | import SearchBar from './SearchBar'; 6 | // import PendingItem from "./PendingItem"; 7 | 8 | export {Card, Form, Modal, Nav, SearchBar}; 9 | -------------------------------------------------------------------------------- /src/containers/MainFormLayoutInventory.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | 3 | import {Form, SearchBar} from '../components'; 4 | import {InventoryContext} from '../context/InventoryContext'; 5 | import formTypes from '../enums/enumFormTypes'; 6 | 7 | function MainFormLayoutInventory() { 8 | const { 9 | state: { 10 | showForm, showSearchResults, 11 | searchResults, formType, 12 | }, handleSearchBarChange, 13 | handleSearchBarFocus, 14 | handleSearchBarBlur, 15 | handleSearchBarItemClick, 16 | } = useContext(InventoryContext); 17 | 18 | return ( 19 | <> 20 | handleSearchBarItemClick(res)} 26 | checkIfOutOfStock={false} 27 | /> 28 | 29 | {(showForm && formType === formTypes.inventoryPerQuantity) && } 30 | {/* {(showForm && formType === formTypes.inventoryPerSack) && }*/} 31 | 32 | ); 33 | } 34 | 35 | export default MainFormLayoutInventory; 36 | -------------------------------------------------------------------------------- /src/containers/MainFormLayoutSales.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | 3 | import {Form, SearchBar} from '../components'; 4 | import {SalesContext} from '../context/SalesContext'; 5 | import formTypes from '../enums/enumFormTypes'; 6 | 7 | function MainFormLayoutSales() { 8 | const { 9 | state: { 10 | showForm, showSearchResults, 11 | searchResults, formType, 12 | }, handleSearchBarChange, 13 | handleSearchBarFocus, 14 | handleSearchBarBlur, 15 | handleSearchBarItemClick, 16 | } = useContext(SalesContext); 17 | 18 | return ( 19 | <> 20 | handleSearchBarItemClick(res)}/> 26 | 27 | {(showForm && formType === formTypes.salesPerQuantity) && } 28 | {/* {(showForm && formType === formTypes.salesPerKilo) && }*/} 29 | 30 | ); 31 | } 32 | 33 | export default MainFormLayoutSales; 34 | -------------------------------------------------------------------------------- /src/containers/MainLayout.css: -------------------------------------------------------------------------------- 1 | .main-title { 2 | font-size: 2.25rem; 3 | margin-left: 0.9375rem; 4 | padding-top: 1rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/containers/MainLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Row from 'react-bootstrap/Row'; 4 | 5 | import MainLayoutNav from './MainLayoutNav'; 6 | 7 | import './MainLayout.css'; 8 | 9 | const MainLayout = ({type, children}) => ( 10 | <> 11 | 12 | 13 | 14 | {children} 15 | 16 | ); 17 | 18 | 19 | export default MainLayout; 20 | -------------------------------------------------------------------------------- /src/containers/MainLayoutNav.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | 3 | import {Nav} from '../components'; 4 | import {AppContext} from '../context/AppContext'; 5 | 6 | const MainLayoutNav = ({type}) => { 7 | const {state: {user: {username}}} = useContext(AppContext); 8 | 9 | return <> 10 | 11 |
12 | 13 |
14 | ; 15 | }; 16 | 17 | export default MainLayoutNav; 18 | -------------------------------------------------------------------------------- /src/containers/ModalLayout.css: -------------------------------------------------------------------------------- 1 | .modal-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | margin: auto; 8 | background-color: #c4c4c4; 9 | opacity: 0.8; 10 | } 11 | 12 | .modal-container { 13 | position: absolute; 14 | left: 25%; 15 | top: 25%; 16 | right: 25%; 17 | bottom: 25%; 18 | 19 | margin: auto; 20 | border-radius: 5px; 21 | width: 29.5rem; 22 | height: 41.25rem; 23 | background-color: white; 24 | z-index: 1; 25 | padding: 1.875rem; 26 | } 27 | 28 | .modal-close { 29 | position: relative; 30 | left: 40%; 31 | max-width: 1rem; 32 | font-size: 2rem; 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/ModalLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Row from 'react-bootstrap/Row'; 4 | 5 | import {ReactComponent as CloseBtn} from '../assets/icons/close.svg'; 6 | import './ModalLayout.css'; 7 | 8 | function ModalLayout({handleClose, children}) { 9 | return ( 10 | <> 11 |
12 |
13 | 14 | {handleClose && ( 15 | 16 | 17 | 18 | )} 19 | 20 | {children} 21 | 22 |
23 | 24 | ); 25 | } 26 | 27 | export default ModalLayout; 28 | -------------------------------------------------------------------------------- /src/containers/PendingItemsLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Form} from '../components'; 4 | import PendingItem from '../components/PendingItem'; 5 | import pendingItemTypes from '../enums/enumPendingItemTypes'; 6 | import {connect} from 'react-redux'; 7 | import {addPendingInventoryItem, removeAllPendingItems, removeSinglePendingItem} from '../actions/pendingItemsActions'; 8 | 9 | class PendingItemsLayout extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.removeAllStyle = { 14 | fontSize: '0.875rem', 15 | opacity: 0.7, 16 | float: 'right', 17 | padding: 0, 18 | marginBottom: '1.5rem', 19 | }; 20 | 21 | this.pendingItemsContainerStyle = { 22 | borderLeft: '0.5px solid rgba(0, 0, 0, 0.2)', 23 | height: '100%', 24 | }; 25 | } 26 | 27 | render() { 28 | const showTotal = this.props.pendingItems.length > 0 && this.props.pendingItemTypes === pendingItemTypes.sales; 29 | 30 | return ( 31 |
32 |
33 | {this.props.pendingItems.length > 0 && ( 34 | 40 | )} 41 |
42 | {this.props.pendingItems.map((pi) => { 43 | if (this.props.pendingItemTypes === pendingItemTypes.inventory) { 44 | return ( 45 | 0 ? `(${pi.kilo} kg)` : ''}`} 50 | textBelow={pi.barcode} 51 | textBelow2={pi.supplierName} 52 | removePendingItem={this.props.removeSinglePendingItem} 53 | /> 54 | ); 55 | } else if (this.props.pendingItemTypes === pendingItemTypes.sales) { 56 | const textRight = `₱${((pi.price - pi.discount) * pi.quantity).toFixed(2)}`; 57 | const totalDiscount = pi.discount * pi.quantity; 58 | const textRightBelow = `₱${(totalDiscount).toFixed(2)}`; 59 | 60 | return ( 61 | 0 ? `(${pi.kilo} kg)` : ''}`} 66 | textBelow={pi.barcode} 67 | // textBelow2={pi.supplierName} 68 | textRight={textRight} 69 | textRightStyle={{fontSize: '1.2rem'}} 70 | textRightBelow={totalDiscount > 0 && textRightBelow} 71 | removePendingItem={this.props.removeSinglePendingItem} 72 | /> 73 | ); 74 | } 75 | })} 76 |
77 | 78 |
79 | {showTotal && 80 |

Total: ₱{this.props.pendingItems.reduce((acc, pi) => { 81 | return acc + (pi.price - pi.discount) * pi.quantity; 82 | }, 0).toFixed(2)} 83 |

84 | } 85 | {this.props.pendingItems.length > 0 && 86 | this.props.setState({isConfirming: true})} 92 | />} 93 |
94 |
95 | ); 96 | } 97 | } 98 | 99 | // export default PendingItemsLayout; 100 | export default connect((state) => ({ 101 | pendingItems: state.pending.pendingItems, 102 | }), {addPendingItem: addPendingInventoryItem, removeAllPendingItems, removeSinglePendingItem}, 103 | )(PendingItemsLayout); 104 | -------------------------------------------------------------------------------- /src/context/AppContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const AppContext = React.createContext(); 4 | -------------------------------------------------------------------------------- /src/context/InventoryContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const InventoryContext = React.createContext(); 4 | -------------------------------------------------------------------------------- /src/context/SalesContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SalesContext = React.createContext(); 4 | -------------------------------------------------------------------------------- /src/enums/enumFormTypes.js: -------------------------------------------------------------------------------- 1 | export default Object.freeze({ 2 | inventoryPerQuantity: 'INVENTORY_PER_QUANTITY', 3 | inventoryPerSack: 'INVENTORY_PER_SACK', 4 | salesPerQuantity: 'SALES_PER_QUANTITY', 5 | salesPerKilo: 'SALES_PER_KILO', 6 | }); 7 | -------------------------------------------------------------------------------- /src/enums/enumKiloType.js: -------------------------------------------------------------------------------- 1 | export default Object.freeze({ 2 | kilo: 'KILO', 3 | sack: 'SACK', 4 | }); 5 | -------------------------------------------------------------------------------- /src/enums/enumPendingItemTypes.js: -------------------------------------------------------------------------------- 1 | export default Object.freeze({ 2 | 'inventory': 'INVENTORY', 3 | 'sales': 'SALES', 4 | }); 5 | -------------------------------------------------------------------------------- /src/enums/enumSubmitConfirmTypes.js: -------------------------------------------------------------------------------- 1 | export const enumSubmitConfirmTypes = { 2 | INVENTORY_SUBMIT: 'INVENTORY_SUBMIT', 3 | SALES_SUBMIT: 'SALES_SUBMIT', 4 | }; 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root'), 10 | ); 11 | -------------------------------------------------------------------------------- /src/pages/AbstractPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Api from '../api/Api'; 3 | import {ModelStocks} from '../api/models'; 4 | import {getRoute} from '../routeConfig'; 5 | import enumKiloType from '../enums/enumKiloType'; 6 | import {enumSubmitConfirmTypes} from '../enums/enumSubmitConfirmTypes'; 7 | import ModelTransactionItem from '../api/models/ModelTransactionItem'; 8 | import {ModelTransactions} from '../api/models/ModelTransactions'; 9 | 10 | export class AbstractPage extends React.Component { 11 | constructor(props) { 12 | if (new.target === AbstractPage) { 13 | throw new TypeError('Cannot construct PageAbstract instances directly'); 14 | } 15 | 16 | super(props); 17 | this.pendingItemsCounter = 0; 18 | 19 | this.state = { 20 | /* Modals*/ 21 | isSuccess: false, 22 | isFailed: false, 23 | isLoading: false, 24 | isConfirming: false, 25 | /* Forms*/ 26 | pendingItems: [], 27 | formDetail: { 28 | price: 0, 29 | discount: 0, 30 | }, 31 | mainForm: { 32 | kiloType: enumKiloType.kilo, 33 | kiloTypes: [ 34 | {id: 1, value: enumKiloType.kilo, name: 'per kilo'}, 35 | {id: 2, value: enumKiloType.sack, name: 'per sack'}, 36 | ], 37 | itemText: '', 38 | itemBarcode: '', 39 | suppliers: [], 40 | sacks: [], 41 | supplierId: '', 42 | supplierName: '', 43 | quantity: '', 44 | kilo: 0, 45 | discount: 0, 46 | }, 47 | showForm: false, 48 | formGroupRef: React.createRef(), 49 | formType: '', 50 | searchResults: [], 51 | showSearchResults: false, 52 | }; 53 | } 54 | 55 | resetForm = () => { 56 | this.setState((prevState, props) => ({ 57 | mainForm: { 58 | kiloType: enumKiloType.kilo, 59 | kiloTypes: [ 60 | {id: 1, value: enumKiloType.kilo, name: 'per kilo'}, 61 | {id: 2, value: enumKiloType.sack, name: 'per sack'}, 62 | ], 63 | itemText: '', 64 | itemBarcode: '', 65 | suppliers: [], 66 | sacks: [], 67 | supplierId: '', 68 | supplierName: '', 69 | quantity: '', 70 | kilo: 0, 71 | }, 72 | formDetail: { 73 | price: 0, 74 | discount: 0, 75 | }, 76 | })); 77 | } 78 | 79 | isValidFormInventory = (item) => { 80 | const nonEmptyFields = [ 81 | 'itemText', 82 | 'itemBarcode', 83 | // 'suppliers', 84 | // 'supplierName', 85 | 'quantity', 86 | // 'kilo', 87 | ]; 88 | 89 | for (const key of Object.keys(item)) { 90 | if (nonEmptyFields.includes(key) && !item[key]) { 91 | window.alert(`Invalid "${key.toUpperCase()}" values`); 92 | return false; 93 | } 94 | } 95 | 96 | return true; 97 | } 98 | 99 | isValidFormSales = (item) => { 100 | const nonEmptyFields = [ 101 | 'itemText', 102 | 'itemBarcode', 103 | 'quantity', 104 | ]; 105 | 106 | for (const key of Object.keys(item)) { 107 | if (nonEmptyFields.includes(key) && !item[key]) { 108 | window.alert(`Invalid "${key.toUpperCase()}" values`); 109 | return false; 110 | } 111 | } 112 | 113 | return true; 114 | } 115 | 116 | checkDuplicate = (pi) => { 117 | const { 118 | itemBarcode, 119 | supplierId, 120 | } = this.state.mainForm; 121 | 122 | return pi.barcode === itemBarcode && pi.supplierId === supplierId; 123 | } 124 | 125 | filterDuplicate = (pi) => { 126 | const { 127 | itemBarcode, 128 | supplierId, 129 | } = this.state.mainForm; 130 | 131 | if (pi.barcode !== itemBarcode) return true; 132 | return pi.barcode === itemBarcode && pi.supplierId !== supplierId; 133 | } 134 | 135 | removeDuplicate = () => { 136 | const isDuplicated = this.state.pendingItems.some(this.checkDuplicate); 137 | 138 | if (isDuplicated) { 139 | const isConfirmed = window.confirm('Item already added, do you want to replace the item?'); 140 | 141 | if (isConfirmed) { 142 | this.setState((prevState, props) => { 143 | const newPendingItems = prevState.pendingItems.filter(this.filterDuplicate); 144 | return {pendingItems: newPendingItems}; 145 | }); 146 | 147 | return true; // if there's duplicate and already replace, update state 148 | } 149 | 150 | return false; // if there's duplicate and confirm = no, don't update state 151 | } 152 | 153 | return true; // if no duplicate update state 154 | } 155 | 156 | removePendingItem = (id) => { 157 | this.setState((prevState, props) => ({ 158 | pendingItems: prevState.pendingItems.filter((pi) => pi.id !== id), 159 | })); 160 | }; 161 | 162 | removeAllPendingItems = () => { 163 | if (window.confirm('Do you want to remove all items?')) { 164 | this.setState({pendingItems: []}); 165 | } 166 | }; 167 | 168 | closeForm = () => this.setState({showForm: false}); 169 | showForm = (type) => this.setState({showForm: true, formType: type}); 170 | 171 | closeSearchResults = () => this.setState({showSearchResults: false}); 172 | showSearchResults = () => this.setState({showSearchResults: true}); 173 | 174 | getRemainingItems = (val) => { 175 | const jwt = this.context.state.jwt; 176 | 177 | // eslint-disable-next-line camelcase 178 | ModelStocks.getRemaining(jwt, val).then(({data: {remaining_items}}) => { 179 | console.log('remaining_items'); 180 | console.log(remaining_items); 181 | 182 | Api.getItems(this.context.state.jwt, val).then(({data}) => { 183 | console.log('data'); 184 | console.log(data); 185 | 186 | data = data.map((d) => { 187 | const remaining = remaining_items.find((rs) => rs.id === d.id)?.remaining_unit || 0; 188 | return {...d, remaining}; 189 | }); 190 | 191 | console.log('new data'); 192 | console.log(data); 193 | 194 | this.props.updateSearchResults(data); 195 | this.showSearchResults(); 196 | }).then((data) => null, (err) => console.log(err)); 197 | }).then((data) => null, (err) => console.log(err)); 198 | } 199 | 200 | handleSearchBarChange = (e) => { 201 | const val = e.target.value; 202 | 203 | if (val) { 204 | this.getRemainingItems(val); 205 | } else if (!val && this.props.searchResults.length > 0) { 206 | this.showSearchResults(); 207 | } else { 208 | this.closeSearchResults(); 209 | } 210 | }; 211 | 212 | handleSearchBarFocus = (e) => { 213 | const val = e.target.value; 214 | 215 | if (!val) { 216 | this.getRemainingItems(val, {_limit: 10}); 217 | } else if (val && this.props.searchResults.length > 0 && !this.state.showSearchResults) this.showSearchResults(); 218 | } 219 | 220 | handleSearchBarBlur = (e) => { 221 | setTimeout(() => this.closeSearchResults(), 100); 222 | } 223 | 224 | handleSubmitConfirm = (enumSubmitConfirmType) => { 225 | this.setState({isConfirming: false, isLoading: true}); 226 | 227 | const jwt = this.context.state.jwt; 228 | const id = this.context.state.user.id; 229 | const pendingItems = this.props.pendingItems; // coming from redux 230 | 231 | if (enumSubmitConfirmType === enumSubmitConfirmTypes.INVENTORY_SUBMIT) { 232 | // Handle Inventory Submission 233 | ModelStocks.createBatch(jwt, id, pendingItems) 234 | .then((data) => 235 | setTimeout(() => this.setState({isSuccess: true, pendingItems: []}), 1000), 236 | (err) => this.showFailedModal()); 237 | } else if (enumSubmitConfirmType === enumSubmitConfirmTypes.SALES_SUBMIT) { 238 | // Handle Sales Submission 239 | ModelTransactions.create(jwt, id).then(({data}) => { 240 | ModelTransactionItem.createBatch(jwt, data.id, pendingItems) 241 | .then((data) => { 242 | console.log('data'); 243 | console.log(data); 244 | setTimeout(() => this.setState({isSuccess: true, pendingItems: []}), 1000); 245 | }, 246 | (err) => this.showFailedModal()); 247 | }, (err) => this.showFailedModal()); 248 | } 249 | } 250 | 251 | showFailedModal() { 252 | setTimeout(() => this.setState({isFailed: true}), 1000); 253 | } 254 | 255 | handleSupplierSelectChange = (e) => { 256 | this.setState((prevState, props) => { 257 | const supplierName = prevState.mainForm.suppliers.find((s)=>s.id === e.target.value).supplierName; 258 | 259 | return { 260 | mainForm: {...prevState.mainForm, supplierId: e.target.value, supplierName: supplierName}, 261 | }; 262 | }); 263 | } 264 | 265 | handleQuantityInputChange = (e) => { 266 | let quantity = parseInt(e.target.value); 267 | quantity = isNaN(quantity) ? 0 : quantity; 268 | 269 | this.setState((prevState, props) => { 270 | let discount = quantity <= 0 ? 0 : prevState.formDetail.discount; 271 | if (isNaN(discount) || !discount) discount = 0; 272 | 273 | return { 274 | mainForm: {...prevState.mainForm, quantity: quantity}, 275 | formDetail: { 276 | price: prevState.formDetail.price, 277 | discount: discount, 278 | }, 279 | }; 280 | }, 281 | ); 282 | } 283 | 284 | handleKiloInputChange = (e) => { 285 | let kilo = parseInt(e.target.value); 286 | kilo = isNaN(kilo) ? 0 : kilo; 287 | 288 | this.setState((prevState, props) => ({ 289 | mainForm: {...prevState.mainForm, kilo: kilo}, 290 | })); 291 | } 292 | 293 | handleDiscountInputChange = (e) => { 294 | let discount = parseFloat(e.target.value); 295 | discount = isNaN(discount) ? 0 : discount; 296 | 297 | if (this.state.mainForm.quantity <= 0) return; 298 | if (discount >= this.state.formDetail.price) discount = this.state.formDetail.price; 299 | this.setState((prevState, props) => ({ 300 | formDetail: {...prevState.formDetail, discount: discount}, 301 | })); 302 | } 303 | 304 | handleSackSelectChange = (e) => { 305 | const id = e.target.value; // id if using the redux 306 | const kilo = this.props.sacks.find((s) => s.id === id).value; 307 | 308 | this.props.updateSackSelectedIdAndKilo(id); 309 | 310 | this.setState((prevState, props) => ({ 311 | mainForm: {...prevState.mainForm, kilo: kilo}, 312 | })); 313 | } 314 | 315 | handleModalSuccessClick = (e) => { 316 | this.props.history.push(getRoute('selection')); 317 | } 318 | 319 | handleModalFailedClose = (e) => { 320 | e.preventDefault(); 321 | // this.handleSubmitConfirm(); 322 | this.setState({isFailed: false, isLoading: false}); 323 | } 324 | 325 | handleModalFailedClick = (submitConfirmType) => { 326 | this.setState({isFailed: false, isLoading: false}); 327 | setTimeout(() => this.handleSubmitConfirm(submitConfirmType), 250); 328 | } 329 | 330 | addOpacityBlur = () => { 331 | const fGRef = this.state.formGroupRef.current; 332 | if (fGRef) { 333 | fGRef.classList.remove('form-group-container'); 334 | setTimeout(() => fGRef.classList.add('form-group-container'), 100); 335 | } 336 | } 337 | 338 | handleItemTypeSelectChange = (e) => { 339 | const kiloType = e.target.value; 340 | 341 | if (kiloType === enumKiloType.kilo) { 342 | this.props.updateKilo(1); 343 | this.props.updateQuantity(1); 344 | } else if (kiloType === enumKiloType.sack) { 345 | this.props.resetQuantity(); 346 | this.props.updateKiloBySackId(this.props.sacks[0].id); 347 | this.props.updatePriceBySackId(this.props.sacks[0].id); 348 | } 349 | 350 | this.setState((prevState, props) => ({ 351 | mainForm: { 352 | ...prevState.mainForm, kiloType: kiloType, 353 | kilo: kiloType === enumKiloType.kilo ? 0 : prevState.mainForm?.sacks[0]?.sackValue || 0}, 354 | })); 355 | } 356 | 357 | providerFunctions = () => { 358 | return { 359 | state: this.state, 360 | setState: this.setState.bind(this), 361 | addPendingItems: this.addPendingItems, 362 | removePendingItem: this.removePendingItem, 363 | removeAllPendingItems: this.removeAllPendingItems, 364 | closeForm: this.closeForm, 365 | showForm: this.showForm, 366 | handleSearchBarChange: this.handleSearchBarChange, 367 | handleSearchBarItemClick: this.handleSearchBarItemClick, 368 | handleSearchBarFocus: this.handleSearchBarFocus, 369 | handleSearchBarBlur: this.handleSearchBarBlur, 370 | handleSubmitConfirm: this.handleSubmitConfirm, 371 | handleSupplierSelectChange: this.handleSupplierSelectChange, 372 | handleQuantityInputChange: this.handleQuantityInputChange, 373 | handleSackSelectChange: this.handleSackSelectChange, 374 | handleDiscountInputChange: this.handleDiscountInputChange, 375 | handleItemTypeSelectChange: this.handleItemTypeSelectChange, 376 | handleKiloInputChange: this.handleKiloInputChange, 377 | }; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Row from 'react-bootstrap/Row'; 4 | import {Card, Modal} from '../components'; 5 | import profileMalePic from '../assets/images/profile_male.png'; 6 | import profileFemalePic from '../assets/images/profile_female.jpg'; 7 | import axios from 'axios'; 8 | import env from 'react-dotenv'; 9 | import {getRoute} from '../routeConfig'; 10 | import {AppContext} from '../context/AppContext'; 11 | import {withRouter} from 'react-router-dom'; 12 | import Api from '../api/Api'; 13 | 14 | class Home extends React.Component { 15 | _isMounted = false; 16 | static contextType = AppContext; 17 | 18 | constructor(props) { 19 | super(props); 20 | 21 | this.state = { 22 | selectedUserGender: '', 23 | selectedUser: null, 24 | selectedUserEmail: '', 25 | selectedUserPassword: '', 26 | showModal: false, 27 | incorrectPassword: false, 28 | users: [], 29 | }; 30 | } 31 | 32 | componentDidMount() { 33 | this._isMounted = true; 34 | Api.getUsers().then(({data}) => { 35 | const users = data.map((u) => ({id: u.id, user: u.username, email: u.email, gender: u.gender})); 36 | this._isMounted && this.setState({users: users}); 37 | }); 38 | } 39 | 40 | componentWillUnmount() { 41 | this._isMounted = false; 42 | } 43 | 44 | handleClose = () => this.setState({showModal: false}); 45 | 46 | handleClick = (user, email, gender) =>{ 47 | this.setState({selectedUser: user, selectedUserEmail: email, selectedUserGender: gender, showModal: true}); 48 | } 49 | 50 | handleLogin = () => { 51 | this.setState({incorrectPassword: false}); 52 | axios 53 | .post(`${env.API_URL}/auth/local`, { 54 | identifier: this.state.selectedUser, 55 | password: this.state.selectedUserPassword, 56 | }) 57 | .then( 58 | ({data: {user, jwt}}) => { 59 | this.context.login(user, jwt); 60 | this.props.history.push(getRoute('selection')); 61 | }, 62 | (err) => { 63 | const statusCode = err.response.data.statusCode; 64 | if (statusCode >= 400 && statusCode < 500 && this._isMounted) { 65 | this.setState({incorrectPassword: true}); 66 | } 67 | }, 68 | ); 69 | } 70 | 71 | handlePinChange = (e) => this.setState({selectedUserPassword: e.target.value}); 72 | 73 | render() { 74 | return ( 75 | <> 76 | 80 | {this.state.users.map((u) => ( 81 | 89 | ))} 90 | 91 | {this.state.showModal && 92 | 100 | } 101 | 102 | ); 103 | } 104 | } 105 | 106 | export default withRouter(Home); 107 | -------------------------------------------------------------------------------- /src/pages/InventoryPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Modal} from '../components'; 4 | import MainLayout from '../containers/MainLayout'; 5 | import MainFormLayoutInventory from '../containers/MainFormLayoutInventory'; 6 | import PendingItemsLayout from '../containers/PendingItemsLayout'; 7 | import {withRouter} from 'react-router-dom'; 8 | import {InventoryContext} from '../context/InventoryContext'; 9 | import {AppContext} from '../context/AppContext'; 10 | import {AbstractPage} from './AbstractPage'; 11 | import pendingItemTypes from '../enums/enumPendingItemTypes'; 12 | import {connect} from 'react-redux'; 13 | import {addPendingInventoryItem, removeAllPendingItems} from '../actions/pendingItemsActions'; 14 | import {updateSearchResults} from '../actions/searchResultsActions'; 15 | import {updateSuppliers} from '../actions/suppliersActions'; 16 | import {updateSacksAndKilo, updateSackSelectedId, updateSackSelectedIdAndKilo} from '../actions/sacksActions'; 17 | import {updateItemBarcode, updateItemRemaining, updateItemText} from '../actions/itemActions'; 18 | import {enumSubmitConfirmTypes} from '../enums/enumSubmitConfirmTypes'; 19 | import formTypes from '../enums/enumFormTypes'; 20 | import {resetQuantity} from '../actions/quantityActions'; 21 | // import {updateKiloOnInput} from '../actions/kiloActions'; 22 | 23 | class InventoryPage extends AbstractPage { 24 | static contextType = AppContext; 25 | 26 | addPendingItems = () => { 27 | const item = { 28 | name: this.props.itemText, 29 | barcode: this.props.itemBarcode, 30 | quantity: this.props.quantity, 31 | kilo: this.props.kilo, 32 | }; 33 | 34 | if (!this.isValidFormInventory(item)) return; 35 | if (!this.removeDuplicate()) return; 36 | 37 | this.props.addPendingInventoryItem(item); 38 | 39 | this.closeForm(); 40 | this.resetForm(); 41 | }; 42 | 43 | handleSearchBarItemClick = (res) => { 44 | const itemText = res.name; 45 | const itemBarcode = res.id; 46 | const suppliers = res.suppliers; 47 | const sacks = res.sacks; 48 | const remaining = res.remaining; 49 | const formType = res.kiloAble ? formTypes.inventoryPerSack : formTypes.inventoryPerQuantity; 50 | 51 | this.addOpacityBlur(); 52 | 53 | this.closeSearchResults(); 54 | this.resetForm(); 55 | 56 | this.props.resetQuantity(); 57 | this.props.updateItemText(itemText); 58 | this.props.updateItemBarcode(itemBarcode); 59 | this.props.updateItemRemaining(remaining); 60 | this.props.updateSuppliers(suppliers); 61 | this.props.updateSacksAndKilo(sacks); 62 | 63 | this.showForm(formType); 64 | }; 65 | 66 | componentDidMount() { 67 | this.props.removeAllPendingItems(false); 68 | } 69 | 70 | render() { 71 | return ( 72 | 75 | 76 |
77 |
78 |
79 | 80 |
81 |
82 | 86 |
87 |
88 |
89 |
90 | {this.state.isConfirming && this.handleSubmitConfirm(enumSubmitConfirmTypes.INVENTORY_SUBMIT)} 92 | setState={this.context.setState.bind(this)} 93 | confirmItems={this.props.pendingItems.map((pi) => ({ 94 | id: pi.id, 95 | leftText: `${pi.quantity} x ${pi.name} ${pi.kilo > 0 ? `(${pi.kilo} kg)` : ''}`, 96 | rightText: pi.supplierName, 97 | }), 98 | )} 99 | />} 100 | {this.state.isLoading && } 101 | {this.state.isSuccess && } 102 | {this.state.isFailed && this.handleModalFailedClick(enumSubmitConfirmTypes.INVENTORY_SUBMIT)} 104 | handleClose={this.handleModalFailedClose}/>} 105 |
106 | ); 107 | } 108 | } 109 | 110 | export default withRouter(connect((state) => ({ 111 | pendingItems: state.pending.pendingItems, 112 | quantity: state.quantity, 113 | sacks: state.sacksStore.sacks, 114 | kilo: state.kilo, 115 | itemText: state.item.text, 116 | itemBarcode: state.item.barcode, 117 | searchResults: state.searchResults, 118 | }), { 119 | addPendingInventoryItem, 120 | updateSearchResults, 121 | updateSuppliers, 122 | updateSacksAndKilo, 123 | updateSackSelectedId, 124 | updateSackSelectedIdAndKilo, 125 | updateItemText, 126 | updateItemBarcode, 127 | removeAllPendingItems, 128 | updateItemRemaining, 129 | resetQuantity, 130 | }, 131 | )(InventoryPage)); 132 | -------------------------------------------------------------------------------- /src/pages/SalesPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Modal} from '../components'; 4 | import MainLayout from '../containers/MainLayout'; 5 | import MainFormLayoutSales from '../containers/MainFormLayoutSales'; 6 | import {withRouter} from 'react-router-dom'; 7 | import {SalesContext} from '../context/SalesContext'; 8 | import {AppContext} from '../context/AppContext'; 9 | import {AbstractPage} from './AbstractPage'; 10 | import PendingItemsLayout from '../containers/PendingItemsLayout'; 11 | import pendingItemTypes from '../enums/enumPendingItemTypes'; 12 | // import enumKiloType from '../enums/enumKiloType'; 13 | // import enumFormTypes from '../enums/enumFormTypes'; 14 | import {connect} from 'react-redux'; 15 | import {updateSearchResults} from '../actions/searchResultsActions'; 16 | import {updateSuppliers} from '../actions/suppliersActions'; 17 | import {updateSacks, updateSackSelectedId} from '../actions/sacksActions'; 18 | import {addPendingSalesItem, removeAllPendingItems} from '../actions/pendingItemsActions'; 19 | import {updatePrice, updatePriceBySackId} from '../actions/priceActions'; 20 | import {resetQuantity, updateQuantity} from '../actions/quantityActions'; 21 | import {updateItemBarcode, updateItemPrice, updateItemRemaining, updateItemText} from '../actions/itemActions'; 22 | import {updateKilo, updateKiloBySackId} from '../actions/kiloActions'; 23 | import {enumSubmitConfirmTypes} from '../enums/enumSubmitConfirmTypes'; 24 | import formTypes from '../enums/enumFormTypes'; 25 | import {resetDiscount} from '../actions/discountActions'; 26 | 27 | class SalesPage extends AbstractPage { 28 | static contextType = AppContext; 29 | 30 | addPendingItems = () => { 31 | const item = { 32 | name: this.props.itemText, 33 | barcode: this.props.itemBarcode, 34 | quantity: this.props.quantity, 35 | kilo: this.props.kilo, 36 | discount: this.props.discount, 37 | price: this.props.price, 38 | }; 39 | 40 | if (!this.isValidFormSales(item)) return; 41 | if (!this.removeDuplicate()) return; 42 | 43 | this.props.addPendingSalesItem(item); 44 | 45 | this.closeForm(); 46 | this.resetForm(); 47 | }; 48 | 49 | handleSearchBarItemClick = (res) => { 50 | const itemText = res.name; 51 | const itemBarcode = res.id; 52 | const suppliers = res.suppliers; 53 | const sacks = res.sacks; 54 | const price = res.price; 55 | const remaining = res.remaining; 56 | const formType = res.kiloAble ? formTypes.salesPerKilo : formTypes.salesPerQuantity; 57 | 58 | this.addOpacityBlur(); 59 | 60 | this.closeSearchResults(); 61 | this.resetForm(); 62 | 63 | this.props.resetQuantity(); 64 | this.props.resetDiscount(); 65 | this.props.updateKilo(0); 66 | this.props.updateItemText(itemText); 67 | this.props.updateItemBarcode(itemBarcode); 68 | this.props.updateItemPrice(price); // retain item kilo/qty price 69 | this.props.updateSuppliers(suppliers); 70 | this.props.updateItemRemaining(remaining); 71 | this.props.updateSacks(sacks); 72 | this.props.updatePrice(price); 73 | 74 | this.setState((prevState, props) => { 75 | return { 76 | // @TODO FORM TYPE MUST BE INCLUDED IN REDUX BEFORE REMOVING THIS 77 | mainForm: { 78 | ...prevState.mainForm, 79 | ...res, 80 | }, 81 | }; 82 | }); 83 | 84 | this.showForm(formType); 85 | }; 86 | 87 | componentDidMount() { 88 | this.props.removeAllPendingItems(false); 89 | } 90 | 91 | render() { 92 | return ( 93 | 96 | 97 |
98 |
99 |
100 | 101 |
102 |
103 | 107 |
108 |
109 |
110 |
111 | {this.state.isConfirming && this.handleSubmitConfirm(enumSubmitConfirmTypes.SALES_SUBMIT)} 113 | submitConfirmType={enumSubmitConfirmTypes.SALES_SUBMIT} 114 | setState={this.context.setState.bind(this)} 115 | confirmItems={this.props.pendingItems.map((pi) => ({ 116 | id: pi.id, 117 | leftText: `${pi.quantity} x ${pi.name} ${pi.kilo > 0 ? `(${pi.kilo} kg)` : ''}`, 118 | rightText: '₱'+(pi.price - pi.discount) * pi.quantity, 119 | }), 120 | )} 121 | />} 122 | {this.state.isLoading && } 123 | {this.state.isSuccess && } 124 | {this.state.isFailed && this.handleModalFailedClick(enumSubmitConfirmTypes.SALES_SUBMIT)} 126 | handleClose={this.handleModalFailedClose}/>} 127 |
128 | ); 129 | } 130 | } 131 | 132 | export default withRouter(connect((state) => ({ 133 | pendingItems: state.pending.pendingItems, 134 | quantity: state.quantity, 135 | sacks: state.sacksStore.sacks, 136 | itemText: state.item.text, 137 | itemBarcode: state.item.barcode, 138 | kilo: state.kilo, 139 | discount: state.discount, 140 | price: state.price, 141 | searchResults: state.searchResults, 142 | }), { 143 | addPendingSalesItem, 144 | updateSearchResults, 145 | updateSuppliers, 146 | updateSacks, 147 | updateSackSelectedId, 148 | updatePrice, 149 | resetQuantity, 150 | updateQuantity, 151 | updateItemText, 152 | updateItemBarcode, 153 | updateItemPrice, 154 | updateKilo, 155 | updateKiloBySackId, 156 | updatePriceBySackId, 157 | removeAllPendingItems, 158 | updateItemRemaining, 159 | resetDiscount, 160 | }, 161 | )(SalesPage)); 162 | -------------------------------------------------------------------------------- /src/pages/SelectionPage.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | 3 | import {Card, Nav} from '../components'; 4 | 5 | import Row from 'react-bootstrap/Row'; 6 | import InventoryLogo from '../assets/icons/inventory.svg'; 7 | import MoneyLogo from '../assets/icons/money.svg'; 8 | 9 | import {useHistory} from 'react-router-dom'; 10 | import {getRoute} from '../routeConfig'; 11 | import {AppContext} from '../context/AppContext'; 12 | 13 | const SelectionScreen = () => { 14 | const history = useHistory(); 15 | const {state: {user: {username}}} = useContext(AppContext); 16 | 17 | return ( 18 |
19 | 20 | 21 | 22 | 23 | history.push(getRoute('inventory'))} 26 | label="Inventory" 27 | className="mr-4" 28 | /> 29 | history.push(getRoute('sales'))} 33 | label="Sales" 34 | className="ml-4" 35 | // notAllowed 36 | /> 37 | 38 |
39 | ); 40 | }; 41 | 42 | export default SelectionScreen; 43 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import HomePage from './HomePage'; 2 | import InventoryPage from './InventoryPage'; 3 | import SelectionPage from './SelectionPage'; 4 | import SalesPage from './SalesPage'; 5 | 6 | export { 7 | HomePage, 8 | InventoryPage, 9 | SelectionPage, 10 | SalesPage, 11 | }; 12 | -------------------------------------------------------------------------------- /src/reducers/discountReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = 0; 4 | 5 | export const discountReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case types.UPDATE_DISCOUNT: 8 | return action.payload.discount; 9 | case types.RESET_DISCOUNT: 10 | return 0; 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/reducers/itemReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = { 4 | text: '', 5 | barcode: '', 6 | price: 0, 7 | remaining: 0, 8 | }; 9 | 10 | export const itemReducer = (state = initialState, action) => { 11 | switch (action.type) { 12 | case types.UPDATE_ITEM_TEXT: 13 | return {...state, text: action.payload.text}; 14 | case types.UPDATE_ITEM_BARCODE: 15 | return {...state, barcode: action.payload.barcode}; 16 | case types.UPDATE_ITEM_PRICE: 17 | return {...state, price: action.payload.price}; 18 | case types.UPDATE_ITEM_REMAINING: 19 | return {...state, remaining: action.payload.remaining}; 20 | default: 21 | return state; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/reducers/kiloReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = 0; 4 | 5 | export const kiloReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case types.UPDATE_KILO: 8 | return action.payload.kilo; 9 | default: 10 | return state; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/reducers/pendingItemsReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = {pendingItems: []}; 4 | 5 | export const pendingItemsReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case types.ADD_TO_PENDING_ITEMS: 8 | case types.REMOVE_ALL_PENDING_ITEMS: 9 | case types.REMOVE_SINGLE_PENDING_ITEMS: 10 | case types.ADD_TO_PENDING_SALES_ITEMS: 11 | return {pendingItems: action.payload.pendingItems}; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/reducers/priceReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = 0; 4 | 5 | export const priceReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case types.UPDATE_PRICE: 8 | return action.payload.price; 9 | default: 10 | return state; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/reducers/quantityReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = 0; 4 | 5 | export const quantityReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case types.UPDATE_QUANTITY: 8 | return action.payload.quantity; 9 | case types.RESET_QUANTITY: 10 | return 0; 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/reducers/sacksReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = { 4 | selectedSackId: '', 5 | sacks: [], 6 | }; 7 | 8 | export const sacksReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case types.UPDATE_SACKS: 11 | return {...state, sacks: action.payload.sacks}; 12 | case types.UPDATE_SACK_SELECTED_VALUE: 13 | return {...state, selectedSackId: action.payload.sackId}; 14 | default: 15 | return state; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/reducers/searchResultsReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = []; 4 | 5 | export const searchResultsReducer = (state = initialState, action) => { 6 | switch (action.type) { 7 | case types.UPDATE_SEARCH_RESULTS: 8 | return action.payload.searchResults; 9 | default: 10 | return state; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/reducers/suppliersReducers.js: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | const initialState = { 4 | supplierSelectedId: '', 5 | suppliers: [], 6 | }; 7 | 8 | export const suppliersReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case types.UPDATE_SUPPLIERS: 11 | return {...state, suppliers: action.payload.suppliers}; 12 | case types.UPDATE_SUPPLIER_SELECTED_ID: 13 | return {...state, supplierSelectedId: action.payload.supplierSelectedId}; 14 | default: 15 | return state; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/routeConfig.js: -------------------------------------------------------------------------------- 1 | export const routes = [ 2 | {name: 'home', path: '/'}, 3 | {name: 'inventory', path: '/inventory'}, 4 | {name: 'selection', path: '/selection'}, 5 | {name: 'sales', path: '/sales'}, 6 | ]; 7 | 8 | export const getRoute = (name) => { 9 | return routes.find((r) => r.name === name).path; 10 | }; 11 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | 3 | import Enzyme from 'enzyme'; 4 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 5 | 6 | Enzyme.configure({adapter: new Adapter()}); 7 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose, combineReducers} from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import {pendingItemsReducer} from './reducers/pendingItemsReducers'; 4 | import {quantityReducer} from './reducers/quantityReducers'; 5 | import {searchResultsReducer} from './reducers/searchResultsReducers'; 6 | import {suppliersReducer} from './reducers/suppliersReducers'; 7 | import {sacksReducer} from './reducers/sacksReducers'; 8 | import {kiloReducer} from './reducers/kiloReducers'; 9 | import {discountReducer} from './reducers/discountReducers'; 10 | import {priceReducer} from './reducers/priceReducers'; 11 | import {itemReducer} from './reducers/itemReducers'; 12 | 13 | const initialState = {}; 14 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 15 | const store = createStore( 16 | combineReducers({ 17 | pending: pendingItemsReducer, 18 | quantity: quantityReducer, 19 | searchResults: searchResultsReducer, 20 | suppliersStore: suppliersReducer, 21 | sacksStore: sacksReducer, 22 | kilo: kiloReducer, 23 | discount: discountReducer, 24 | price: priceReducer, 25 | item: itemReducer, 26 | }), 27 | initialState, 28 | composeEnhancer(applyMiddleware(thunk)), 29 | ); 30 | 31 | export default store; 32 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | export const ADD_TO_PENDING_ITEMS = 'ADD_TO_PENDING_ITEMS'; // inventory 2 | export const ADD_TO_PENDING_SALES_ITEMS = 'ADD_TO_PENDING_SALES_ITEMS'; // sales 3 | export const REMOVE_ALL_PENDING_ITEMS = 'REMOVE_ALL_PENDING_ITEMS'; 4 | export const REMOVE_SINGLE_PENDING_ITEMS = 'REMOVE_SINGLE_PENDING_ITEMS'; 5 | 6 | export const UPDATE_SEARCH_RESULTS = 'UPDATE_SEARCH_RESULTS'; 7 | 8 | export const UPDATE_ITEM_TEXT = 'UPDATE_ITEM_TEXT'; 9 | export const UPDATE_ITEM_BARCODE = 'UPDATE_ITEM_BARCODE'; 10 | export const UPDATE_ITEM_PRICE = 'UPDATE_ITEM_PRICE'; 11 | export const UPDATE_ITEM_REMAINING = 'UPDATE_ITEM_REMAINING'; 12 | 13 | export const UPDATE_QUANTITY = 'UPDATE_QUANTITY'; 14 | export const RESET_QUANTITY = 'RESET_QUANTITY'; 15 | 16 | export const UPDATE_SUPPLIERS = 'UPDATE_SUPPLIERS'; 17 | export const UPDATE_SUPPLIER_SELECTED_ID = 'UPDATE_SUPPLIER_SELECTED_ID'; 18 | 19 | export const UPDATE_PRICE = 'UPDATE_PRICE'; 20 | 21 | export const UPDATE_KILO = 'UPDATE_KILO'; 22 | 23 | export const UPDATE_DISCOUNT = 'UPDATE_DISCOUNT'; 24 | export const RESET_DISCOUNT = 'RESET_DISCOUNT'; 25 | 26 | export const UPDATE_SACKS = 'UPDATE_SACKS'; 27 | export const UPDATE_SACK_SELECTED_VALUE = 'UPDATE_SACK_SELECTED_VALUE'; 28 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | export const mapSearchResults = (d) => ({ 2 | id: d.id.toString(), 3 | barcode: d.id.toString(), 4 | name: `${d.item_group.item_name} ${d.unit_of_measurement} (${d.unit_of_measurement_value} KG)`, 5 | kiloAble: false, 6 | // sacks: d.item_kilos.map((ik) => ({sackId: ik.id.toString(), sackLabel: ik.label, sackValue: ik.value, sackPrice: ik.price})), 7 | sacks: [], 8 | suppliers: d.item_group.suppliers.map((s) => ({ 9 | id: s.id.toString(), 10 | supplierName: s.supplier_name, 11 | })), 12 | price: d.price, 13 | remaining: d.remaining, 14 | }); 15 | 16 | export const mapSacks = (s) => ({id: s.sackId.toString(), name: s.sackLabel, value: s.sackValue, price: s.sackPrice}); 17 | 18 | const checkRemaining = (remaining) => remaining > 0; 19 | 20 | export const isValidQuantity = (remaining, quantity) => (checkRemaining(remaining) && quantity <= remaining) || isNaN(quantity); 21 | --------------------------------------------------------------------------------