diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..4afece8 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "env", "stage-2" + ] +} \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a8f7c1a --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/server/build \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..05060ff --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "extends": "airbnb-base", + "rules": { + "linebreak-style":[ + "error", "windows" + ], + "arrow-body-style": [ + "error", "as-needed" + ], + "consistent-return": 0, + "array-callback-return": 0 + } + +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f8f1f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules +package-lock.json +/server/build +/.nyc_output +.env \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..59a5fd7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js + +node_js: + - 8 + +install: + - npm install + +test: + - npm test + +after_success: + - npm run coveralls \ No newline at end of file diff --git a/README.md b/README.md index 32e171e..e716587 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,104 @@ -# storemanager -A web application that helps store owners manage sales and product inventory records. +[![Build Status](https://travis-ci.org/Easybuoy/storemanager.svg?branch=develop)](https://travis-ci.org/Easybuoy/storemanager) +[![Coverage Status](https://coveralls.io/repos/github/Easybuoy/storemanager/badge.svg?branch=develop)](https://coveralls.io/github/Easybuoy/storemanager?branch=develop) +[![Maintainability](https://api.codeclimate.com/v1/badges/969d38484786692dd8c5/maintainability)](https://codeclimate.com/github/Easybuoy/storemanager/maintainability) +![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg) + +# Store Manager +A web application that helps store owners manage sales and product inventory records.. + +

Link To Pivotal Tracker: Pivotal Tracker

+ +
+

Store Manager UI Template

+ +

Built With

+ + + +

Link to template: Store Manager Template

+
+ +

Store Manager API Backend

+ +

Built With

+ + +

Testing Tools

+ + +

Link to API: Store Manager API

+ +

Getting Started

+

Prerequisites

+You need Nodejs Installed to be able to run this project on your machine. + +

Installing

+ +git clone https://github.com/Easybuoy/storemanager +
+
+ + +cd storemanager +
+
+ + +npm install +
+
+ + +npm run start +
+
+ + +npm run test +
+
+ + +
+npm run coveralls +
+
+ +

API Routes


+Register User => POST || /api/v1/users/register

+Login User => POST || /api/v1/users/login

+Get Current User Details => GET || /api/v1/users/current

+Create New Product => POST || /api/v1/products

+Get Products Details => GET || /api/v1/products

+Get Single Product Detail => GET || /api/v1/products/{productId}

+Create New Sale Record => POST || /api/v1/sales

+Get Sale Records => GET || /api/v1/sales

+Get Single Sale Record => GET || /api/v1/sales/{salesId}

+ + + + + + + + +

License

+

This project makes use of the MIT License which can be found here

\ No newline at end of file diff --git a/UI/admin_asign_product_to_category.html b/UI/admin_asign_product_to_category.html new file mode 100644 index 0000000..3e0ab65 --- /dev/null +++ b/UI/admin_asign_product_to_category.html @@ -0,0 +1,213 @@ + + + + + + + + + Store Manager |Assign Products + + + + + + + + + + + +
+ × +
  • Dashboard
  • +
  • Create Product
  • +
  • View Products
  • +
  • Create Sales Attendant
  • +
  • View Sales
  • +
  • Assign Products
  • +
  • Logout
  • +
    + +
    +
    +
    +
    +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + +
    + +
    +
    +
    +
    + +
    + + + + + + + + \ No newline at end of file diff --git a/UI/admin_create_product.html b/UI/admin_create_product.html new file mode 100644 index 0000000..cf3b507 --- /dev/null +++ b/UI/admin_create_product.html @@ -0,0 +1,83 @@ + + + + + + + + + Store Manager | Create Product + + + + + + + + + + + +
    + × +
  • Dashboard
  • +
  • Create Product
  • +
  • View Products
  • +
  • Create Sales Attendant
  • +
  • View Sales
  • +
  • Assign Products
  • +
  • Logout
  • +
    + +
    +
    +
    +
    +
    +

    Create New Product

    +
    +

    Product Name

    + +

    Product Summary

    + +

    Product Amount

    + +

    Product Quantity

    + +

    Product Image

    + + + + +
    +
    +
    +
    +
    + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/admin_create_sales_attendant.html b/UI/admin_create_sales_attendant.html new file mode 100644 index 0000000..24f6a8e --- /dev/null +++ b/UI/admin_create_sales_attendant.html @@ -0,0 +1,78 @@ + + + + + + + + + Store Manager | Create Sales Attendant + + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    Create Sales Attendant

    +
    +

    Name

    + +

    Email

    + +

    Password

    +
    + + +
    +
    +
    +
    +
    + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/admin_edit_product.html b/UI/admin_edit_product.html new file mode 100644 index 0000000..1ab9af9 --- /dev/null +++ b/UI/admin_edit_product.html @@ -0,0 +1,83 @@ + + + + + + + + + Store Manager | Edit Product + + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    Edit Product

    +
    +

    Product Name

    + +

    Product Summary

    + +

    Product Amount

    + +

    Product Quantity

    + +

    Product Image

    + + + + +
    +
    +
    +
    +
    + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/admin_view_products.html b/UI/admin_view_products.html new file mode 100644 index 0000000..28d5f75 --- /dev/null +++ b/UI/admin_view_products.html @@ -0,0 +1,180 @@ + + + + + + + + + Store Manager | View Products + + + + + + + + +
    +
  • Store Manager

  • + + + + + + +
    + +
    +
    +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + + +
    + +
    +
    +
    +
    +
    + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/admin_view_sales.html b/UI/admin_view_sales.html new file mode 100644 index 0000000..6c385b7 --- /dev/null +++ b/UI/admin_view_sales.html @@ -0,0 +1,101 @@ + + + + + + + + + Store Manager | View Sales + + + + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Sales
    DateStore AttendantProductAmountStatus
    04/10/2018Store Attendant 1iPhone Xs$1,190Completed
    04/10/2018Store Attendant 8Google Pixel 2$790Completed
    07/10/2018Store Attendant 121iPhone 7$500Completed
    +
    +
    +
    + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/attendant_admin_view.html b/UI/attendant_admin_view.html new file mode 100644 index 0000000..1f94ce4 --- /dev/null +++ b/UI/attendant_admin_view.html @@ -0,0 +1,193 @@ + + + + + + + + + Store Manager | Attendant Admin + + + + + + + + +
    +
  • Store Manager

  • + + + + +
    + × +
  • Dashboard
  • +
  • Products
  • +
  • Logout
  • + +
    + +
    + +
    +
    +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + +
    + +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + +
    + +
    +
    +
    +
    +
    + + + + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/css/k2d-webfont.woff b/UI/css/k2d-webfont.woff new file mode 100644 index 0000000..c1d07fe Binary files /dev/null and b/UI/css/k2d-webfont.woff differ diff --git a/UI/css/k2d-webfont.woff2 b/UI/css/k2d-webfont.woff2 new file mode 100644 index 0000000..52168c0 Binary files /dev/null and b/UI/css/k2d-webfont.woff2 differ diff --git a/UI/css/style.css b/UI/css/style.css new file mode 100644 index 0000000..7a12093 --- /dev/null +++ b/UI/css/style.css @@ -0,0 +1,1130 @@ +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + height: 100%; + margin: 0; + padding: 0; + } + + /* Sections + ========================================================================== */ + + + /** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + + h1 { + font-size: 2em; + margin: 0.67em 0; + } + + /* Grouping content + ========================================================================== */ + + /** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + + hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /* Text-level semantics + ========================================================================== */ + + /** + * Remove the gray background on active links in IE 10. + */ + + a { + background-color: transparent; + } + + /** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + + abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ + } + + /** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /** + * Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + + /* Embedded content + ========================================================================== */ + + /** + * Remove the border on images inside links in IE 10. + */ + + img { + border-style: none; + } + + /* Forms + ========================================================================== */ + + /** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ + } + + /** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + + button, + input { /* 1 */ + overflow: visible; + } + + /** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + + button, + select { /* 1 */ + text-transform: none; + } + + /** + * Correct the inability to style clickable types in iOS and Safari. + */ + + button, + [type="button"], + [type="reset"], + [type="submit"] { + -webkit-appearance: button; + } + + /** + * Remove the inner border and padding in Firefox. + */ + + button::-moz-focus-inner, + [type="button"]::-moz-focus-inner, + [type="reset"]::-moz-focus-inner, + [type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; + } + + /** + * Restore the focus styles unset by the previous rule. + */ + + button:-moz-focusring, + [type="button"]:-moz-focusring, + [type="reset"]:-moz-focusring, + [type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; + } + + /** + * Correct the padding in Firefox. + */ + + fieldset { + padding: 0.35em 0.75em 0.625em; + } + + /** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + + legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ + } + + /** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + + progress { + vertical-align: baseline; + } + + /** + * Remove the default vertical scrollbar in IE 10+. + */ + + textarea { + overflow: auto; + } + + /** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + + [type="checkbox"], + [type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + } + + /** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + + [type="number"]::-webkit-inner-spin-button, + [type="number"]::-webkit-outer-spin-button { + height: auto; + } + + /** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + + [type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ + } + + /** + * Remove the inner padding in Chrome and Safari on macOS. + */ + + [type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + + /** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + + ::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ + } + + /* Interactive + ========================================================================== */ + + /* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + + details { + display: block; + } + + /* + * Add the correct display in all browsers. + */ + + summary { + display: list-item; + } + + /* Misc + ========================================================================== */ + + /** + * Add the correct display in IE 10+. + */ + + template { + display: none; + } + + /** + * Add the correct display in IE 10. + */ + + [hidden] { + display: none; + } + + + +@font-face { + font-family: 'k2d_semiboldregular'; + src: url('k2d-webfont.woff2') format('woff2'), + url('k2d-webfont.woff') format('woff'); + font-weight: normal; + font-style: normal; + +} + + +body{ + font-family: 'k2d_semiboldregular', sans-serif; + font-size: 15px; + line-height: 1.5; + padding: 0; + margin: 0; + background-color: #f4f4f4; + overflow-x: hidden; + height: 100%; + /* min-height: 100%; */ + /* position: relative; */ +} + +/* body { + background-color: #f4f4f4; + overflow-x: hidden; +} */ + +/* Global */ +.container{ + width: 100%; + /* margin: 100px; */ + overflow: hidden; +} + +.text-center{ + text-align:center; +} + +.topmargin{ + margin-top: 100px; +} + +.dashboard{ + display: none !important; +} + + + +/* Navigation */ +nav .highlight, nav .current a{ + color: #0099ff; + font-weight: bold; + font-size: 22px; +} + +nav a:hover{ + color:#cccccc; + font-weight: bold; +} + +#appnameli{ + float: left; +} + +#appname{ + font-size: 25px; + margin: 0; + padding: 0; + +} + +#searchimg{ + width: 40px; + height: 40px; + margin: 0; + padding: 0px; + display: block; + float: right; + color: #fff; + background: inherit; +} + +#userimg{ + width: 40px; + height: 40px; + margin-top: 0; + padding: 0px; + display: block; + float: right; + color: #fff; + border-radius: 50%; + border: #0099ff 3px solid; +} + +#cartimage{ + width: 40px; + height: 40px; + margin: 0; + padding: 0px; + /* display: block; */ + float: right; + /* background: #fff; */ +} + +.navbar{ + background-color: #1c588a; + overflow: hidden; + height: 63px; + position: fixed; + top: 0; + z-index: 1; + width: 100%; + /* margin-bottom: 500px; */ + border-bottom: #0099ff 3px solid; + +} + +#openslidemenu{ +float: left; +} + +.navbar a{ + float: right; + /* display: block; */ + color: #f2f2f2; + text-align: center; + padding: 14px 16px; + text-decoration: none; + font-size: 17px; + /* margin-right: 10px; */ +} + + +.navbar ul { + margin: 8px 0 0 0; + list-style: none; +} + +/* .navbar a:hover { + background-color: #ddd; + color: black; +} */ + +.search{ + outline: none; + height: 30px; + color: black; + text-align: center; + /* background: #cccccc; */ + border: 2px solid #0099ff; + /* width: 30%; */ + font-size: 16px; +} + + +.side-nav{ + height: 100%; + width: 0; + position: fixed; + z-index: 1; + top: 0; + left: 0; + background-color: #111; + opacity: 0.9; + overflow-x: hidden; + padding-top: 60px; + transition: 0.5s; +} + +.side-nav a{ + padding: 10px 10px 10px 30px; + text-decoration: none; + font-size: 20px; + color: #ccc; + display: block; + transition: 0.3; + text-align: center; + text-transform: uppercase; +} + +.side-nav .current a{ + color: #0099ff; + font-weight: bold; +} + +.side-nav a:hover{ + color: #fff; + background: #0099ff; + +} + +.side-nav .btn-close{ + position: absolute; + top: 0; + right: 22px; + font-size: 25px; + margin-left: 50px; + background: inherit !important; +} + +.side-nav li{ + list-style: none; +} + +#side-menu{ + width: 250px; +} + +#main{ + transition: margin-left 0.5s; + overflow: hidden; + width: 100%; +} + +/* TopCards */ +.topcardsgroup { + display: flex ; + justify-content: space-between; + justify-items: flex-start; + +} + +#topcards{ + /* margin: 20px 0 20px 0; */ + margin-top: 100px; +} + + +.topcardsbox{ + background: linear-gradient(89deg, #4d4dff 0%, #80bfff 100%) !important; + width: 31%; + text-align: center; + margin: 0 auto; + height: 100%; +} + + +.showcasecard{ + padding: 5%; + transition: 0.9s; + -webkit-box-shadow: -1px 2px 7px -1px rgba(0,0,0,0.75); + -moz-box-shadow: -1px 2px 7px -1px rgba(0,0,0,0.75); + box-shadow: -1px 2px 7px -1px rgba(0,0,0,0.75); + box-sizing: border-box; +} + + + + + +/* Card */ + + +.cardgroup{ + width: 100%; + display: block; + text-align: center; +} + + + +.card { + background: #fff; + border-radius: 2px; + display: inline-block; + height: 400px; + margin: 1rem; + position: relative; + width: 300px; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + transition: all 0.3s cubic-bezier(.25,.8,.25,1); +} + + +.card:hover { + box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); +} + +.cardimg{ + width: 300px; + height: 150px; +} +.cardbody{ + margin-bottom: 10px; + background: #ccc; + height: 250px; +} + +.button_1{ + height: 38px; + background: #0099ff; + border: none; + padding-left: 20px; + padding-right: 20px; + color: #ffffff; + cursor: pointer; +} + +.button_2{ + height: 38px; + background: red; + border: none; + padding-left: 20px; + padding-right: 20px; + color: #ffffff; + cursor: pointer; +} + +.button_3{ + height: 38px; + background: green; + border: none; + padding-left: 20px; + padding-right: 20px; + color: #ffffff; + cursor: pointer; +} + +.button_1 a{ + text-decoration: none; + color: inherit; + background: inherit; + margin: 0; + padding: 0; +} + +input[type="number"]{ + border: none; + /* border-bottom: 1px solid black; */ + /* background: transparent; */ + outline: none; + height: 30px; + color: black; + text-align: center; + /* background: #cccccc; */ + border: 2px solid #0099ff; + width: 30%; + font-size: 16px; +} + +/* Table Styling */ + +.table{ + /* margin-left: 50px; */ + /* margin-right: 50px; + padding: 50px; */ + /* width: 80%; */ + +} +table { + border: 1px solid #ccc; + border-collapse: collapse; + margin: 0px auto; + padding: 0; + width: 80%; + table-layout: fixed; +} + +table caption { + font-size: 1.5em; + margin: .5em 0 .75em; +} + +table tr { + background-color: #f8f8f8; + border: 1px solid #ddd; + padding: .35em; +} + +table th, +table td { + padding: .625em; + text-align: center; +} + +table th { + font-size: .85em; + letter-spacing: .1em; + text-transform: uppercase; +} + + + + + + + +/* End Of Table Styling */ +.footerstickbottom{ + position: absolute !important; + bottom:0; + left: 0; +} + +#footer{ + padding: 20px; + margin-top: 20px; + /* height: 100px; */ + color: #ffffff; + background: #1c588a; + text-align: center; + position: relative; + width:100%; + } + +.wrapper { + min-height: 100%; + /* margin-bottom: -20px; */ + /* position: absolute; */ + /* margin: 0 auto; */ +} + +#productviewsection{ + display: flex; +} + +.productviewcardimg{ + width: 300px; + height: 300px; +} + +#productdetailsimg{ + float: left; + margin-left: 100px; + /* width: 50%; */ + +} + +#productdetails{ +float: right; +margin-right: 100px; +width: 50%; +} + + +.list-group { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; +} + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid rgba(0,0,0,.125); + overflow-wrap: break-word; + +} + + + + +/* Login Page */ +#loginbody{ + margin: 0; + padding: 0; + background: url(../img/background.jpg) no-repeat; + background-size: cover; + background-position: center; + font-family: sans-serif; + height: 100%; +} + +/* Styling For Login Page */ +.loginbox{ + width: 320px; + height: auto; + background: #000; + color: #fff; + top: 50%; + left: 50%; + position: absolute; + transform: translate(-50%,-50%); + box-sizing: border-box; + padding: 70px 30px; +} + +.avatar{ + width: 100px; + height: 100px; + border-radius: 50%; + position: absolute;; + top: -50px; + left: calc(50% - 50px); +} + +/* h1{ + margin: 0; + padding: 0 0 20px; + text-align: center; + font-size: 22px; +} */ + +.loginbox p{ + margin: 0; + padding: 0; + font-weight: bold; +} + +.loginbox input{ + width: 100%; + margin-bottom: 20px; +} + +.loginbox input[type="text"], input[type="password"]{ + border: none; + border-bottom: 1px solid #fff; + background: transparent; + outline: none; + height: 40px; + color: #fff; + font-size: 16px; +} + +#loginpassword { + color: #fff !important; +} +.loginbox input[type="submit"]{ + border: none; + outline: none; + height: 40px; + background: #0099ff; + color: #fff; + font-size: 18px; + border-radius: 20px; +} + +.loginbox input[type="submit"]:hover{ + cursor: pointer; + background: #99ebff; + color: #000; +} + +.loginbox a{ + text-decoration: none; + font-size: 12px; + line-height: 20px; + color: darkgray; +} + +.loginbox a:hover{ + color: #99ebff; +} +/* End Of Stying For Login Page */ + + +/* Shopping Cart Modal Styling */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + padding-top: 100px; /* Location of the box */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +.modal-content { + background-color: #fefefe; + margin: auto; + padding: 20px; + border: 1px solid #888; + width: 80%; +} + +/* The Close Button */ +.close { + color: #aaaaaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +/* End Of Shopping Cart Modal Styling */ + +#prof { + margin: 0 auto; + width: 50%; +} + +#profiledetails { + /* width: 60%; */ + /* margin: 50px; */ + text-align: center; + display: inline; + float: none; + justify-content: center; +} + +/* Styling For Admin Create Sales Attendant Page */ +select{ + width: 50%; + overflow: hidden !important; + height: 30px; + outline: none; + /* margin: 20px; */ +} + +#create_sales_assistant{ + width: 50%; + /* text-align: center; */ + margin: 0 auto; + padding: 5%; +} + +#create_sales_assistant p{ + margin: 0; + padding: 0; + font-weight: bold; +} + +#create_sales_assistant h1{ + text-transform: uppercase; + margin-bottom: 2%; +} + +#create_sales_assistant input{ + width: 100%; + margin-bottom: 20px; +} + +#create_sales_assistant input[type="text"], input[type="password"], input[type="email"]{ + border: none; + border-bottom: 1px solid #0099ff; + background: transparent; + outline: none; + height: 40px; + color: #000; + font-size: 16px; +} + +#create_sales_assistant input[type="submit"]{ + border: none; + outline: none; + height: 40px; + background: #0099ff; + color: #fff; + font-size: 18px; + border-radius: 20px; +} + +#create_sales_assistant input[type="submit"]:hover{ + cursor: pointer; + background: #99ebff; + color: #000; +} + + +/* End Of Styling For Admin Create Sales Attendant Page */ + + +@media (max-width: 1020px) { + .card { + width: 450px; + } + .cardimg{ + width: 450px; + } +} + +@media (max-width: 985px) { + .card { + width: 400px; + } + .cardimg{ + width: 400px; + } +} + +@media (max-width: 885px) { + .card { + width: 300px; + } + .cardimg{ + width: 300px; + } +} + +@media (max-width: 687px) { + .card { + width: 100%; + } + .cardimg{ + width: 100%; + } +} + + +@media screen and (max-width: 600px) { + table { + border: 0; + } + + table caption { + font-size: 1.3em; + } + + table thead { + border: none; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } + + table tr { + border-bottom: 3px solid #ddd; + display: block; + margin-bottom: .625em; + } + + table td { + border-bottom: 1px solid #ddd; + display: block; + font-size: .8em; + text-align: right; + } + + table td::before { + /* + * aria-label has no advantage, it won't be read inside a table + content: attr(aria-label); + */ + content: attr(data-label); + float: left; + font-weight: bold; + text-transform: uppercase; + } + + table td:last-child { + border-bottom: 0; + } +} + +@media (max-width: 568px) { + .dashboard{ + display: block !important; + } + + .topcardsgroup { + display: block; + } + .topcardsbox{ + display: block; + text-align: center; + /* float: ; */ + display: block !important; + width: 100%; + padding: 5px; + text-align: center; + margin: 5px; + } + + .navbar-nav{ + display: none; + } + + #side-menu{ + width: 0; + } + + #main{ + margin-left: 0; + } +} + +/* @media (min-width: 568px) { + .open-slide{ + display: none; + } +} */ \ No newline at end of file diff --git a/UI/dashboard.html b/UI/dashboard.html new file mode 100644 index 0000000..3d2708a --- /dev/null +++ b/UI/dashboard.html @@ -0,0 +1,237 @@ + + + + + + + + + Store Manager | Welcome + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +

    Total Sales Record Created

    +
    + +

    999

    +
    + + +
    +
    + +
    +
    +

    Total Products Sold

    +
    + +

    850

    +
    +
    +
    +
    +
    +

    Total Goods Sold Worth

    +
    + +

    $30,322.53

    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + + +
    + +
    +
    +
    +
    +
    + + + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/img/avatar.png b/UI/img/avatar.png new file mode 100644 index 0000000..6f16b6b Binary files /dev/null and b/UI/img/avatar.png differ diff --git a/UI/img/background.jpg b/UI/img/background.jpg new file mode 100644 index 0000000..285167a Binary files /dev/null and b/UI/img/background.jpg differ diff --git a/UI/img/cart.png b/UI/img/cart.png new file mode 100644 index 0000000..0b8418c Binary files /dev/null and b/UI/img/cart.png differ diff --git a/UI/img/cart1.png b/UI/img/cart1.png new file mode 100644 index 0000000..eb6d9de Binary files /dev/null and b/UI/img/cart1.png differ diff --git a/UI/img/favicon.ico b/UI/img/favicon.ico new file mode 100644 index 0000000..a8d9515 Binary files /dev/null and b/UI/img/favicon.ico differ diff --git a/UI/img/gpixel2.jpg b/UI/img/gpixel2.jpg new file mode 100644 index 0000000..aafb3db Binary files /dev/null and b/UI/img/gpixel2.jpg differ diff --git a/UI/img/gpixel3.jpg b/UI/img/gpixel3.jpg new file mode 100644 index 0000000..cfd4034 Binary files /dev/null and b/UI/img/gpixel3.jpg differ diff --git a/UI/img/gpixel3r.png b/UI/img/gpixel3r.png new file mode 100644 index 0000000..0e27bed Binary files /dev/null and b/UI/img/gpixel3r.png differ diff --git a/UI/img/iphonex.png b/UI/img/iphonex.png new file mode 100644 index 0000000..d93d42d Binary files /dev/null and b/UI/img/iphonex.png differ diff --git a/UI/img/iphonexs.png b/UI/img/iphonexs.png new file mode 100644 index 0000000..175e1b0 Binary files /dev/null and b/UI/img/iphonexs.png differ diff --git a/UI/img/product.jpeg b/UI/img/product.jpeg new file mode 100644 index 0000000..b87082f Binary files /dev/null and b/UI/img/product.jpeg differ diff --git a/UI/img/search.png b/UI/img/search.png new file mode 100644 index 0000000..93c0d04 Binary files /dev/null and b/UI/img/search.png differ diff --git a/UI/img/showcase.jpg b/UI/img/showcase.jpg new file mode 100644 index 0000000..c5dafd8 Binary files /dev/null and b/UI/img/showcase.jpg differ diff --git a/UI/index.html b/UI/index.html new file mode 100644 index 0000000..addb972 --- /dev/null +++ b/UI/index.html @@ -0,0 +1,32 @@ + + + + + + + + + Store Manager | Login + + + + + + +
    + +

    Login

    +
    +

    Username

    + +

    Password

    + + + + Lost your password?
    + Don't have an account +
    +
    + + + \ No newline at end of file diff --git a/UI/js/main.js b/UI/js/main.js new file mode 100644 index 0000000..5deaea9 --- /dev/null +++ b/UI/js/main.js @@ -0,0 +1,104 @@ +'use strict'; + +const openSlideMenu = () => { + document.getElementById('side-menu').style.width = '250px'; + document.getElementById('main').style.marginLeft = '250px'; +}; + + +const closeSlideMenu = () =>{ + document.getElementById('side-menu').style.width = '0'; + document.getElementById('main').style.marginLeft = '0'; +}; + +const addtocart = (nameofproduct, amountofproduct) => { + const quantity = document.getElementById('number').value; + let product = {}; + let data = []; + + let previouscartitem = JSON.parse(localStorage.getItem('product')); + if (previouscartitem) { + data = previouscartitem; + amountofproduct = amountofproduct.replace('Price: ', '') + product = { + id: previouscartitem.id++, + productname: nameofproduct, + productquantity: quantity, + productamount: amountofproduct + }; + data.push(product); + } else { + amountofproduct = amountofproduct.replace('Price: ', ''); + product = { + id: 1, + productname: nameofproduct, + productquantity: quantity, + productamount: amountofproduct + }; + data.push(product); + } + + localStorage.setItem('product', JSON.stringify(data)); + const totalcartitem = JSON.parse(localStorage.getItem('product')); + document.getElementById('shoppingcartlabel').innerHTML = totalcartitem.length; +showCart(totalcartitem); +}; + +// This function shows cart in table format +const showCart = (data) => { + if (data === undefined) { + data = JSON.parse(localStorage.getItem('product')); + } + if (data == null || data == undefined) { + let cartmodaldiv = document.getElementById('cartmodaldiv'); + cartmodaldiv.style.display = 'none'; + cartmodaldiv.style.backgroundColor = 'white'; + } else { + let cartmodaldiv = document.getElementById('cartmodaldiv'); + cartmodaldiv.style.display = 'block'; + let carttablebody = document.getElementById('carttablebody'); + carttablebody.innerHTML = data.map((val) => { + return `${val.productname}${val.productamount}${val.productquantity}`; + }).join(''); + } +}; + +// THis function clears the cart and reloads the page +const clearCart = () => { + localStorage.clear(); + return window.location.reload(); +}; + +// Populate shopping cart +let totalcartitem = JSON.parse(localStorage.getItem('product')); +if (totalcartitem) { + document.getElementById('shoppingcartlabel').innerHTML = totalcartitem.length; +} + +showCart(); + +// Get the modal +const modal = document.getElementById('myModal'); + +// Get the button that opens the modal +const btn = document.getElementById("shoppingcart"); + +// Get the element that closes the modal +const span = document.getElementsByClassName("close")[0]; + +// When the user clicks the button, open the modal +btn.onclick = function () { + modal.style.display = "block"; +}; + +// When the user clicks on (x), close the modal +span.onclick = function () { + modal.style.display = "none"; +}; + +// When the user clicks anywhere outside of the modal, close it +window.onclick = function (event) { + if (event.target == modal) { + modal.style.display = "none"; + } +}; diff --git a/UI/sales_new.html b/UI/sales_new.html new file mode 100644 index 0000000..1ea1579 --- /dev/null +++ b/UI/sales_new.html @@ -0,0 +1,203 @@ + + + + + + + + + Store Manager | Add Products + + + + + + + + +
    +
  • Store Manager

  • + + + + + + +
    + +
    +
    +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 2

    +

    The Google Pixel 2 is powered by 1.9GHz octa-core

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Google Pixel 3

    +

    The Pixel 3 is the latest causality. Wireless charging is..

    +

    Quantity: 3

    +

    Price: $649

    + + +
    + +
    + +
    + + +
    +

    Iphone XS

    +

    The iPhone XS display has rounded corners that...

    +

    Quantity: 3

    +

    Price: $1500

    + + +
    + +
    + +
    + + +
    +

    IPhone X

    +

    The phone comes with a 5.80-inch touchscreen display

    +

    Quantity: 3

    +

    Price: $890

    + + +
    + +
    +
    +
    +
    +
    + + + + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/sales_view.html b/UI/sales_view.html new file mode 100644 index 0000000..6e24072 --- /dev/null +++ b/UI/sales_view.html @@ -0,0 +1,118 @@ + + + + + + + + + Store Manager | View Sales + + + + + + + + + + + + + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Sales
    DateProductAmountStatus
    04/10/2018iPhone Xs$1,190Completed
    04/10/2018Google Pixel 2$790Completed
    07/10/2018iPhone 7$500Completed
    +
    +
    + +
    + + + +
    +

    Store Manager Copyright © 2018

    +
    + + + + + \ No newline at end of file diff --git a/UI/store_attendant_profile.html b/UI/store_attendant_profile.html new file mode 100644 index 0000000..215a9d3 --- /dev/null +++ b/UI/store_attendant_profile.html @@ -0,0 +1,136 @@ + + + + + + + + + Store Manager | View Profile + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +

    Total Sales Record Created

    +
    + +

    999

    +
    + + +
    +
    + +
    +
    +

    Total Products Sold

    +
    + +

    850

    +
    +
    +
    +
    +
    +

    Total Goods Sold Worth

    +
    + +

    $30,322.53

    +
    +
    +
    + +
    +
    + +
    +
    + + +
    +
    + + +
    +
      +
    • Name: Store Attendant 1
    • +
    • Email: attendant1@gmail.com
    • +
    +
    + +
    +
    + + + + + + + + + + + \ No newline at end of file diff --git a/UI/view_product_details.html b/UI/view_product_details.html new file mode 100644 index 0000000..6372b36 --- /dev/null +++ b/UI/view_product_details.html @@ -0,0 +1,100 @@ + + + + + + + + + Store Manager | Add Products + + + + + + + + + + + +
    + × +
  • Dashboard
  • +
  • Logout
  • + +
    + +
    + +
    +
    +
    + +
    + + +
    + +
    +
      +
    • Product Name: Google Pixel 2
    • +
    • Description: The Google Pixel 2 is powered by 1.9GHz octa-core processor and it comes with 4GB of RAM. The phone packs 64GB of internal storage that cannot be expanded.
    • +
    • Price: $649
    • +
    • Quantity: 3
    • +
    +
    +
    +
    +
    + +
    + + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9dc5fed --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "storemanager", + "version": "1.0.0", + "description": "API backend for store manager application.", + "main": "server/app.js", + "scripts": { + "dev": "nodemon server/app", + "start": "npm run clean-dev && babel server -d server/build/dev && node server/build/dev/app", + "test": "mocha server/test --compilers js:babel-core/register --exit || true", + "test-dev": "npm run clean-dev && npm run clean && babel server -d server/build/dev && mocha --recursive server/build/dev --exit || true", + "lint": "eslint ./server", + "clean": "rm -rf server/build", + "clean-dev": "rm -rf server/build/dev", + "build": "npm run clean && babel server -d server/build && node server/build/app", + "coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Easybuoy/storemanager.git" + }, + "engines": { + "npm": "5.5.1", + "node": "8.9.1" + }, + "nyc": { + "exclude": [ + "server/app.js", + "server/config/keys.js" + ] + }, + "author": "Ekunola Ezekiel", + "license": "ISC", + "bugs": { + "url": "https://github.com/Easybuoy/storemanager/issues" + }, + "homepage": "https://github.com/Easybuoy/storemanager#readme", + "dependencies": { + "babel-preset-stage-2": "^6.24.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.18.3", + "chai": "^4.2.0", + "chai-http": "^4.2.0", + "cors": "^2.8.4", + "coveralls": "^3.0.2", + "dotenv": "^6.1.0", + "eslint-config-airbnb-base": "^13.1.0", + "eslint-plugin-import": "^2.14.0", + "express": "^4.16.4", + "jsonwebtoken": "^8.3.0", + "mocha": "^5.2.0", + "morgan": "^1.9.1", + "multer": "^1.4.1", + "nodemon": "^1.18.4", + "nyc": "^13.0.1", + "pg": "^7.5.0", + "validator": "^10.8.0" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-preset-env": "^1.7.0", + "eslint": "^4.19.1" + } +} diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..a1954d8 --- /dev/null +++ b/server/app.js @@ -0,0 +1,55 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import morgan from 'morgan'; + +import products from './routes/api/v1/products'; +import sales from './routes/api/v1/sales'; +import users from './routes/api/v1/users'; + +const app = express(); + +// Body Parser Middleware +app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); + +app.use(cors()); + +// Make uploads folder available publicly +app.use('/uploads', express.static('uploads')); + + +app.get('/', (req, res) => { + res.json({ message: 'Welcome To Store Manager API' }); +}); + +// Use morgan to log requests. +app.use(morgan('dev')); + +// using routes +app.use('/api/v1/products', products); +app.use('/api/v1/sales', sales); +app.use('/api/v1/users', users); + +app.use((req, res, next) => { + const error = new Error('Not found'); + error.status = 404; + next(error); +}); + +app.use((error, req, res, next) => { + res.status(error.status || 500); + res.json({ + error: { + message: error.message, + }, + }); + next(); +}); + +const port = process.env.PORT || 3000; + +// eslint-disable-next-line no-console +app.listen(port, () => console.log(`sever listening on port ${port}`)); + +module.exports = app; diff --git a/server/controllers/productController.js b/server/controllers/productController.js new file mode 100644 index 0000000..d975749 --- /dev/null +++ b/server/controllers/productController.js @@ -0,0 +1,99 @@ +import db from '../models/mockdb'; + +import productsValidation from '../validation/products'; + +class productController { + // @route POST api/v1/products + // @desc This function implements the logic for creating new product. + // @access Private + static createProduct(req, res) { + const { errors, isValid } = productsValidation.validateProductInput(req.body); + // Check validation + if (!isValid) { + return res.status(400).json(errors); + } + + let productImage = 'uploads\\products\\default.png'; + if (req.file) { + productImage = req.file.path; + } + + let id = db.products.length; + id += 1; + const host = req.get('host'); + const data = { + id, + name: req.body.name, + description: req.body.description, + quantity: req.body.quantity, + price: { + currency: '$', + amount: req.body.price, + }, + productImage, + }; + + db.products.push(data); + + data.request = { + method: 'GET', + url: `${host}/api/v1/products/${id}`, + }; + + return res.status(201).json({ message: 'Product added successfully', data }); + } + + + // @route GET api/v1/products + // @desc This function implements the logic for getting all products. + // @access Private + static getProducts(req, res) { + res.json(db.products); + } + + // @route GET api/v1/products/ + // @desc This function implements the logic for getting a product detail by Id. + // @access Private + static getProductById(req, res) { + const { id } = req.params; + + const product = db.products[id - 1]; + if (!product) { + return res.status(400).json({ message: `Product with id ${id} not found.` }); + } + + return res.json(product); + } + + // @route DELETE api/v1/products/ + // @desc This function implements the logic for deleting a product by Id. + // @access Private + static deleteProductById(req, res) { + // Checks if user making the request is the store owner / admin + if (!(Number(req.user.type) === 1)) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + const { id } = req.params; + + const dbidtoberemoved = id - 1; + + const product = db.products[dbidtoberemoved]; + + if (!product) { + return res.status(400).json({ message: `Product with id ${id} not found.` }); + } + + if (product.id !== Number(id)) { + return res.status(400).json({ message: `Product with id ${id} not found.` }); + } + + if (db.products.splice(dbidtoberemoved, 1)) { + return res.json({ message: `Product with id ${id} deleted successfully.` }); + } + + return res.json({ message: 'Unable to delete product.' }); + } +} + +export default productController; diff --git a/server/controllers/saleController.js b/server/controllers/saleController.js new file mode 100644 index 0000000..dbd046a --- /dev/null +++ b/server/controllers/saleController.js @@ -0,0 +1,118 @@ +import db from '../models/mockdb'; +import salesValidation from '../validation/sales'; + +class salesControler { + // @route POST api/v1/sales + // @desc This function implements the logic for creating a new sale. + // @access Private + static createSale(req, res) { + const processSale = (order) => { + let isMoreThanStock = false; + let isNotProductAvailable = false; + let totalSalesAmount = 0; + order.map((ordertobeprocessed) => { + const { quantity } = ordertobeprocessed; + const productId = ordertobeprocessed.product_id; + + const product = db.products[productId - 1]; + + if (!product) { + isNotProductAvailable = true; + return false; + } + + const productPrice = Number(product.price.amount); + const productQuantity = Number(product.quantity); + // Check if quantity in stock for product is more than quantity requested + if (quantity > product.quantity) { + isMoreThanStock = true; + return false; + } + + const totalamount = quantity * productPrice; + + // Update 'product table' by reducing quantity left in store from what user just bought + product.quantity = productQuantity - quantity; + totalSalesAmount += totalamount; + return { status: 200, totalamount }; + }); + + if (isNotProductAvailable) { + return { status: 400, message: 'One Of Product Requested Is Not Available' }; + } + + if (isMoreThanStock) { + return { status: 400, message: 'One Of Product Requested Is More Than In Stock' }; + } + + const response = { amount: totalSalesAmount, currency: '$' }; + return response; + }; + + const { errors, isValid } = salesValidation.validateSalesInput(req.body); + + // Check validation + if (!isValid) { + return res.status(400).json(errors); + } + + const processedSale = processSale(req.body.order); + // Check if there is error processing sale + if (processedSale.status === 400) { + return res.status(400).json({ message: processedSale.message }); + } + + let id = db.sales.length; + id += 1; + + const data = { + id, + store_attendant_user_id: req.user.id, + order: req.body.order, + totalSaleAmount: processedSale, + date_time: new Date(), + }; + db.sales.push(data); + + return res.status(201).json({ message: 'Sale added successfully', data }); + } + + // @route GET api/v1/sales + // @desc This function implements the logic for getting all sale records. + // @access Private + static getSales(req, res) { + res.json(db.sales); + } + + // @route GET api/v1/sales/ + // @desc This function implements the logic for getting a sale details by Id. + // @access Private + static getSaleById(req, res) { + // check if user making the request is the Store Owner / Admin + + if (!Number(req.user.type) === 1 || !Number(req.user.type) === 3) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + const { id } = req.params; + + const sales = db.sales[id - 1]; + + if (sales) { + if (Number(req.user.type) !== 1) { + // check if user making the request is the store attendant that made the sale + if (req.user.id !== sales.store_attendant_user_id) { + return res.status(401).json({ message: 'Unauthorized' }); + } + } + } + + if (!sales) { + return res.status(400).json({ message: `Sales with id ${id} not found.` }); + } + + return res.json(sales); + } +} + +export default salesControler; diff --git a/server/controllers/userController.js b/server/controllers/userController.js new file mode 100644 index 0000000..bb7e732 --- /dev/null +++ b/server/controllers/userController.js @@ -0,0 +1,125 @@ +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +/* eslint-disable import/first */ +import dotenv from 'dotenv'; + +dotenv.config(); +import db from '../models/mockdb'; +// Load Input validation +import usersValidation from '../validation/users'; + +const { SECRET_OR_KEY } = process.env; +class usersController { + // @route POST api/users/register + // @desc This function implements the logic for registering a new user. + // @access Private + static signup(req, res) { + const { errors, isValid } = usersValidation.validateSignupInput(req.body); + + // Check validation + if (!isValid) { + return res.status(400).json(errors); + } + + const { + email, password, name, type, + } = req.body; + let exist = false; + db.user.map((user) => { + if (user.email === email) { + exist = true; + return exist; + } + }); + if (exist) { + return res.status(409).json({ email: 'Email Already Exist' }); + } + + let id = db.user.length; + id += 1; + + const data = { + id, + email, + password, + name, + status: 1, + type, + date_time: new Date(), + }; + + bcrypt.genSalt(10, (err, salt) => { + // Check if there is error generating salt + if (err) { + return res.status(500).json({ message: 'Error Creating User, Try again ' }); + } + + bcrypt.hash(data.password, salt, (error, hash) => { + if (error) throw error; + data.password = hash; + db.user.push(data); + res.status(201).json({ message: 'User Created Successfully', data }); + }); + }); + } + + // @route POST api/users/login + // @desc This function implements the logic to loggin a user. + // @access Public + static login(req, res) { + const { errors, isValid } = usersValidation.validateLoginInput(req.body); + + // Check validation + if (!isValid) { + return res.status(400).json(errors); + } + + let exist = false; + let userData = {}; + const { email, password } = req.body; + db.user.map((user) => { + if (user.email === email) { + exist = true; + userData = user; + return user; + } + }); + + if (exist === false) { + return res.status(404).json({ email: 'User Not Found' }); + } + + bcrypt.compare(password, userData.password) + .then((isMatch) => { + if (isMatch) { + // User Matched + const payload = { + id: userData.id, + email: userData.email, + name: userData.name, + type: userData.type, + }; + // Sign Token + jwt.sign(payload, SECRET_OR_KEY, { expiresIn: 3600 }, (err, token) => { + res.json({ success: true, token: `Bearer ${token}` }); + }); + } else { + return res.status(401).json({ password: 'Incorrect Password' }); + } + }); + } + + // @route POST api/users/current + // @desc This function implements the logic for getting the current user + // details with token parsed. + // @access Private + static getCurrentUser(req, res) { + res.json({ + id: req.user.id, + name: req.user.name, + email: req.user.email, + }); + } +} + +export default usersController; diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js new file mode 100644 index 0000000..34533c4 --- /dev/null +++ b/server/middleware/authenticate.js @@ -0,0 +1,48 @@ +import jwt from 'jsonwebtoken'; +/* eslint-disable import/first */ +import dotenv from 'dotenv'; + +dotenv.config(); + +const { SECRET_OR_KEY } = process.env; + +class Authenticate { + + static isLoggedIn(req, res, next) { + try { + const token = req.headers.authorization.split(' ')[1]; + const decoded = jwt.verify(token, SECRET_OR_KEY); + req.user = decoded; + next(); + } catch (e) { + return res.status(401).json({ message: 'Unauthorized' }); + } + } + + // Checks if user making the request is the store owner / admin + static isAdmin(req, res, next) { + if (req.user.type !== 1) { + return res.status(401).json({ message: 'Unauthorized' }); + } + next(); + } + + // Checks if user making the request is the store attendant admin + // static isStoreAttendantAdmin(req, res, next) { + // if (req.user.type !== 2) { + // return res.status(401).json({ message: 'Unauthorized' }); + // } + // next(); + // } + + + // Checks if user making the request is the store attendant + static isStoreAttendant(req, res, next) { + if (req.user.type !== 3) { + return res.status(401).json({ message: 'Unauthorized' }); + } + next(); + } +} + +export default Authenticate; diff --git a/server/models/mockdb.js b/server/models/mockdb.js new file mode 100644 index 0000000..ac406af --- /dev/null +++ b/server/models/mockdb.js @@ -0,0 +1,100 @@ +module.exports = { + products: [ + { + id: 1, + name: 'Google Pixel 2', + description: 'The Google Pixel 2 is powered by 1.9GHz octa-core processor and it comes with 4GB of RAM. The phone packs 64GB of internal storage that cannot be expanded.', + quantity: 50, + price: { + currency: '$', + amount: '649', + }, + productImage: 'uploads\\products\\default.png', + }, + { + id: 2, + name: 'Google Pixel 3', + description: 'The Pixel 3 is the latest causality. Wireless charging is a new feature for the Pixel phones, and a welcome change now that Google is launching the Pixel Stand wireless charger alongside its new devices.', + quantity: 75, + price: { + currency: '$', + amount: '649', + }, + productImage: 'uploads\\products\\default.png', + }, + { + id: 3, + name: 'iPhone 7 Plus', + description: 'The phone comes with a 5.50-inch touchscreen display with a resolution of 1080 pixels by 1920 pixels at a PPI of 401 pixels per inch.', + quantity: 125, + price: { + currency: '$', + amount: '649', + }, + productImage: 'uploads\\products\\default.png', + }, + ], + sales: [ + { + id: 1, + store_attendant_user_id: 2, + order: [{ + product_id: '2', + quantity: '5', + }, + { + product_id: '2', + quantity: '10', + }], + totalSaleAmount: '50000', + date_time: '2018-10-12T13:04:51.884Z', + + }, + { + id: 2, + store_attendant_user_id: 1, + order: [{ + product_id: '2', + quantity: '5', + }, + { + product_id: 3, + quantity: 4, + }], + totalSaleAmount: '30000', + date_time: '2018-10-12T13:04:51.884Z', + }, + ], + user: [ + { + id: 1, + email: 'example@gmail.com', + password: '$2a$10$/SwT..8jeE8Bo0sOUZ7mc.yR1EXFrKA/5pa9RWj8LbsctDzJsG4EO', + name: 'Ezekiel Example', + status: 1, + type: 1, + userImage: 'uploads\\user\\default.png', + date_time: '2018-10-12T13:04:51.884Z', + }, + { + id: 2, + email: 'example2@gmail.com', + password: '$2a$10$/SwT..8jeE8Bo0sOUZ7mc.yR1EXFrKA/5pa9RWj8LbsctDzJsG4EO', + name: 'Example Example', + status: 1, + type: 3, + userImage: 'uploads\\user\\default.png', + date_time: '2018-10-12T13:04:51.884Z', + }, + { + id: 3, + email: 'example3@gmail.com', + password: '$2a$10$/SwT..8jeE8Bo0sOUZ7mc.yR1EXFrKA/5pa9RWj8LbsctDzJsG4EO', + name: 'Example Example', + status: 1, + type: 2, + userImage: 'uploads\\user\\default.png', + date_time: '2018-10-12T13:04:51.884Z', + }, + ], +}; diff --git a/server/routes/api/v1/products.js b/server/routes/api/v1/products.js new file mode 100644 index 0000000..296b174 --- /dev/null +++ b/server/routes/api/v1/products.js @@ -0,0 +1,65 @@ +import express from 'express'; +// import multer from 'multer'; + +import authenticate from '../../../middleware/authenticate'; +import productController from '../../../controllers/productController'; + +const { isLoggedIn, isAdmin } = authenticate; +const { + createProduct, getProducts, getProductById, deleteProductById, +} = productController; + +// const fileFilter = (req, file, cb) => { +// // reject a file +// if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') { +// cb(null, true); +// } else { +// cb(null, false); +// } +// }; +// const storage = multer.diskStorage({ +// destination: (req, file, cb) => { +// cb(null, 'uploads/products/'); +// }, +// filename: (req, file, cb) => { +// cb(null, new Date().getTime() + file.originalname); +// }, +// }); + +// const upload = multer( +// { +// storage, +// limits: { +// fileSize: 1024 * 1024 * 5, + +// }, +// fileFilter, +// }, +// ); + +const router = express.Router(); + +// @route GET api/v1/products +// @desc Get/Fetch all products +// @access Private +router.get('/', isLoggedIn, getProducts); + + +// @route GET api/v1/products/ +// @desc Get/Fetch a single product record +// @access Private +router.get('/:id', isLoggedIn, getProductById); + +// @route POST api/v1/products/ +// @desc Create a product +// @access Private +// router.post('/', authenticate, upload.single('productImage'), createProduct); +router.post('/', isLoggedIn, isAdmin, createProduct); + + +// @route DELETE api/v1/products/ +// @desc Delete a single product record +// @access Private +router.delete('/:id', isLoggedIn, isAdmin, deleteProductById); + +module.exports = router; diff --git a/server/routes/api/v1/sales.js b/server/routes/api/v1/sales.js new file mode 100644 index 0000000..fe97984 --- /dev/null +++ b/server/routes/api/v1/sales.js @@ -0,0 +1,27 @@ +import express from 'express'; + +import authenticate from '../../../middleware/authenticate'; +// import db from '../../../models/db'; +import saleController from '../../../controllers/saleController'; + +const { isLoggedIn, isAdmin, isStoreAttendant } = authenticate; +const { createSale, getSales, getSaleById } = saleController; +const router = express.Router(); + +// @route GET api/v1/sales +// @desc Get/Fetch all sale records +// @access Private +router.get('/', isLoggedIn, isAdmin, getSales); + + +// @route GET api/v1/sales/ +// @desc Get/Fetch a single sale record +// @access Private +router.get('/:id', isLoggedIn, getSaleById); + +// @route POST api/v1/sales +// @desc Create a sale order +// @access Private +router.post('/', isLoggedIn, isStoreAttendant, createSale); + +module.exports = router; diff --git a/server/routes/api/v1/users.js b/server/routes/api/v1/users.js new file mode 100644 index 0000000..d2d43bc --- /dev/null +++ b/server/routes/api/v1/users.js @@ -0,0 +1,29 @@ +import express from 'express'; + +import authenticate from '../../../middleware/authenticate'; +import usersController from '../../../controllers/userController'; + +const { login, signup, getCurrentUser } = usersController; + +const { isLoggedIn, isAdmin } = authenticate; +const router = express.Router(); + +// @route POST api/v1/users/register +// @desc Register user +// @access Private +router.post('/signup', isLoggedIn, isAdmin, signup); + + +// @route POST api/v1/users/register +// @desc Login user / Returning JWT Token +// @access Public +router.post('/login', login); + + +// @route GET api/v1/users/current +// @desc Return current user +// @access Private +router.get('/current', isLoggedIn, getCurrentUser); + + +module.exports = router; diff --git a/server/test/.eslintrc.json b/server/test/.eslintrc.json new file mode 100644 index 0000000..f22b7c0 --- /dev/null +++ b/server/test/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "env": { + "node": true, + "mocha": true + } +} \ No newline at end of file diff --git a/server/test/appTest.js b/server/test/appTest.js new file mode 100644 index 0000000..4aad728 --- /dev/null +++ b/server/test/appTest.js @@ -0,0 +1,28 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; + +import app from '../app'; + +const { expect } = chai; + +chai.use(chaiHttp); + +describe('App', () => { + it('returns welcome to API message', (done) => { + chai.request(app).get('/') + .end((err, res) => { + expect(res).to.have.status(200); + expect(res.body.message).to.equal('Welcome To Store Manager API'); + }); + done(); + }); + + it('returns 404 because route does not exist', (done) => { + chai.request(app).post('/') + .end((err, res) => { + expect(res).to.have.status(404); + expect(res.body.error.message).to.equal('Not found'); + }); + done(); + }); +}); diff --git a/server/test/productsTest.js b/server/test/productsTest.js new file mode 100644 index 0000000..6f83cc2 --- /dev/null +++ b/server/test/productsTest.js @@ -0,0 +1,267 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import app from '../app'; + +const { expect } = chai; + +chai.use(chaiHttp); + +describe('Get Products', () => { + it('returns array of all products', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).get('/api/v1/products') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(200); + expect(data.body).to.be.an('array'); + done(); + }); + }); + }); + + it('returns unauthorized because user is not logged in', (done) => { + chai.request(app).get('/api/v1/products') + .end((error, res) => { + expect(res).to.have.status(401); + done(); + }); + }); +}); + +describe('Get A Product', () => { + it('returns details of a product', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + const id = 2; + chai.request(app).get(`/api/v1/products/${id}`) + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(200); + expect(id).to.equal(data.body.id); + done(); + }); + }); + }); + + it('return product not found error', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + const id = 89; + chai.request(app).get(`/api/v1/products/${id}`) + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(400); + expect(res.body).to.be.an('object'); + expect(data.body.message).to.equal(`Product with id ${id} not found.`); + done(); + }); + }); + }); + + it('returns unauthorized because user is not logged in', (done) => { + const id = 2; + chai.request(app).get(`/api/v1/products/${id}`) + .end((error, res) => { + expect(res).to.have.status(401); + done(); + }); + }); +}); + +describe('Create New Product', () => { + it('return unauthorized because user is not admin', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example2@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/products') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(401); + expect(data.body.message).to.equal('Unauthorized'); + done(); + }); + }); + }); + + it('return validation error if no data is sent', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/products') + .set('Authorization', token) + .send({ + name: 'iPhone', + }) + .end((error, data) => { + expect(data).to.have.status(400); + expect(data.body).to.be.an('object'); + // expect(data.body.name).to.equal('Name field is required'); + expect(data.body.description).to.equal('Description field is required'); + expect(data.body.price).to.equal('Price field is required'); + expect(data.body.quantity).to.equal('Quantity field is required'); + done(); + }); + }); + }); + + + it('create a new product', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/products') + .send({ + name: 'Tecno', description: 'Tecno Phone', quantity: '2', price: '$200', + }) + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(201); + expect(data.body).to.be.an('object'); + expect(data.body.message).to.equal('Product added successfully'); + done(); + }); + }); + }); + + it('returns unauthorized because user is not logged in', (done) => { + chai.request(app).post('/api/v1/products') + .send({ + name: 'Tecno', description: 'Tecno Phone', quantity: '2', price: '$200', + }) + .end((error, res) => { + expect(res).to.have.status(401); + done(); + }); + }); +}); + + +describe('Delete A Product', () => { + it('should delete a product', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).del('/api/v1/products/3') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(200); + expect(data.body).to.be.an('object'); + expect(data.body.message).to.equal('Product with id 3 deleted successfully.'); + done(); + }); + }); + }); + + it('should return unauthorized because user does not have right access', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example2@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).del('/api/v1/products/3') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(401); + done(); + }); + }); + }); + + it('should return product not found', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).del('/api/v1/products/59') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(400); + expect(data.body).to.be.an('object'); + expect(data.body.message).to.equal('Product with id 59 not found.'); + done(); + }); + }); + }); + + it('should return product not found because the product has just been deleted', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).del('/api/v1/products/2') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(200); + expect(data.body).to.be.an('object'); + chai.request(app).del('/api/v1/products/2') + .set('Authorization', token) + .end((error2, data2) => { + expect(data2).to.have.status(400); + expect(data2.body).to.be.an('object'); + expect(data2.body.message).to.equal('Product with id 2 not found.'); + done(); + }); + }); + }); + }); +}); diff --git a/server/test/salesTest.js b/server/test/salesTest.js new file mode 100644 index 0000000..6178f0c --- /dev/null +++ b/server/test/salesTest.js @@ -0,0 +1,254 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import app from '../app'; + +const { expect } = chai; + +chai.use(chaiHttp); + +describe('Get All Sale Records', () => { + it('returns array of all sale records', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).get('/api/v1/sales') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(200); + expect(data.body).to.be.an('array'); + done(); + }); + }); + }); + + it('returns error because only store owner / admin has access to view all sales', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example2@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).get('/api/v1/sales') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(401); + done(); + }); + }); + }); + + it('returns unauthorized because user is not logged in', (done) => { + chai.request(app).get('/api/v1/sales') + .end((error, res) => { + expect(res).to.have.status(401); + done(); + }); + }); +}); + +describe('Get A Sale Record', () => { + it('returns details of a sale record', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + const id = 2; + chai.request(app).get(`/api/v1/sales/${id}`) + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(200); + expect(id).to.equal(data.body.id); + done(); + }); + }); + }); + + it('returns unauthorized because he/she did not create the sale || is not store owner / admin', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example3@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + const id = 2; + chai.request(app).get(`/api/v1/sales/${id}`) + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(401); + done(); + }); + }); + }); + + it('return sale not found error', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + const id = 299; + chai.request(app).get(`/api/v1/sales/${id}`) + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(400); + expect(data.body).to.be.an('object'); + expect(data.body.message).to.equal(`Sales with id ${id} not found.`); + done(); + }); + }); + }); + + it('returns unauthorized because user is not logged in', (done) => { + const id = 2; + chai.request(app).get(`/api/v1/sales/${id}`) + .end((error, res) => { + expect(res).to.have.status(401); + done(); + }); + }); +}); + +describe('Create New Sale Record', () => { + it('create a new sale', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example2@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/sales') + .send({ + order: [{ quantity: 2, product_id: 2 }, { quantity: 8, product_id: 1 }], + }) + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(201); + expect(data.body).to.be.an('object'); + expect(data.body.message).to.equal('Sale added successfully'); + done(); + }); + }); + }); + + it('return validation error if no data is sent', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example2@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/sales') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(400); + expect(data.body).to.be.an('object'); + done(); + }); + }); + }); + + it('return error because quantity of product requested is more than quantity in store', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example2@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/sales') + .set('Authorization', token) + .send({ + order: [{ quantity: 200, product_id: 2 }, { quantity: 8, product_id: 1 }], + }) + .end((error, data) => { + expect(data).to.have.status(400); + expect(data.body).to.be.an('object'); + expect(data.body.message).to.equal('One Of Product Requested Is More Than In Stock'); + done(); + }); + }); + }); + + it('return error because one of product requested is not available in store', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example2@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/sales') + .set('Authorization', token) + .send({ + order: [{ quantity: 200, product_id: 299 }, { quantity: 8, product_id: 3 }], + }) + .end((error, data) => { + expect(data).to.have.status(400); + expect(data.body).to.be.an('object'); + expect(data.body.message).to.equal('One Of Product Requested Is Not Available'); + done(); + }); + }); + }); + + it('return unauthorized because only store attendant can create a sale record', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/sales') + .set('Authorization', token) + .send({ + order: [{ quantity: 2, product_id: 2 }, { quantity: 8, product_id: 3 }], + }) + .end((error, data) => { + expect(data).to.have.status(401); + done(); + }); + }); + }); + + it('returns unauthorized because user is not logged in', (done) => { + chai.request(app).post('/api/v1/sales') + .end((error, res) => { + expect(res).to.have.status(401); + done(); + }); + }); +}); diff --git a/server/test/userTest.js b/server/test/userTest.js new file mode 100644 index 0000000..e5c4045 --- /dev/null +++ b/server/test/userTest.js @@ -0,0 +1,202 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import app from '../app'; + +const { expect } = chai; + +chai.use(chaiHttp); + +describe('Signup Route', () => { + it('create a user and return user details', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', + password: '123456', + }) + .end((loginerr, loginres) => { + const { token } = loginres.body; + expect(loginres).to.have.status(200); + expect(loginres.body).to.be.an('object'); + expect(loginres.body.success).to.equal(true); + expect(loginres.body.token).to.include('Bearer'); + + chai.request(app).post('/api/v1/users/signup') + .set('Authorization', token) + .send({ + email: 'a@gmail.com', + name: 'John Example', + password: '123456', + type: '1', + }) + .end((err, res) => { + expect(res).to.have.status(201); + expect(res.body).to.be.an('object'); + expect(res.body.message).to.equal('User Created Successfully'); + done(); + }); + }); + }); + + it('return validation error if no data is sent', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', + password: '123456', + }) + .end((loginerr, loginres) => { + const { token } = loginres.body; + expect(loginres).to.have.status(200); + expect(loginres.body).to.be.an('object'); + expect(loginres.body.success).to.equal(true); + expect(loginres.body.token).to.include('Bearer'); + chai.request(app).post('/api/v1/users/signup') + .set('Authorization', token) + .end((err, res) => { + expect(res).to.have.status(400); + expect(res.body).to.be.an('object'); + expect(res.body.email).to.equal('Email field is required'); + expect(res.body.password).to.equal('Password field is required'); + expect(res.body.name).to.equal('Name field is required'); + expect(res.body.type).to.equal('Type field is required'); + done(); + }); + }); + }); + + it('return email already exist', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', + password: '123456', + }) + .end((loginerr, loginres) => { + const { token } = loginres.body; + expect(loginres).to.have.status(200); + expect(loginres.body).to.be.an('object'); + expect(loginres.body.success).to.equal(true); + expect(loginres.body.token).to.include('Bearer'); + chai.request(app).post('/api/v1/users/signup') + .set('Authorization', token) + .send({ + email: 'example@gmail.com', + name: 'John Example', + password: '123456', + type: '1', + }) + .end((err, res) => { + expect(res).to.have.status(409); + expect(res.body).to.be.an('object'); + expect(res.body.email).to.equal('Email Already Exist'); + done(); + }); + }); + }); +}); + + +describe('Login Route', () => { + it('return token', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', + password: '123456', + }) + .end((err, res) => { + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + expect(res.body.token).to.include('Bearer'); + + done(); + }); + }); + + it('return validation error if no data is sent', (done) => { + chai.request(app).post('/api/v1/users/login') + .end((err, res) => { + expect(res).to.have.status(400); + expect(res.body).to.be.an('object'); + expect(res.body.email).to.equal('Email field is required'); + expect(res.body.password).to.equal('Password field is required'); + + done(); + }); + }); + + it('return user not found', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example232@gmail.com', + password: '123456', + }) + .end((err, res) => { + expect(res).to.have.status(404); + expect(res.body).to.be.an('object'); + expect(res.body.email).to.equal('User Not Found'); + done(); + }); + }); + + it('return incorrect password', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', + password: '1234', + }) + .end((err, res) => { + expect(res).to.have.status(401); + expect(res.body).to.be.an('object'); + expect(res.body.password).to.equal('Incorrect Password'); + done(); + }); + }); +}); + +describe('Get Current user', () => { + it('returns details of current user', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).get('/api/v1/users/current') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(200); + expect(data.body).to.be.an('object'); + done(); + }); + }); + }); + + it('returns unauthorized because user is not logged in', (done) => { + chai.request(app).get('/api/v1/users/current') + .end((error, data) => { + expect(data).to.have.status(401); + done(); + }); + }); + + it('returns 404 error because post method is not allowed', (done) => { + chai.request(app).post('/api/v1/users/login') + .send({ + email: 'example@gmail.com', password: '123456', + }) + .end((err, res) => { + const { token } = res.body; + expect(res).to.have.status(200); + expect(res.body).to.be.an('object'); + expect(res.body.success).to.equal(true); + chai.request(app).post('/api/v1/users/current') + .set('Authorization', token) + .end((error, data) => { + expect(data).to.have.status(404); + done(); + }); + }); + }); +}); diff --git a/server/test/usersValidationTest.js b/server/test/usersValidationTest.js new file mode 100644 index 0000000..b95853c --- /dev/null +++ b/server/test/usersValidationTest.js @@ -0,0 +1,51 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; + +import usersValidation from '../validation/users'; + +const { expect } = chai; + +chai.use(chaiHttp); + +describe('Login Validation', () => { + it('returns empty object because all validation is passed', (done) => { + const result = usersValidation.validateLoginInput({ email: 'example@gmail.com', password: '123456' }); + expect(result.isValid).to.equal(true); + expect(Object.keys(result.errors).length).to.equal(0); + done(); + }); + + it('returns object of validation required', (done) => { + const result = usersValidation.validateLoginInput({}); + expect(result.isValid).to.equal(false); + expect(Object.keys(result.errors).length).to.be.greaterThan(0); + expect(result.errors.email).to.equal('Email field is required'); + expect(result.errors.password).to.equal('Password field is required'); + expect(result.errors).to.be.an('object'); + done(); + }); +}); + + +describe('Login Validation', () => { + it('returns empty object because all validation is passed', (done) => { + const result = usersValidation.validateLoginInput( + { + email: 'example@gmail.com', password: '123456', name: 'Example', type: '2', + }, + ); + expect(result.isValid).to.equal(true); + expect(Object.keys(result.errors).length).to.equal(0); + done(); + }); + + it('returns object of validation required', (done) => { + const result = usersValidation.validateLoginInput({}); + expect(result.isValid).to.equal(false); + expect(Object.keys(result.errors).length).to.be.greaterThan(0); + expect(result.errors.email).to.equal('Email field is required'); + expect(result.errors.password).to.equal('Password field is required'); + expect(result.errors).to.be.an('object'); + done(); + }); +}); diff --git a/server/test/validationTest.js b/server/test/validationTest.js new file mode 100644 index 0000000..3b799a9 --- /dev/null +++ b/server/test/validationTest.js @@ -0,0 +1,120 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; + +import usersValidation from '../validation/users'; +import productsValidation from '../validation/products'; +import salesValidation from '../validation/sales'; +import isEmpty from '../validation/isEmpty'; + +const { expect } = chai; + +chai.use(chaiHttp); + +describe('Login Validation', () => { + it('returns empty object because all validation is passed', (done) => { + const result = usersValidation.validateLoginInput({ email: 'example@gmail.com', password: '123456' }); + expect(result.isValid).to.equal(true); + expect(Object.keys(result.errors).length).to.equal(0); + done(); + }); + + it('returns object of validation required', (done) => { + const result = usersValidation.validateLoginInput({}); + expect(result.isValid).to.equal(false); + expect(Object.keys(result.errors).length).to.be.greaterThan(0); + expect(result.errors.email).to.equal('Email field is required'); + expect(result.errors.password).to.equal('Password field is required'); + expect(result.errors).to.be.an('object'); + done(); + }); +}); + + +describe('Signup Validation', () => { + it('returns empty object because all validation is passed', (done) => { + const result = usersValidation.validateSignupInput( + { + email: 'example@gmail.com', password: '123456', name: 'Example', type: '2', + }, + ); + expect(result.isValid).to.equal(true); + expect(Object.keys(result.errors).length).to.equal(0); + done(); + }); + + it('returns object of validation required', (done) => { + const result = usersValidation.validateSignupInput({}); + expect(result.isValid).to.equal(false); + expect(Object.keys(result.errors).length).to.be.greaterThan(0); + expect(result.errors.email).to.equal('Email field is required'); + expect(result.errors.password).to.equal('Password field is required'); + expect(result.errors.name).to.equal('Name field is required'); + expect(result.errors.type).to.equal('Type field is required'); + expect(result.errors).to.be.an('object'); + done(); + }); +}); + + +describe('Product Validation', () => { + it('returns empty object because all validation is passed', (done) => { + const result = productsValidation.validateProductInput( + { + name: 'Samsung Galaxy', description: 'Good Phone', price: '300', quantity: '29', + }, + ); + expect(result.isValid).to.equal(true); + expect(Object.keys(result.errors).length).to.equal(0); + done(); + }); + + it('returns object of validation required', (done) => { + const result = productsValidation.validateProductInput({}); + expect(result.isValid).to.equal(false); + expect(Object.keys(result.errors).length).to.be.greaterThan(0); + expect(result.errors.name).to.equal('Name field is required'); + expect(result.errors.description).to.equal('Description field is required'); + expect(result.errors.price).to.equal('Price field is required'); + expect(result.errors.quantity).to.equal('Quantity field is required'); + expect(result.errors).to.be.an('object'); + done(); + }); +}); + + +describe('Sales Validation', () => { + it('returns empty object because all validation is passed', (done) => { + const result = salesValidation.validateSalesInput( + { + order: [{ product_id: '2', quantity: '5' }], + }, + ); + expect(result.isValid).to.equal(true); + expect(Object.keys(result.errors).length).to.equal(0); + done(); + }); + + it('returns object of validation required', (done) => { + const result = salesValidation.validateSalesInput({}); + expect(result.isValid).to.equal(false); + expect(Object.keys(result.errors).length).to.be.greaterThan(0); + expect(result.errors.order).to.equal('Order must be an array of object(s)'); + expect(result.errors).to.be.an('object'); + done(); + }); +}); + + +describe('isEmpty Function', () => { + it('returns false because input passed is not empty', (done) => { + const result = isEmpty('example@gmail.com'); + expect(result).to.equal(false); + done(); + }); + + it('returns true because input passed is empty', (done) => { + const result = isEmpty(''); + expect(result).to.equal(true); + done(); + }); +}); diff --git a/server/validation/isEmpty.js b/server/validation/isEmpty.js new file mode 100644 index 0000000..34b1339 --- /dev/null +++ b/server/validation/isEmpty.js @@ -0,0 +1,8 @@ +const isEmpty = (value) => { + if (value === undefined || value === null || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim().length === 0)) { + return true; + } + return false; +}; + +module.exports = isEmpty; diff --git a/server/validation/products.js b/server/validation/products.js new file mode 100644 index 0000000..1327fcb --- /dev/null +++ b/server/validation/products.js @@ -0,0 +1,36 @@ +import Validator from 'validator'; +import isEmpty from './isEmpty'; + +const validateProductInput = (input) => { + const errors = {}; + const data = input; + data.name = !isEmpty(data.name) ? data.name : ''; + data.description = !isEmpty(data.description) ? data.description : ''; + data.quantity = !isEmpty(data.quantity) ? data.quantity : ''; + data.price = !isEmpty(data.price) ? data.price : ''; + + if (Validator.isEmpty(data.name)) { + errors.name = 'Name field is required'; + } + + if (Validator.isEmpty(data.description)) { + errors.description = 'Description field is required'; + } + + if (Validator.isEmpty(data.price)) { + errors.price = 'Price field is required'; + } + + if (Validator.isEmpty(data.quantity)) { + errors.quantity = 'Quantity field is required'; + } + + return { + errors, + isValid: isEmpty(errors), + }; +}; + +module.exports = { + validateProductInput, +}; diff --git a/server/validation/sales.js b/server/validation/sales.js new file mode 100644 index 0000000..f0b5b74 --- /dev/null +++ b/server/validation/sales.js @@ -0,0 +1,22 @@ +import isEmpty from './isEmpty'; + +const validateSalesInput = (input) => { + const errors = {}; + const data = input; + data.quantity = !isEmpty(data.quantity) ? data.quantity : ''; + data.product_id = !isEmpty(data.product_id) ? data.product_id : ''; + + if (!Array.isArray(data.order)) { + errors.order = 'Order must be an array of object(s)'; + } + + + return { + errors, + isValid: isEmpty(errors), + }; +}; + +module.exports = { + validateSalesInput, +}; diff --git a/server/validation/users.js b/server/validation/users.js new file mode 100644 index 0000000..10afc7a --- /dev/null +++ b/server/validation/users.js @@ -0,0 +1,78 @@ +import Validator from 'validator'; +import isEmpty from './isEmpty'; + +const validateSignupInput = (input) => { + const errors = {}; + const data = input; + data.name = !isEmpty(data.name) ? data.name : ''; + data.email = !isEmpty(data.email) ? data.email : ''; + data.password = !isEmpty(data.password) ? data.password : ''; + data.type = !isEmpty(data.type) ? data.type : ''; + + + if (!Validator.isLength(data.name, { min: 2, max: 30 })) { + errors.name = 'Name must be between 2 and 30 characters'; + } + + if (Validator.isEmpty(data.name)) { + errors.name = 'Name field is required'; + } + + if (Validator.isEmpty(data.email)) { + errors.email = 'Email field is required'; + } + + if (!errors.email) { + if (!Validator.isEmail(data.email)) { + errors.email = 'Email is invalid'; + } + } + + if (Validator.isEmpty(data.password)) { + errors.password = 'Password field is required'; + } + + if (!errors.password) { + if (!Validator.isLength(data.password, { min: 6, max: 30 })) { + errors.password = 'Password must be at least 6 characters'; + } + } + + if (Validator.isEmpty(data.type)) { + errors.type = 'Type field is required'; + } + + return { + errors, + isValid: isEmpty(errors), + }; +}; + +const validateLoginInput = (input) => { + const errors = {}; + const data = input; + data.email = !isEmpty(data.email) ? data.email : ''; + data.password = !isEmpty(data.password) ? data.password : ''; + + if (!Validator.isEmail(data.email)) { + errors.email = 'Email is invalid'; + } + + if (Validator.isEmpty(data.email)) { + errors.email = 'Email field is required'; + } + + if (Validator.isEmpty(data.password)) { + errors.password = 'Password field is required'; + } + + return { + errors, + isValid: isEmpty(errors), + }; +}; + +module.exports = { + validateSignupInput, + validateLoginInput, +}; diff --git a/uploads/products/default.png b/uploads/products/default.png new file mode 100644 index 0000000..4cf4597 Binary files /dev/null and b/uploads/products/default.png differ diff --git a/uploads/users/default-avatar.png b/uploads/users/default-avatar.png new file mode 100644 index 0000000..e7596cc Binary files /dev/null and b/uploads/users/default-avatar.png differ diff --git a/uploads/users/default.png b/uploads/users/default.png new file mode 100644 index 0000000..ddcb797 Binary files /dev/null and b/uploads/users/default.png differ